memleaf 0.2.48__tar.gz → 0.2.49__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.48 → memleaf-0.2.49}/CHANGELOG.md +9 -0
- {memleaf-0.2.48/src/memleaf.egg-info → memleaf-0.2.49}/PKG-INFO +2 -2
- {memleaf-0.2.48 → memleaf-0.2.49}/README.en.md +1 -1
- {memleaf-0.2.48 → memleaf-0.2.49}/README.md +1 -1
- {memleaf-0.2.48 → memleaf-0.2.49}/pyproject.toml +1 -1
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/__init__.py +1 -1
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/hermes_provider/plugin.yaml +1 -1
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/single_pass_plan.py +91 -26
- {memleaf-0.2.48 → memleaf-0.2.49/src/memleaf.egg-info}/PKG-INFO +2 -2
- {memleaf-0.2.48 → memleaf-0.2.49}/LICENSE +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/MANIFEST.in +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/docs/capture-budget-design.md +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/docs/config-migrations.md +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/docs/core-refactor.md +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/docs/evidence-retention.md +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/docs/extraction-latency.md +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/docs/gate-evidence-boundary.md +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/docs/general-processing.md +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/docs/hermes-mcp-runtime.md +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/docs/processing-quality-acceptance.md +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/docs/v0.2.26-processing-status.md +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/examples/README.md +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/examples/basic_usage.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/examples/mcp_stdio.ndjson +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/install.ps1 +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/install.sh +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/setup.cfg +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/__main__.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/adapters/__init__.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/adapters/antigravity.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/adapters/base.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/adapters/codex.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/adapters/hermes.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/admission.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/batch_review.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/budget.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/capture.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/cli.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/compaction.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/config.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/create_coordinator.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/credentials.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/evidence_budget.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/evidence_policy.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/evidence_structure.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/evidence_syntax.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/extraction_budget.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/extraction_capability.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/extraction_work_state.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/frontmatter.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/hermes_provider/README.md +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/hermes_provider/__init__.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/hermes_provider/_mcp_client.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/hermes_provider/_provider.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/hermes_provider/_shared.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/hermes_provider/evidence_budget.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/hermes_runtime.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/host_events.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/host_runtime.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/inbox.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/index.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/inspection.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/installer.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/llm/__init__.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/llm/base.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/llm/claude_compatible.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/llm/gemini.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/llm/openai_compatible.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/llm/router.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/llm/thinking.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/locking.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/mcp_server.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/memory_commit.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/memory_planner.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/memory_writer.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/model_capabilities.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/model_discovery.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/model_execution.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/models.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/native_index.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/native_registration.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/parallel_model.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/planning_context.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/process_common.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/process_jobs.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/process_journal.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/process_owner.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/processing.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/prompts.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/provenance.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/recording_policy.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/redaction.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/retention.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/retrieval.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/retrieval_gate.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/scope_maintenance.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/scope_state.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/service.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/single_pass_memory_planner.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/source_policy.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/state_layout.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/summary_batch.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/target_reconciliation.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/turn_audit.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/turn_plan.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/update_coordinator.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/update_review.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/validation.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf/vault.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf.egg-info/SOURCES.txt +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf.egg-info/dependency_links.txt +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf.egg-info/entry_points.txt +0 -0
- {memleaf-0.2.48 → memleaf-0.2.49}/src/memleaf.egg-info/top_level.txt +0 -0
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to memleaf are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.2.49 — 2026-09-13
|
|
6
|
+
|
|
7
|
+
- Restore candidate-local deferral to the B3 single-pass path. v0.2.26 established that an invalid proposal receives bounded model correction and then explicit candidate-local deferral while "valid siblings proceed", and v0.2.34 required that "candidate-local deferral and idempotent partial retries" be preserved so unresolved ownership, target, evidence or timing never fabricates a write. The B3 planner had lost that property: the first proposal Core rejected raised out of the parser and discarded every other verdict in the turn, so one model slip cost the whole turn even when six other candidates were sound. A proposal that Core can attribute to a single candidate is now deferred with a B3 defer reason and the rest of the turn still commits. Nothing is written for a deferred candidate, no extra model call is made, and the turn stays retryable with its already-settled units intact.
|
|
8
|
+
- Keep the deferral boundary narrow and explicit. Only details that describe the candidate itself are converted: scope authorisation and update-target problems map to `scope_ambiguous` and `target_ambiguous`, while a malformed proposal (todo fields, ungrounded or malformed dates, type, source shape) maps to `maintenance_uncertain` and missing admitted evidence maps to `evidence_insufficient`. Every other failure — transport, truncation, envelope shape, evidence coverage, binding scope, and any detail Core does not recognise — still fails the whole turn closed, so a systemic fault cannot hide behind a deferral.
|
|
9
|
+
- Make the deferral visible rather than silent. Each one is counted as `b3_candidate_deferred_count` on the turn, the cause is recorded against the candidate, and the deferred work continues to surface through `deferred_candidates`, `retryable_deferred_turns` and `coverage_status: partial` in the read-only processing status. The persisted cause is restricted to a bounded identifier so no model text can reach state files.
|
|
10
|
+
- Keep the deferral from being mistaken for a collision. Same-target reconciliation groups items that reach a terminal disposition for one memory, and a deferred candidate reaches none: it writes nothing, carries no update target, and cannot conflict with an admitted update to the same memory. Only the candidates that actually reach UPDATE or NO_CHANGE are grouped now, so a turn that updates one memory and defers another does not spend its second request reconciling a candidate that was never admitted.
|
|
11
|
+
|
|
12
|
+
Verification for this release repeated the same real-model replay used for 0.2.48 and added focused synthetic tests: 90 tests pass with no model call, covering the sibling-commits case, every deferrable detail and the reason it maps to, deferral being driven by an explicit allowlist, the optional argument for direct parser callers, a deferred member not being grouped as a same-target collision, and the turn-level failures that must still abort. Three further preview runs of the previously failing production session completed with `execution_status: ok` and `coverage_status: complete`. A deferral is a visible degradation rather than a success: the candidate is retried, not written, and a prompt or configuration fault that made every proposal invalid would now show up as a fully deferred turn rather than as a hard failure.
|
|
13
|
+
|
|
5
14
|
## 0.2.48 — 2026-09-13
|
|
6
15
|
|
|
7
16
|
- Restore same-target reconciliation in the B3 single-pass path. A real turn was discarded with `B3 target referenced more than once` whenever the model admitted two changes for one existing memory. One memory can only receive one terminal disposition per turn, because the writer archives the previous version and overwrites the same file, so the colliding items are now reconciled with one bounded call into a single current state and an unresolvable group is deferred as `target_ambiguous` instead of guessed. This is the capability the legacy staged path already had; it is not a new retry class, and the compact contract now tells the model to merge such changes into one UPDATE in the first place. The call is charged against the same two-request turn budget as the primary extraction and its structural repair, so a collision can never lengthen a turn: when a repair has already spent the budget, the group defers and every other item still commits. The strict "a target may be used once" rejection remains in the parser for direct callers.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.49
|
|
4
4
|
Summary: A local-first Markdown memory core for AI agents
|
|
5
5
|
Author: memleaf contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -23,7 +23,7 @@ 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.
|
|
26
|
+
> **版本:0.2.49。**
|
|
27
27
|
> 记忆只从用户与 Agent 的可见对话提炼。Agent 已在回复中整理的事实、项目进展和明确待办可作为来源;邮件、附件、网页和终端等工具原文不进入记忆提炼。模型负责保留原话中的不确定性,现有记忆仅用于比较、去重和更新。Markdown 仍是唯一事实源。
|
|
28
28
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
29
29
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[中文](README.md) · [PyPI](https://pypi.org/project/memleaf/) · [GitHub](https://github.com/miffyblueboo/memleaf)
|
|
6
6
|
|
|
7
|
-
> **Version: 0.2.
|
|
7
|
+
> **Version: 0.2.49.**
|
|
8
8
|
> Automatic extraction now uses only the current turn's visible user input and final assistant reply. Raw tool output, attachments, web/file/terminal payloads and legacy tool-evidence bodies are not new source evidence; existing or retrieved memory remains comparison context rather than source authority. Background processing is persisted as a local job and can be checked through the read-only `process_status` MCP tool; failed work remains retryable and fail closed. The release also tightens source/date grounding, target reconciliation, duplicate/no-op handling and semantic review before writes. Markdown remains the sole source of truth with no SQLite runtime dependency. Acceptance covers deterministic regression suites and synthetic inputs; it does not claim real-mail or customer-business acceptance.
|
|
9
9
|
> **The current release supports Hermes and Codex.** Antigravity is not detected, installed, or configured.
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@
|
|
|
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.
|
|
7
|
+
> **版本:0.2.49。**
|
|
8
8
|
> 记忆只从用户与 Agent 的可见对话提炼。Agent 已在回复中整理的事实、项目进展和明确待办可作为来源;邮件、附件、网页和终端等工具原文不进入记忆提炼。模型负责保留原话中的不确定性,现有记忆仅用于比较、去重和更新。Markdown 仍是唯一事实源。
|
|
9
9
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
10
10
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Local-first Markdown memory core for AI agents."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.2.
|
|
3
|
+
__version__ = "0.2.49"
|
|
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
|
|
@@ -45,6 +45,29 @@ _DEFER_REASONS = frozenset({
|
|
|
45
45
|
"lookup_incomplete",
|
|
46
46
|
"maintenance_uncertain",
|
|
47
47
|
})
|
|
48
|
+
# A rejected proposal is a candidate-local problem, not a turn-local one.
|
|
49
|
+
# v0.2.26 established that "invalid proposals receive bounded model correction,
|
|
50
|
+
# then explicit candidate-local deferral; valid siblings proceed", and v0.2.34
|
|
51
|
+
# required that candidate-local deferral and idempotent partial retries be
|
|
52
|
+
# preserved. Only details that describe the candidate itself are listed here:
|
|
53
|
+
# anything unrecognised, and every transport or invariant failure, still fails
|
|
54
|
+
# the whole turn closed.
|
|
55
|
+
_B3_DEFERRABLE_DETAILS = {
|
|
56
|
+
"scope_not_grounded": "scope_ambiguous",
|
|
57
|
+
"scope_drift": "scope_ambiguous",
|
|
58
|
+
"invalid_scope": "scope_ambiguous",
|
|
59
|
+
"invalid_scope_source": "scope_ambiguous",
|
|
60
|
+
"invalid_update_target": "target_ambiguous",
|
|
61
|
+
"duplicate_update_target": "target_ambiguous",
|
|
62
|
+
"invalid_due_date": "maintenance_uncertain",
|
|
63
|
+
"due_date_not_grounded": "maintenance_uncertain",
|
|
64
|
+
"relative_time": "maintenance_uncertain",
|
|
65
|
+
"todo_fields": "maintenance_uncertain",
|
|
66
|
+
"invalid_type": "maintenance_uncertain",
|
|
67
|
+
"source_shape": "maintenance_uncertain",
|
|
68
|
+
"invalid_evidence": "evidence_insufficient",
|
|
69
|
+
}
|
|
70
|
+
_DETAIL_TEXT_RE = re.compile(r"^[a-z_]{1,48}$")
|
|
48
71
|
_MEMORY_FIELDS = frozenset({
|
|
49
72
|
"title", "body", "tags", "aliases", "keywords", "status", "completed_at", "due_date",
|
|
50
73
|
"shadow_native_ids",
|
|
@@ -409,6 +432,7 @@ def parse_single_pass_output(
|
|
|
409
432
|
validate_memory: MemoryValidator,
|
|
410
433
|
target_rows: list[dict[str, Any]] | None = None,
|
|
411
434
|
normalizations: list[str] | None = None,
|
|
435
|
+
deferrals: list[dict[str, Any]] | None = None,
|
|
412
436
|
) -> dict[str, Any]:
|
|
413
437
|
if not callable(validate_memory):
|
|
414
438
|
raise TypeError("validate_memory must be callable")
|
|
@@ -717,30 +741,54 @@ def parse_single_pass_output(
|
|
|
717
741
|
for item, target_record in prepared:
|
|
718
742
|
candidate_id = item["candidate_id"]
|
|
719
743
|
decision = item["decision"]
|
|
744
|
+
evidence = [dict(claim) for claim in bindings[candidate_id]]
|
|
720
745
|
normalized: dict[str, Any] = {
|
|
721
746
|
"candidate_id": candidate_id,
|
|
722
747
|
"decision": decision,
|
|
723
|
-
"evidence":
|
|
748
|
+
"evidence": evidence,
|
|
724
749
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
750
|
+
try:
|
|
751
|
+
if decision == "CREATE":
|
|
752
|
+
validated = validate_memory(
|
|
753
|
+
candidate_id, decision, None, None, item["memory"], evidence, item,
|
|
754
|
+
)
|
|
755
|
+
normalized.update({
|
|
756
|
+
"type": item["type"], "scopes": list(item["scopes"]), "memory": dict(validated),
|
|
757
|
+
})
|
|
758
|
+
elif decision == "UPDATE":
|
|
759
|
+
target_id = item["target_memory_id"]
|
|
760
|
+
validated = validate_memory(
|
|
761
|
+
candidate_id, decision, target_id, target_record,
|
|
762
|
+
item["memory"], evidence, item,
|
|
763
|
+
)
|
|
764
|
+
normalized["target_memory_id"] = target_id
|
|
765
|
+
normalized["memory"] = dict(validated)
|
|
766
|
+
elif decision == "NO_CHANGE":
|
|
767
|
+
normalized["target_memory_id"] = item["target_memory_id"]
|
|
768
|
+
else:
|
|
769
|
+
normalized["reason"] = item["reason"]
|
|
770
|
+
except ModelOutputError as error:
|
|
771
|
+
detail = getattr(error, "validation_detail", None)
|
|
772
|
+
reason = _B3_DEFERRABLE_DETAILS.get(detail) if isinstance(detail, str) else None
|
|
773
|
+
if reason is None:
|
|
774
|
+
raise
|
|
775
|
+
# The proposal is unsafe to write, but its siblings are unaffected.
|
|
776
|
+
# Defer this one candidate so the turn keeps every verdict it can
|
|
777
|
+
# justify, and record the cause instead of failing silently.
|
|
778
|
+
normalized = {
|
|
779
|
+
"candidate_id": candidate_id,
|
|
780
|
+
"decision": "DEFERRED",
|
|
781
|
+
"reason": reason,
|
|
782
|
+
"evidence": evidence,
|
|
783
|
+
}
|
|
784
|
+
if deferrals is not None:
|
|
785
|
+
deferrals.append({
|
|
786
|
+
"candidate_id": candidate_id,
|
|
787
|
+
"reason": reason,
|
|
788
|
+
"detail": (
|
|
789
|
+
detail if _DETAIL_TEXT_RE.fullmatch(detail) else "unrecognised_detail"
|
|
790
|
+
),
|
|
791
|
+
})
|
|
744
792
|
normalized_items.append(normalized)
|
|
745
793
|
return {"protocol_version": PROTOCOL_VERSION, "items": normalized_items, "no_memory": normalized_no_memory}
|
|
746
794
|
|
|
@@ -1060,10 +1108,12 @@ def run_single_pass_stage(
|
|
|
1060
1108
|
primary_system = "" if inline_system else SINGLE_PASS_SYSTEM
|
|
1061
1109
|
target_rows: list[dict[str, Any]] = []
|
|
1062
1110
|
normalizations: list[str] = []
|
|
1111
|
+
deferrals: list[dict[str, Any]] = []
|
|
1063
1112
|
|
|
1064
1113
|
def parse(raw: str) -> dict[str, Any]:
|
|
1065
1114
|
target_rows.clear()
|
|
1066
1115
|
normalizations.clear()
|
|
1116
|
+
deferrals.clear()
|
|
1067
1117
|
return parse_single_pass_output(
|
|
1068
1118
|
raw,
|
|
1069
1119
|
evidence_units=source_units,
|
|
@@ -1072,6 +1122,7 @@ def run_single_pass_stage(
|
|
|
1072
1122
|
validate_memory=validate_memory,
|
|
1073
1123
|
target_rows=target_rows,
|
|
1074
1124
|
normalizations=normalizations,
|
|
1125
|
+
deferrals=deferrals,
|
|
1075
1126
|
)
|
|
1076
1127
|
|
|
1077
1128
|
def finalize(parsed: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -1084,12 +1135,6 @@ def run_single_pass_stage(
|
|
|
1084
1135
|
and an unresolvable group is deferred instead of guessed.
|
|
1085
1136
|
"""
|
|
1086
1137
|
|
|
1087
|
-
grouped: dict[str, list[str]] = {}
|
|
1088
|
-
for row in target_rows:
|
|
1089
|
-
grouped.setdefault(str(row["target"]), []).append(str(row["candidate_id"]))
|
|
1090
|
-
collisions = {key: ids for key, ids in grouped.items() if len(ids) > 1}
|
|
1091
|
-
if not collisions:
|
|
1092
|
-
return parsed
|
|
1093
1138
|
items = parsed.get("items")
|
|
1094
1139
|
if not isinstance(items, list) or not items:
|
|
1095
1140
|
return parsed
|
|
@@ -1098,6 +1143,22 @@ def run_single_pass_stage(
|
|
|
1098
1143
|
for index, row in enumerate(items)
|
|
1099
1144
|
if isinstance(row, Mapping)
|
|
1100
1145
|
}
|
|
1146
|
+
terminal_ids = {
|
|
1147
|
+
str(row.get("candidate_id"))
|
|
1148
|
+
for row in items
|
|
1149
|
+
if isinstance(row, Mapping) and row.get("decision") in {"UPDATE", "NO_CHANGE"}
|
|
1150
|
+
}
|
|
1151
|
+
grouped: dict[str, list[str]] = {}
|
|
1152
|
+
for row in target_rows:
|
|
1153
|
+
candidate_id = str(row["candidate_id"])
|
|
1154
|
+
if candidate_id not in terminal_ids:
|
|
1155
|
+
# A candidate Core deferred writes nothing, so it cannot
|
|
1156
|
+
# collide with a terminal disposition for the same memory.
|
|
1157
|
+
continue
|
|
1158
|
+
grouped.setdefault(str(row["target"]), []).append(candidate_id)
|
|
1159
|
+
collisions = {key: ids for key, ids in grouped.items() if len(ids) > 1}
|
|
1160
|
+
if not collisions:
|
|
1161
|
+
return parsed
|
|
1101
1162
|
|
|
1102
1163
|
def union_evidence(members: Sequence[Mapping[str, Any]]) -> list[Any]:
|
|
1103
1164
|
merged: list[Any] = []
|
|
@@ -1273,6 +1334,8 @@ def run_single_pass_stage(
|
|
|
1273
1334
|
event(repair_context, "parse_accepted_count")
|
|
1274
1335
|
if normalizations:
|
|
1275
1336
|
event(repair_context, "b3_normalization_count", len(normalizations))
|
|
1337
|
+
if deferrals:
|
|
1338
|
+
event(repair_context, "b3_candidate_deferred_count", len(deferrals))
|
|
1276
1339
|
writer = getattr(model_executor, "_write_model_diagnostic", None)
|
|
1277
1340
|
if callable(writer):
|
|
1278
1341
|
try:
|
|
@@ -1289,6 +1352,8 @@ def run_single_pass_stage(
|
|
|
1289
1352
|
event(primary_context, "parse_accepted_count")
|
|
1290
1353
|
if normalizations:
|
|
1291
1354
|
event(primary_context, "b3_normalization_count", len(normalizations))
|
|
1355
|
+
if deferrals:
|
|
1356
|
+
event(primary_context, "b3_candidate_deferred_count", len(deferrals))
|
|
1292
1357
|
writer = getattr(model_executor, "_write_model_diagnostic", None)
|
|
1293
1358
|
if callable(writer):
|
|
1294
1359
|
try:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.49
|
|
4
4
|
Summary: A local-first Markdown memory core for AI agents
|
|
5
5
|
Author: memleaf contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -23,7 +23,7 @@ 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.
|
|
26
|
+
> **版本:0.2.49。**
|
|
27
27
|
> 记忆只从用户与 Agent 的可见对话提炼。Agent 已在回复中整理的事实、项目进展和明确待办可作为来源;邮件、附件、网页和终端等工具原文不进入记忆提炼。模型负责保留原话中的不确定性,现有记忆仅用于比较、去重和更新。Markdown 仍是唯一事实源。
|
|
28
28
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
29
29
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|