memleaf 0.2.6__tar.gz → 0.2.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {memleaf-0.2.6 → memleaf-0.2.8}/CHANGELOG.md +25 -0
- {memleaf-0.2.6/src/memleaf.egg-info → memleaf-0.2.8}/PKG-INFO +3 -3
- {memleaf-0.2.6 → memleaf-0.2.8}/README.md +2 -2
- {memleaf-0.2.6 → memleaf-0.2.8}/pyproject.toml +1 -1
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/__init__.py +1 -1
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/hermes_provider/__init__.py +3 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/hermes_provider/plugin.yaml +1 -1
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/processing.py +48 -10
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/prompts.py +77 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/scope_state.py +14 -2
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/validation.py +122 -3
- {memleaf-0.2.6 → memleaf-0.2.8/src/memleaf.egg-info}/PKG-INFO +3 -3
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_admission_noise.py +9 -11
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_cross_host_acceptance.py +4 -1
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_hermes_provider.py +36 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_maintenance_v2.py +8 -6
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_pypi_install.py +1 -1
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_session_lineage.py +2 -2
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_stage_b1.py +197 -4
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_stage_b2a.py +351 -17
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_stage_b3b_scope.py +29 -8
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_stage_b3d_scope_maintenance.py +11 -2
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_stage_c1_mcp.py +1 -1
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_stage_c3_packaging.py +1 -1
- {memleaf-0.2.6 → memleaf-0.2.8}/IMPLEMENTATION_PLAN.md +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/LICENSE +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/MANIFEST.in +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/README.en.md +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/RELEASE_CHECKLIST.md +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/examples/README.md +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/examples/basic_usage.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/examples/mcp_stdio.ndjson +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/install.ps1 +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/install.sh +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/setup.cfg +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/__main__.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/adapters/__init__.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/adapters/antigravity.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/adapters/base.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/adapters/codex.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/adapters/hermes.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/budget.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/capture.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/cli.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/compaction.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/config.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/credentials.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/frontmatter.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/hermes_provider/README.md +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/host_events.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/host_runtime.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/inbox.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/index.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/installer.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/llm/__init__.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/llm/base.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/llm/claude_compatible.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/llm/gemini.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/llm/openai_compatible.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/llm/router.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/locking.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/mcp_server.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/memory_writer.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/model_discovery.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/models.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/native_index.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/redaction.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/retrieval.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/retrieval_gate.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/scope_maintenance.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/service.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf/vault.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf.egg-info/SOURCES.txt +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf.egg-info/dependency_links.txt +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf.egg-info/entry_points.txt +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/src/memleaf.egg-info/top_level.txt +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/__init__.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_codex_install.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_codex_native_cli.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_context_budget.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_credential_safety.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_hermes_stdio_transport.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_host_events.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_host_runtime_contract.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_install.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_model_discovery.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_retrieval_gate.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_retrieval_v2.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_stage_a.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_stage_b2b.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_stage_b3a_commit.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_stage_b3a_contract.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_stage_b3b_native_context.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_stage_b3b_native_index.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_stage_b3c_retrieval.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_stage_c2_init.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_upgrade_preserves_vault.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_v2_gate_limits.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_v2_host_flow.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_v2_mcp_flow.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/tests/test_v2_nomatch_semantics.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.8}/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.8 — 2026-09-01
|
|
6
|
+
|
|
7
|
+
- Let the automatic summarizer independently reject an over-admitted gate
|
|
8
|
+
candidate with an exact `NO_CHANGE` response, without writing memory,
|
|
9
|
+
history, or Scope state; explicit remember remains write-oriented and does
|
|
10
|
+
not permit this response.
|
|
11
|
+
- Ground every model-attributed project Scope in the candidate's own project
|
|
12
|
+
name or registered alias, and prevent the summarizer from drifting away
|
|
13
|
+
from the gate-selected Scope.
|
|
14
|
+
- Register session and Vault Scopes only for committed memories or trusted
|
|
15
|
+
active duplicate targets, so temporary, deferred, and no-change mailbox
|
|
16
|
+
items cannot pollute Scope state.
|
|
17
|
+
|
|
18
|
+
## 0.2.7 — 2026-09-01
|
|
19
|
+
|
|
20
|
+
- Distinguish an invalid gate type enum from a candidate type that conflicts
|
|
21
|
+
with its selected active update target, while preserving the strict,
|
|
22
|
+
immutable target-type constraint.
|
|
23
|
+
- Give each gate failure a bounded, actionable retry instruction: use a legal
|
|
24
|
+
memory type, preserve the target type for the same future use, or split a
|
|
25
|
+
genuinely different future use into an independent atomic candidate.
|
|
26
|
+
- Preserve the dedicated `update_target_type_mismatch` diagnostic across Core,
|
|
27
|
+
MCP, and Hermes; repeated failures still write nothing and retain the inbox
|
|
28
|
+
for a later retry.
|
|
29
|
+
|
|
5
30
|
## 0.2.6 — 2026-09-01
|
|
6
31
|
|
|
7
32
|
- Reject one worthy memory that spans multiple independent project scopes and
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
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.8。**
|
|
27
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.8 通过 PyPI 分发。
|
|
28
28
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
29
29
|
|
|
30
30
|
## 项目定位
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
[English](README.en.md) · [PyPI](https://pypi.org/project/memleaf/) · [GitHub](https://github.com/miffyblueboo/memleaf)
|
|
6
6
|
|
|
7
|
-
> **当前版本:0.2.
|
|
8
|
-
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.
|
|
7
|
+
> **当前版本:0.2.8。**
|
|
8
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.8 通过 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.8"
|
|
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
|
|
@@ -77,6 +77,9 @@ _MODEL_VALIDATION_DETAILS = frozenset(
|
|
|
77
77
|
"duplicate_candidate_id",
|
|
78
78
|
"duplicate_update_target",
|
|
79
79
|
"mixed_project_scopes",
|
|
80
|
+
"update_target_type_mismatch",
|
|
81
|
+
"scope_not_grounded",
|
|
82
|
+
"scope_drift",
|
|
80
83
|
"invalid_evidence",
|
|
81
84
|
"invalid_flags",
|
|
82
85
|
"invalid_type",
|
|
@@ -32,13 +32,17 @@ from .models import Memory, utc_now
|
|
|
32
32
|
from .native_index import NativeIndexer
|
|
33
33
|
from .prompts import (
|
|
34
34
|
DUPLICATE_TARGET_CORRECTION,
|
|
35
|
+
GATE_TYPE_CORRECTION,
|
|
35
36
|
JSON_CORRECTION,
|
|
36
37
|
MIXED_PROJECT_SCOPES_CORRECTION,
|
|
37
38
|
RELATIVE_TIME_CORRECTION,
|
|
38
39
|
GATE_SYSTEM,
|
|
40
|
+
SCOPE_GROUNDING_CORRECTION,
|
|
41
|
+
SUMMARY_SCOPE_CORRECTION,
|
|
39
42
|
SUMMARY_TARGET_CORRECTION,
|
|
40
43
|
SUMMARY_TYPE_CORRECTION,
|
|
41
44
|
SUMMARIZE_SYSTEM,
|
|
45
|
+
UPDATE_TARGET_TYPE_CORRECTION,
|
|
42
46
|
gate_prompt,
|
|
43
47
|
summarize_prompt,
|
|
44
48
|
)
|
|
@@ -49,6 +53,7 @@ from .scope_maintenance import ScopeMaintainer, ScopeMaintenanceError, scope_reg
|
|
|
49
53
|
from .validation import (
|
|
50
54
|
MODEL_VALIDATION_DETAILS,
|
|
51
55
|
ModelOutputError,
|
|
56
|
+
NO_CHANGE_DECISION,
|
|
52
57
|
parse_gate_output,
|
|
53
58
|
parse_summarize_output,
|
|
54
59
|
)
|
|
@@ -482,6 +487,14 @@ class Processor:
|
|
|
482
487
|
return DUPLICATE_TARGET_CORRECTION
|
|
483
488
|
if hint == "mixed_project_scopes":
|
|
484
489
|
return MIXED_PROJECT_SCOPES_CORRECTION
|
|
490
|
+
if stage == "gate" and hint == "update_target_type_mismatch":
|
|
491
|
+
return UPDATE_TARGET_TYPE_CORRECTION
|
|
492
|
+
if stage == "gate" and hint == "invalid_type":
|
|
493
|
+
return GATE_TYPE_CORRECTION
|
|
494
|
+
if stage == "gate" and hint == "scope_not_grounded":
|
|
495
|
+
return SCOPE_GROUNDING_CORRECTION
|
|
496
|
+
if stage == "summarize" and hint == "scope_drift":
|
|
497
|
+
return SUMMARY_SCOPE_CORRECTION
|
|
485
498
|
if hint == "relative_time":
|
|
486
499
|
return RELATIVE_TIME_CORRECTION
|
|
487
500
|
if stage == "summarize" and hint == "invalid_update_target":
|
|
@@ -1608,6 +1621,7 @@ class Processor:
|
|
|
1608
1621
|
related_native_ids=related_native_ids,
|
|
1609
1622
|
related_memory_ids=related_memory_ids,
|
|
1610
1623
|
scope_registry=validation_scope_registry,
|
|
1624
|
+
allow_no_change=False,
|
|
1611
1625
|
),
|
|
1612
1626
|
diagnostic_context={
|
|
1613
1627
|
"source": turn.source,
|
|
@@ -1646,6 +1660,7 @@ class Processor:
|
|
|
1646
1660
|
current_event_keys=turn.event_keys,
|
|
1647
1661
|
related_memory_ids=gate_related_memory_ids,
|
|
1648
1662
|
related_memory_types=gate_related_memory_types,
|
|
1663
|
+
scope_registry=validation_scope_registry,
|
|
1649
1664
|
),
|
|
1650
1665
|
diagnostic_context={
|
|
1651
1666
|
"source": turn.source,
|
|
@@ -1657,13 +1672,6 @@ class Processor:
|
|
|
1657
1672
|
observed_scopes: list[str] = []
|
|
1658
1673
|
for candidate in gate["candidates"]:
|
|
1659
1674
|
candidate_scopes = list(candidate["scopes"])
|
|
1660
|
-
if (
|
|
1661
|
-
candidate_scopes != ["unscoped"]
|
|
1662
|
-
and candidate.get("scope_source") != "insufficient_context"
|
|
1663
|
-
):
|
|
1664
|
-
for observed_scope in candidate_scopes:
|
|
1665
|
-
if observed_scope not in observed_scopes:
|
|
1666
|
-
observed_scopes.append(observed_scope)
|
|
1667
1675
|
# An automatic candidate with no reliable project attribution is
|
|
1668
1676
|
# retained as a retryable inbox turn, never silently promoted to
|
|
1669
1677
|
# global knowledge. The processed ledger records only a compact
|
|
@@ -1752,6 +1760,27 @@ class Processor:
|
|
|
1752
1760
|
native_refs=candidate_native_refs,
|
|
1753
1761
|
)
|
|
1754
1762
|
)
|
|
1763
|
+
# Automatic duplicate observations are metadata no-ops;
|
|
1764
|
+
# only the already-active target's scopes are trustworthy
|
|
1765
|
+
# session context, never a transient model-provided scope.
|
|
1766
|
+
duplicate_scopes = next(
|
|
1767
|
+
(
|
|
1768
|
+
item.get("scopes")
|
|
1769
|
+
for item in candidate_related
|
|
1770
|
+
if isinstance(item, Mapping)
|
|
1771
|
+
and isinstance(item.get("memory_id"), str)
|
|
1772
|
+
and item["memory_id"].casefold() == duplicate_memory_id.casefold()
|
|
1773
|
+
and isinstance(item.get("scopes"), list)
|
|
1774
|
+
),
|
|
1775
|
+
[],
|
|
1776
|
+
)
|
|
1777
|
+
for observed_scope in duplicate_scopes:
|
|
1778
|
+
if (
|
|
1779
|
+
isinstance(observed_scope, str)
|
|
1780
|
+
and observed_scope != "unscoped"
|
|
1781
|
+
and observed_scope not in observed_scopes
|
|
1782
|
+
):
|
|
1783
|
+
observed_scopes.append(observed_scope)
|
|
1755
1784
|
continue
|
|
1756
1785
|
|
|
1757
1786
|
gate_update_target = candidate.get("update_memory_id")
|
|
@@ -1787,6 +1816,9 @@ class Processor:
|
|
|
1787
1816
|
related_native_ids=candidate_native_ids,
|
|
1788
1817
|
related_memory_ids=candidate_memory_ids,
|
|
1789
1818
|
scope_registry=validation_scope_registry,
|
|
1819
|
+
expected_scopes=candidate["scopes"],
|
|
1820
|
+
expected_scope_source=candidate["scope_source"],
|
|
1821
|
+
allow_no_change=True,
|
|
1790
1822
|
expected_type=(
|
|
1791
1823
|
candidate.get("type")
|
|
1792
1824
|
if (
|
|
@@ -1808,6 +1840,8 @@ class Processor:
|
|
|
1808
1840
|
"turn_index": turn.turn_index,
|
|
1809
1841
|
},
|
|
1810
1842
|
)
|
|
1843
|
+
if summary.get("decision") == NO_CHANGE_DECISION:
|
|
1844
|
+
continue
|
|
1811
1845
|
if gate_update_target is not None:
|
|
1812
1846
|
summary_update_target = summary.get("update_memory_id")
|
|
1813
1847
|
if summary_update_target is None:
|
|
@@ -1833,9 +1867,6 @@ class Processor:
|
|
|
1833
1867
|
scope_source=summary.get("scope_source"),
|
|
1834
1868
|
)
|
|
1835
1869
|
continue
|
|
1836
|
-
for observed_scope in summary["scopes"]:
|
|
1837
|
-
if observed_scope not in observed_scopes:
|
|
1838
|
-
observed_scopes.append(observed_scope)
|
|
1839
1870
|
requests.append(
|
|
1840
1871
|
self._request(
|
|
1841
1872
|
summary,
|
|
@@ -1845,6 +1876,13 @@ class Processor:
|
|
|
1845
1876
|
native_refs=candidate_native_refs,
|
|
1846
1877
|
)
|
|
1847
1878
|
)
|
|
1879
|
+
for observed_scope in summary["scopes"]:
|
|
1880
|
+
if (
|
|
1881
|
+
isinstance(observed_scope, str)
|
|
1882
|
+
and observed_scope != "unscoped"
|
|
1883
|
+
and observed_scope not in observed_scopes
|
|
1884
|
+
):
|
|
1885
|
+
observed_scopes.append(observed_scope)
|
|
1848
1886
|
return requests, observed_scopes
|
|
1849
1887
|
|
|
1850
1888
|
def _state_for_snapshot_unlocked(self, snapshot: _Snapshot, processed: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
@@ -34,6 +34,20 @@ useful. Related active memories only identify a complete duplicate or valid
|
|
|
34
34
|
state-update target; they are not current evidence and cannot make a non-worthy
|
|
35
35
|
operational/test result worthy. Their item, body, and serialized-character
|
|
36
36
|
context is bounded; ellipses and omissions do not prove a fact is absent.
|
|
37
|
+
For a worthy candidate with scope_source=model, each selected project scope must
|
|
38
|
+
be grounded by this candidate's own memory text: use the project's registered
|
|
39
|
+
scope name or one of its registered aliases; for a new unregistered project
|
|
40
|
+
scope, use that project's name itself. Do not borrow a project name from another
|
|
41
|
+
event, the session background, related memories, or an aggregate mailbox turn.
|
|
42
|
+
If the candidate text does not identify exactly one selected project, choose an
|
|
43
|
+
evidence-supported project, use unscoped with insufficient_context, defer it, or
|
|
44
|
+
set worth=false. user and session_context scope sources may rely on their
|
|
45
|
+
authoritative scope context.
|
|
46
|
+
An update target's existing type is immutable: when the same future use is being
|
|
47
|
+
updated, candidate type must exactly equal the related active target type. If a
|
|
48
|
+
fact serves a different future use, do not force it onto that target; omit
|
|
49
|
+
update_memory_id and emit a separate atomic candidate, or use worth=false when
|
|
50
|
+
it is already covered or only temporary.
|
|
37
51
|
Within one gate response, each active memory target may appear at most once
|
|
38
52
|
across all candidates, whether it is named by duplicate_memory_id or
|
|
39
53
|
update_memory_id. If multiple facts share one future-use/update target, merge
|
|
@@ -151,6 +165,16 @@ When the gate candidate supplies update_memory_id, that target is immutable:
|
|
|
151
165
|
omit update_memory_id or copy that exact active ID, never replace it with a
|
|
152
166
|
different, guessed, native, or historical ID. Keep the summary type identical
|
|
153
167
|
to the gate candidate type and to the active update target type.
|
|
168
|
+
For automatic capture/process, independently re-check whether this candidate
|
|
169
|
+
still has a concrete future-use fact or action. If not, return exactly
|
|
170
|
+
{"decision":"NO_CHANGE"}; this is the only no-write summary response. Use it
|
|
171
|
+
for temporary delegation, waiting for feedback, a one-off meeting arrangement with
|
|
172
|
+
no durable outcome/decision/project constraint, or a subject/attachment-only item
|
|
173
|
+
without a concrete impact or conclusion.
|
|
174
|
+
Explicit remember mode never permits NO_CHANGE and must return the normal summary
|
|
175
|
+
object. When producing a normal automatic summary, copy the gate candidate's
|
|
176
|
+
scopes exactly. scope_source may be omitted to inherit the gate value, but an
|
|
177
|
+
explicit value must match it; never drift to another project.
|
|
154
178
|
For the same use, preserve still-valid facts, add confirmed progress, replace
|
|
155
179
|
contradictions, and never reduce the memory to the latest operation or create a
|
|
156
180
|
sibling with a new label.
|
|
@@ -241,6 +265,27 @@ RELATIVE_TIME_CORRECTION = (
|
|
|
241
265
|
)
|
|
242
266
|
|
|
243
267
|
|
|
268
|
+
GATE_TYPE_CORRECTION = (
|
|
269
|
+
"Previous output violated: invalid_type. In gate output, whenever type is "
|
|
270
|
+
"non-null it must be exactly one of preference, fact, project, todo, event, "
|
|
271
|
+
"identity, or other. worth=true requires a non-null legal type; worth=false "
|
|
272
|
+
"may use null or any of those legal types (for example, a duplicate candidate "
|
|
273
|
+
"may retain its type). Do not invent enum values such as requirement, "
|
|
274
|
+
"decision, or task. Return only the strict gate JSON object."
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
UPDATE_TARGET_TYPE_CORRECTION = (
|
|
279
|
+
"Previous output violated: update_target_type_mismatch. An existing active "
|
|
280
|
+
"update target's type is immutable. For the same future-use topic, set the "
|
|
281
|
+
"candidate type exactly equal to the supplied related target type. For a "
|
|
282
|
+
"different future use, remove update_memory_id and emit a separate atomic "
|
|
283
|
+
"candidate, or set worth=false if it is already covered or only temporary. "
|
|
284
|
+
"Use only a supplied related active ID; never guess, use native/history IDs, "
|
|
285
|
+
"or force a cross-purpose update. Return only the strict gate JSON object."
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
244
289
|
MIXED_PROJECT_SCOPES_CORRECTION = (
|
|
245
290
|
"Previous output violated: mixed_project_scopes. Keep each worthy "
|
|
246
291
|
"candidate and summary atomic: it may contain at most one distinct "
|
|
@@ -280,6 +325,28 @@ SUMMARY_TYPE_CORRECTION = (
|
|
|
280
325
|
)
|
|
281
326
|
|
|
282
327
|
|
|
328
|
+
SUMMARY_SCOPE_CORRECTION = (
|
|
329
|
+
"Previous output violated: scope_drift. For a normal automatic summary, copy "
|
|
330
|
+
"the gate candidate scopes exactly and keep its scope_source unchanged. If "
|
|
331
|
+
"scope_source was omitted, inherit the gate value; an explicit value must "
|
|
332
|
+
"match it. Do not move the memory to another project. If the candidate has "
|
|
333
|
+
"no independent future-use fact or action, return exactly "
|
|
334
|
+
'{"decision":"NO_CHANGE"}. Return only the strict JSON object.'
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
SCOPE_GROUNDING_CORRECTION = (
|
|
339
|
+
"Previous output violated: scope_not_grounded. For each worthy candidate "
|
|
340
|
+
"with scope_source=model, choose a project scope named by that candidate's "
|
|
341
|
+
"own memory text: use the registered project name or alias, or the name "
|
|
342
|
+
"itself for a new scope. Do not borrow a name from another event, related "
|
|
343
|
+
"memory, session background, or an aggregate mailbox turn. If exactly one "
|
|
344
|
+
"project cannot be supported, choose the evidence-supported scope, use "
|
|
345
|
+
"unscoped with insufficient_context, defer it, or set worth=false. Return "
|
|
346
|
+
"only the strict gate JSON object."
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
283
350
|
COMPACT_SYSTEM = """You are memleaf's memory compactor. Return JSON only.
|
|
284
351
|
Merge only the supplied low-priority memories when they express compatible
|
|
285
352
|
information. Return an object with a memories array; [] is a safe no-op.
|
|
@@ -372,6 +439,14 @@ def summarize_prompt(
|
|
|
372
439
|
f"Current scope registry (safe projection; no paths):\n"
|
|
373
440
|
f"{_json(scope_registry if scope_registry is not None else [])}\n"
|
|
374
441
|
)
|
|
442
|
+
if not explicit:
|
|
443
|
+
prompt += (
|
|
444
|
+
"Automatic admission re-check: if the candidate has no independent "
|
|
445
|
+
"future-use fact or action after reviewing the evidence, return exactly "
|
|
446
|
+
'{"decision":"NO_CHANGE"}; do not return an empty or partial memory object. '
|
|
447
|
+
"For a normal summary, copy Candidate scopes exactly; omit scope_source "
|
|
448
|
+
"to inherit it or repeat the same value, never choose another scope.\n"
|
|
449
|
+
)
|
|
375
450
|
example_key = _first_event_key(events)
|
|
376
451
|
if example_key is not None:
|
|
377
452
|
candidate_type = candidate.get("type") if isinstance(candidate, dict) else None
|
|
@@ -405,6 +480,8 @@ def summarize_prompt(
|
|
|
405
480
|
}
|
|
406
481
|
)
|
|
407
482
|
)
|
|
483
|
+
if not explicit:
|
|
484
|
+
return prompt + '\nReturn one summary JSON object or exactly {"decision":"NO_CHANGE"}.'
|
|
408
485
|
return prompt + "\nReturn one summary JSON object."
|
|
409
486
|
|
|
410
487
|
|
|
@@ -256,9 +256,20 @@ def _term_matches(query: str, term: str) -> bool:
|
|
|
256
256
|
def resolve_query_project_scope(query: Any, config: Mapping[str, Any]) -> str | None:
|
|
257
257
|
"""Return one unambiguous project scope named or aliased by ``query``."""
|
|
258
258
|
|
|
259
|
+
matches = project_scope_matches_text(query, config)
|
|
260
|
+
return matches[0] if len(matches) == 1 else None
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def project_scope_matches_text(query: Any, config: Mapping[str, Any]) -> list[str]:
|
|
264
|
+
"""Return registered project scopes named or aliased by text.
|
|
265
|
+
|
|
266
|
+
Matching uses the same token/substring rules as query scope resolution,
|
|
267
|
+
but retains every match so callers can reject ambiguous model attribution.
|
|
268
|
+
"""
|
|
269
|
+
|
|
259
270
|
query_value = _query_text(query)
|
|
260
271
|
if not query_value:
|
|
261
|
-
return
|
|
272
|
+
return []
|
|
262
273
|
registry = validate_scope_registry(config.get("scopes", {}) if isinstance(config, Mapping) else {})
|
|
263
274
|
matches: set[str] = set()
|
|
264
275
|
for scope, node in registry.items():
|
|
@@ -271,7 +282,7 @@ def resolve_query_project_scope(query: Any, config: Mapping[str, Any]) -> str |
|
|
|
271
282
|
terms.extend(item for item in aliases if isinstance(item, str))
|
|
272
283
|
if any(_term_matches(query_value, term) for term in terms):
|
|
273
284
|
matches.add(scope)
|
|
274
|
-
return
|
|
285
|
+
return sorted(matches, key=str.casefold)
|
|
275
286
|
|
|
276
287
|
|
|
277
288
|
def _resolved_path(value: str | Path, *, base_dir: Path | None = None) -> Path:
|
|
@@ -320,6 +331,7 @@ def resolve_project_path_scope(
|
|
|
320
331
|
__all__ = [
|
|
321
332
|
"ScopeError",
|
|
322
333
|
"normalize_scopes",
|
|
334
|
+
"project_scope_matches_text",
|
|
323
335
|
"register_scope_nodes",
|
|
324
336
|
"resolve_project_path_scope",
|
|
325
337
|
"resolve_query_project_scope",
|
|
@@ -6,7 +6,12 @@ import json
|
|
|
6
6
|
import re
|
|
7
7
|
from typing import Any, Iterable, Mapping
|
|
8
8
|
|
|
9
|
-
from .scope_state import
|
|
9
|
+
from .scope_state import (
|
|
10
|
+
ScopeError,
|
|
11
|
+
project_scope_matches_text,
|
|
12
|
+
validate_scope_key,
|
|
13
|
+
validate_scope_registry,
|
|
14
|
+
)
|
|
10
15
|
|
|
11
16
|
|
|
12
17
|
class ModelOutputError(ValueError):
|
|
@@ -41,6 +46,7 @@ class ModelOutputError(ValueError):
|
|
|
41
46
|
MEMORY_TYPES = frozenset(("preference", "fact", "project", "todo", "event", "identity", "other"))
|
|
42
47
|
SCOPE_SOURCES = frozenset(("model", "user", "session_context", "insufficient_context"))
|
|
43
48
|
TODO_STATUSES = frozenset(("active", "completed", "cancelled"))
|
|
49
|
+
NO_CHANGE_DECISION = "NO_CHANGE"
|
|
44
50
|
MODEL_VALIDATION_REASONS = frozenset(
|
|
45
51
|
("empty_content", "invalid_json", "schema_violation", "response_shape")
|
|
46
52
|
)
|
|
@@ -53,6 +59,9 @@ MODEL_VALIDATION_DETAILS = frozenset(
|
|
|
53
59
|
"duplicate_candidate_id",
|
|
54
60
|
"duplicate_update_target",
|
|
55
61
|
"mixed_project_scopes",
|
|
62
|
+
"update_target_type_mismatch",
|
|
63
|
+
"scope_not_grounded",
|
|
64
|
+
"scope_drift",
|
|
56
65
|
"invalid_evidence",
|
|
57
66
|
"invalid_flags",
|
|
58
67
|
"invalid_type",
|
|
@@ -248,6 +257,78 @@ def _reject_mixed_project_scopes(scopes: Iterable[str]) -> None:
|
|
|
248
257
|
)
|
|
249
258
|
|
|
250
259
|
|
|
260
|
+
def _reject_ungrounded_project_scope(
|
|
261
|
+
memory: str,
|
|
262
|
+
scopes: Iterable[str],
|
|
263
|
+
scope_registry: Mapping[str, Any] | None,
|
|
264
|
+
) -> None:
|
|
265
|
+
"""Require model-attributed project scopes to be named by this candidate.
|
|
266
|
+
|
|
267
|
+
The gate's memory text is the only candidate-local attribution evidence.
|
|
268
|
+
Session background, other events, and related-memory bodies are
|
|
269
|
+
intentionally excluded so an aggregate mailbox turn cannot lend a
|
|
270
|
+
project name to an unrelated candidate.
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
selected = {
|
|
274
|
+
scope.casefold(): scope
|
|
275
|
+
for scope in scopes
|
|
276
|
+
if isinstance(scope, str) and scope.partition(":")[0] == "project"
|
|
277
|
+
}
|
|
278
|
+
if not selected:
|
|
279
|
+
return
|
|
280
|
+
try:
|
|
281
|
+
registry = validate_scope_registry(scope_registry or {})
|
|
282
|
+
except ScopeError as error:
|
|
283
|
+
raise ModelOutputError("invalid scope registry", validation_detail="invalid_scope") from error
|
|
284
|
+
|
|
285
|
+
# Resolve a selected project key through a registered canonical scope when
|
|
286
|
+
# it is an alias (including the full ``project:...`` spelling). A merged
|
|
287
|
+
# source may remain selectable as an alias of its target; treating both as
|
|
288
|
+
# separate projects would incorrectly reject that otherwise valid update.
|
|
289
|
+
selected_owners: dict[str, str] = {}
|
|
290
|
+
for selected_scope in selected.values():
|
|
291
|
+
selected_owner = selected_scope
|
|
292
|
+
for registered_scope, node in registry.items():
|
|
293
|
+
if not registered_scope.startswith("project:"):
|
|
294
|
+
continue
|
|
295
|
+
terms = {
|
|
296
|
+
registered_scope.casefold(),
|
|
297
|
+
registered_scope.split(":", 1)[1].casefold(),
|
|
298
|
+
}
|
|
299
|
+
aliases = node.get("aliases", [])
|
|
300
|
+
if isinstance(aliases, list):
|
|
301
|
+
for alias in aliases:
|
|
302
|
+
if isinstance(alias, str):
|
|
303
|
+
alias_key = alias.casefold()
|
|
304
|
+
terms.add(alias_key)
|
|
305
|
+
if ":" not in alias:
|
|
306
|
+
terms.add(f"project:{alias_key}")
|
|
307
|
+
if selected_scope.casefold() in terms:
|
|
308
|
+
selected_owner = registered_scope
|
|
309
|
+
break
|
|
310
|
+
selected_owners[selected_scope.casefold()] = selected_owner.casefold()
|
|
311
|
+
|
|
312
|
+
# Include unregistered selections as temporary nodes: new scopes must be
|
|
313
|
+
# grounded by their own project name, while registered scopes also accept
|
|
314
|
+
# their configured aliases. All known projects are checked to reject an
|
|
315
|
+
# otherwise ambiguous candidate that names multiple projects.
|
|
316
|
+
candidate_registry = dict(registry)
|
|
317
|
+
for scope in selected.values():
|
|
318
|
+
owner = selected_owners[scope.casefold()]
|
|
319
|
+
if owner not in {registered.casefold() for registered in registry}:
|
|
320
|
+
candidate_registry.setdefault(scope, {})
|
|
321
|
+
matches = {
|
|
322
|
+
scope.casefold()
|
|
323
|
+
for scope in project_scope_matches_text(memory, {"scopes": candidate_registry})
|
|
324
|
+
}
|
|
325
|
+
if len(matches) != 1 or not matches.intersection(selected_owners.values()):
|
|
326
|
+
raise ModelOutputError(
|
|
327
|
+
"model project scope is not grounded by this candidate",
|
|
328
|
+
validation_detail="scope_not_grounded",
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
|
|
251
332
|
def _memory_id(value: Any, field: str) -> str:
|
|
252
333
|
result = _string(value, field)
|
|
253
334
|
if "/" in result or "\\" in result or result in (".", ".."):
|
|
@@ -419,6 +500,7 @@ def validate_gate_output(
|
|
|
419
500
|
current_event_keys: Iterable[Any] | None = None,
|
|
420
501
|
related_memory_ids: Iterable[Any] | None = None,
|
|
421
502
|
related_memory_types: Mapping[str, Any] | None = None,
|
|
503
|
+
scope_registry: Mapping[str, Any] | None = None,
|
|
422
504
|
) -> dict[str, Any]:
|
|
423
505
|
"""Validate and return a normalized gate object without writing anything."""
|
|
424
506
|
|
|
@@ -540,7 +622,7 @@ def validate_gate_output(
|
|
|
540
622
|
if target_type is not None and candidate_type != target_type:
|
|
541
623
|
raise ModelOutputError(
|
|
542
624
|
"candidate type does not match update target",
|
|
543
|
-
validation_detail="
|
|
625
|
+
validation_detail="update_target_type_mismatch",
|
|
544
626
|
)
|
|
545
627
|
item["update_memory_id"] = update_id
|
|
546
628
|
for target_field in ("duplicate_memory_id", "update_memory_id"):
|
|
@@ -558,6 +640,8 @@ def validate_gate_output(
|
|
|
558
640
|
item["scopes"] = _scopes(item["scopes"], item["scope_source"])
|
|
559
641
|
if item["worth"]:
|
|
560
642
|
_reject_mixed_project_scopes(item["scopes"])
|
|
643
|
+
if item["scope_source"] == "model":
|
|
644
|
+
_reject_ungrounded_project_scope(item["memory"], item["scopes"], scope_registry)
|
|
561
645
|
if "reason" in item:
|
|
562
646
|
_string(item["reason"], "reason", nonempty=False)
|
|
563
647
|
if len(item["reason"]) > 30:
|
|
@@ -574,6 +658,7 @@ def parse_gate_output(
|
|
|
574
658
|
current_event_keys: Iterable[Any] | None = None,
|
|
575
659
|
related_memory_ids: Iterable[Any] | None = None,
|
|
576
660
|
related_memory_types: Mapping[str, Any] | None = None,
|
|
661
|
+
scope_registry: Mapping[str, Any] | None = None,
|
|
577
662
|
) -> dict[str, Any]:
|
|
578
663
|
try:
|
|
579
664
|
parsed = parse_strict_json(raw)
|
|
@@ -588,6 +673,7 @@ def parse_gate_output(
|
|
|
588
673
|
current_event_keys=current_event_keys,
|
|
589
674
|
related_memory_ids=related_memory_ids,
|
|
590
675
|
related_memory_types=related_memory_types,
|
|
676
|
+
scope_registry=scope_registry,
|
|
591
677
|
)
|
|
592
678
|
except ModelOutputError as error:
|
|
593
679
|
if error.validation_reason is None:
|
|
@@ -636,6 +722,9 @@ def validate_summarize_output(
|
|
|
636
722
|
expected_type: str | None = None,
|
|
637
723
|
expected_update_memory_id: str | None = None,
|
|
638
724
|
expected_target_type: str | None = None,
|
|
725
|
+
expected_scopes: Iterable[Any] | None = None,
|
|
726
|
+
expected_scope_source: str | None = None,
|
|
727
|
+
allow_no_change: bool = False,
|
|
639
728
|
) -> dict[str, Any]:
|
|
640
729
|
"""Validate one atomic memory summary; this function has no filesystem effects."""
|
|
641
730
|
|
|
@@ -643,6 +732,13 @@ def validate_summarize_output(
|
|
|
643
732
|
value = parse_strict_json(value)
|
|
644
733
|
if not isinstance(value, Mapping):
|
|
645
734
|
raise ModelOutputError("summarize output must be an object", validation_detail="root_shape")
|
|
735
|
+
if "decision" in value:
|
|
736
|
+
if allow_no_change and dict(value) == {"decision": NO_CHANGE_DECISION}:
|
|
737
|
+
return {"decision": NO_CHANGE_DECISION}
|
|
738
|
+
raise ModelOutputError(
|
|
739
|
+
"NO_CHANGE is only valid as the exact automatic no-write response",
|
|
740
|
+
validation_detail="unknown_fields",
|
|
741
|
+
)
|
|
646
742
|
allowed = {
|
|
647
743
|
"memory_id",
|
|
648
744
|
"update_memory_id",
|
|
@@ -680,8 +776,24 @@ def validate_summarize_output(
|
|
|
680
776
|
"summary type does not match update target",
|
|
681
777
|
validation_detail="invalid_type",
|
|
682
778
|
)
|
|
683
|
-
|
|
779
|
+
summary_scope_source = item.get("scope_source")
|
|
780
|
+
if "scope_source" not in item and expected_scope_source is not None:
|
|
781
|
+
summary_scope_source = expected_scope_source
|
|
782
|
+
item["scope_source"] = summary_scope_source
|
|
783
|
+
item["scopes"] = _scopes(item["scopes"], summary_scope_source)
|
|
684
784
|
_reject_mixed_project_scopes(item["scopes"])
|
|
785
|
+
if expected_scopes is not None:
|
|
786
|
+
expected_scope_values = list(expected_scopes)
|
|
787
|
+
if item["scopes"] != expected_scope_values:
|
|
788
|
+
raise ModelOutputError(
|
|
789
|
+
"summary scopes differ from gate candidate",
|
|
790
|
+
validation_detail="scope_drift",
|
|
791
|
+
)
|
|
792
|
+
if expected_scope_source is not None and summary_scope_source != expected_scope_source:
|
|
793
|
+
raise ModelOutputError(
|
|
794
|
+
"summary scope source differs from gate candidate",
|
|
795
|
+
validation_detail="scope_drift",
|
|
796
|
+
)
|
|
685
797
|
item["scope_operations"] = _scope_operations(
|
|
686
798
|
item.get("scope_operations"),
|
|
687
799
|
summary_scopes=item["scopes"],
|
|
@@ -769,6 +881,9 @@ def parse_summarize_output(
|
|
|
769
881
|
expected_type: str | None = None,
|
|
770
882
|
expected_update_memory_id: str | None = None,
|
|
771
883
|
expected_target_type: str | None = None,
|
|
884
|
+
expected_scopes: Iterable[Any] | None = None,
|
|
885
|
+
expected_scope_source: str | None = None,
|
|
886
|
+
allow_no_change: bool = False,
|
|
772
887
|
) -> dict[str, Any]:
|
|
773
888
|
try:
|
|
774
889
|
parsed = parse_strict_json(raw)
|
|
@@ -786,6 +901,9 @@ def parse_summarize_output(
|
|
|
786
901
|
expected_type=expected_type,
|
|
787
902
|
expected_update_memory_id=expected_update_memory_id,
|
|
788
903
|
expected_target_type=expected_target_type,
|
|
904
|
+
expected_scopes=expected_scopes,
|
|
905
|
+
expected_scope_source=expected_scope_source,
|
|
906
|
+
allow_no_change=allow_no_change,
|
|
789
907
|
)
|
|
790
908
|
except ModelOutputError as error:
|
|
791
909
|
if error.validation_reason is None:
|
|
@@ -893,6 +1011,7 @@ __all__ = [
|
|
|
893
1011
|
"MODEL_VALIDATION_DETAILS",
|
|
894
1012
|
"MODEL_VALIDATION_REASONS",
|
|
895
1013
|
"ModelOutputError",
|
|
1014
|
+
"NO_CHANGE_DECISION",
|
|
896
1015
|
"SCOPE_SOURCES",
|
|
897
1016
|
"TODO_STATUSES",
|
|
898
1017
|
"parse_gate",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
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.8。**
|
|
27
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.8 通过 PyPI 分发。
|
|
28
28
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
29
29
|
|
|
30
30
|
## 项目定位
|