memleaf 0.2.61__tar.gz → 0.2.63__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.61 → memleaf-0.2.63}/CHANGELOG.md +18 -0
- {memleaf-0.2.61/src/memleaf.egg-info → memleaf-0.2.63}/PKG-INFO +8 -8
- {memleaf-0.2.61 → memleaf-0.2.63}/README.en.md +7 -7
- {memleaf-0.2.61 → memleaf-0.2.63}/README.md +7 -7
- {memleaf-0.2.61 → memleaf-0.2.63}/docs/extraction-latency.md +1 -1
- memleaf-0.2.63/docs/semantic-extraction-protocol.md +37 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/pyproject.toml +1 -1
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/__init__.py +1 -1
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/hermes_provider/plugin.yaml +1 -1
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/memory_writer.py +7 -2
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/planning_context.py +34 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/process_common.py +12 -0
- memleaf-0.2.63/src/memleaf/semantic_maintenance.py +140 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/semantic_protocol.py +41 -20
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/single_pass_memory_planner.py +208 -104
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/single_pass_plan.py +147 -90
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/turn_audit.py +4 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/validation.py +11 -9
- {memleaf-0.2.61 → memleaf-0.2.63/src/memleaf.egg-info}/PKG-INFO +8 -8
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf.egg-info/SOURCES.txt +1 -0
- memleaf-0.2.61/docs/semantic-extraction-protocol.md +0 -33
- {memleaf-0.2.61 → memleaf-0.2.63}/LICENSE +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/MANIFEST.in +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/docs/capture-budget-design.md +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/docs/config-migrations.md +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/docs/core-refactor.md +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/docs/evidence-retention.md +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/docs/gate-evidence-boundary.md +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/docs/general-processing.md +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/docs/hermes-mcp-runtime.md +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/docs/processing-quality-acceptance.md +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/docs/v0.2.26-processing-status.md +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/examples/README.md +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/examples/basic_usage.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/examples/mcp_stdio.ndjson +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/install.ps1 +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/install.sh +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/setup.cfg +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/__main__.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/adapters/__init__.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/adapters/antigravity.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/adapters/base.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/adapters/codex.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/adapters/hermes.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/admission.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/batch_review.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/budget.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/capture.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/cli.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/compaction.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/config.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/create_coordinator.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/credentials.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/evidence_budget.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/evidence_policy.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/evidence_structure.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/evidence_syntax.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/extraction_budget.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/extraction_capability.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/extraction_work_state.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/frontmatter.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/hermes_provider/README.md +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/hermes_provider/__init__.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/hermes_provider/_mcp_client.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/hermes_provider/_provider.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/hermes_provider/_shared.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/hermes_provider/evidence_budget.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/hermes_runtime.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/host_events.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/host_runtime.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/inbox.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/index.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/inspection.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/installer.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/llm/__init__.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/llm/base.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/llm/claude_compatible.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/llm/gemini.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/llm/openai_compatible.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/llm/router.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/llm/thinking.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/locking.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/mcp_server.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/memory_commit.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/memory_planner.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/model_capabilities.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/model_discovery.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/model_execution.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/models.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/native_index.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/native_registration.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/parallel_model.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/process_jobs.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/process_journal.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/process_owner.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/processing.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/prompts.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/provenance.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/recording_policy.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/redaction.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/retention.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/retrieval.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/retrieval_gate.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/scope_maintenance.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/scope_state.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/service.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/source_policy.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/state_layout.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/subprocess_flags.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/summary_batch.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/target_reconciliation.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/turn_plan.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/update_coordinator.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/update_review.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/vault.py +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf.egg-info/dependency_links.txt +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf.egg-info/entry_points.txt +0 -0
- {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf.egg-info/top_level.txt +0 -0
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to memleaf are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.2.63 — 2026-09-16
|
|
6
|
+
|
|
7
|
+
- Unify the retention decision across first-pass extraction, topic selection, candidate repair and `semantic_maintenance`. The model now judges future reuse, information gain, direct usability and forgetting cost, keeping only the smallest core with clear material value; the contract contains no mail, report, customer or project-specific exclusion rule.
|
|
8
|
+
- Keep maintenance complementary to extraction: it applies the same value judgment to incoming candidates, continues maintaining candidates that qualify, discards those without clear value and defers only unresolved meaning. The `0.2.62` date/ownership degradation and update-target grouping remain intact.
|
|
9
|
+
|
|
10
|
+
Verification covers prompt consistency and compactness assertions, Python 3.11 compilation/imports, package metadata consistency, a package wheel containing the Hermes provider resource, and `git diff --check`. Real-provider replay remains runtime acceptance after installation.
|
|
11
|
+
|
|
12
|
+
## 0.2.62 — 2026-09-16
|
|
13
|
+
|
|
14
|
+
- Record conclusions, not process. The contract now opens with one precedence rule -- 只记结论: what the user chose, agreed to, or what happened; options, plans and drafts offered by the assistant are process even when they look like reusable knowledge. It replaces a genuine conflict between two earlier sentences: `reusable`/通用经验 invited storing the assistant's own outline, while the assistant-proposal clause forbade it, so the model decided by coin flip. The contradictory sentence about assistant suggestions never being a task basis is gone with it. Measured on the reproduced turn, the assistant's four-section outline was written in one of two runs before the change and in none of four runs after it.
|
|
15
|
+
- Write one topic once. The contract states it directly instead of enumerating types, and the fact+todo twin that a single sentence ("明天要给鑫元基金发任务排期清单") used to produce is gone. Types are not enumerated because the topic may be neither a fact nor a todo.
|
|
16
|
+
- Stop discarding whole candidates. Four fail-closed gates were removed or degraded, because each one traded a whole memory for one doubtful field: (1) a missing owner no longer defers -- the model's project name is kept, and `unscoped`/empty normalizes to the single project named by the candidate's own evidence, otherwise `global`; (2) a deadline is validated rather than demanded -- `_resolve_candidate_due_date` accepts it only when the wording occurs in the candidate's evidence and anchors to ISO against that evidence's timestamp (`明天`, `周五`, `下周一`, `月底` resolve; anything else), and an unresolvable date costs the date field only; (3) assistant-stated intent, multi-project bodies and NO_CHANGE target relevance are the model's judgement again; (4) an unauthorized cross-scope UPDATE keeps the target's own scope instead of failing. Dates inside the body that cannot be anchored are now counted, not rejected.
|
|
17
|
+
- Keep the update target through review. The reproduced failure was that a state change became a new memory: the model set `target` correctly but labelled the type `fact` while the target was a `todo`, the review stage grouped candidates by (scope, type), the target fell outside the group, and the reviewer -- which was never shown the primary's target -- re-emitted it as a new item. Grouping now uses the target's own type and scope, the already-chosen target is kept in the group's `existing`, and target records stay in the review catalog.
|
|
18
|
+
- Delete the dead English B3 prompt. `SINGLE_PASS_SYSTEM` and `B3_COMPACT_CONTRACT` were no longer sent to any model; they survived only as an internal scaffold for fragment slicing and a compatibility read path, so they and the guard/imports they kept alive are removed. B3 remains readable for host adapters and stored plans, and the structure-repair request now carries a one-line schema note instead of the full contract.
|
|
19
|
+
- Tests: the suite went from 18 red to 115 green. Sixteen failures asserted sentences in that deleted English prompt; they are removed, and the one whose intent still holds (the model used to receive the date anchor) now pins `_resolve_candidate_due_date` with eight cases. Two behavioural tests were aligned to the current design and this is a deliberate call, not a discovered bug: a deferral row legitimately carries diagnostics now, and the documented turn budget is three requests (`MAX_MODEL_REQUESTS = 3`) rather than two.
|
|
20
|
+
|
|
21
|
+
Verification for this release replayed three real Hermes sessions (seven turns, real assistant replies, isolated Vault, live model route). Before: the email turn wrote nothing -- two candidates deferred for `scope_not_grounded` and the todo for `due_date_not_grounded` -- so no todo ever existed and every later "已经完成了" created another memory, which is how the live Vault ended up with two overlapping "排期已发出" facts. After: the first turn writes three memories including the todo with a resolved `2026-09-16` deadline, the completion turn updates that same todo to `completed`, the cross-session repeat is NO_CHANGE, a new request creates a new todo with `2026-09-18`, the query turn writes nothing, and every turn reports zero deferrals with `coverage_status=complete`, across four runs. Known residual: an assistant statement about its own state ("我这边没有底稿") is still stored occasionally.
|
|
22
|
+
|
|
5
23
|
## 0.2.61 — 2026-09-15
|
|
6
24
|
|
|
7
25
|
- Split ordinary automatic extraction into a model-facing semantic protocol and a Core-owned B3 write protocol. Core supplies short immutable evidence-fragment IDs and binds their original spans locally; the model first selects reusable topics and then synthesizes memory content. Core compiles the result into B3 candidate IDs, decisions, evidence, targets and write fields before the existing validators and commit path run.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.63
|
|
4
4
|
Summary: A local-first Markdown memory core for AI agents
|
|
5
5
|
Author: memleaf contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -23,11 +23,11 @@ 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
|
-
>
|
|
28
|
-
> B3
|
|
29
|
-
>
|
|
30
|
-
>
|
|
26
|
+
> **版本:0.2.63。**
|
|
27
|
+
> 自动提炼和复核共用同一条“未来记忆价值”标准:模型综合未来复用、信息增量、再次读取时的直接可用性和忘记成本,只保留对未来理解、判断或行动有实质影响的最小核心;没有明确价值的信息不提炼,不按具体业务场景硬编码排除。
|
|
28
|
+
> 提炼契约现在只有一份中文语义契约(旧版英文 B3 提示词已删除),首轮、主题选择、候选修复和 `semantic_maintenance` 使用同一套保留价值判断;同一件事只写一次。
|
|
29
|
+
> Core 不再因为归属、日期、任务依据或跨项目而丢弃整条候选:归属缺失会归一到证据里唯一被点名的项目、否则落 `global`;日期只接受能在本候选证据里找到出处、且能锚定成 ISO 的写法(`明天`、`周五`、`月底` 都会被解析),无法解析时只丢掉日期字段,记忆照常写入;未获授权的改归属沿用目标原归属。
|
|
30
|
+
> 待办 `due_date` 只表示待办动作本身明确声明的截止日;属于待办主题或预期结果的日期不会被误作截止日。项目归属由模型按语义判断,不要求项目名在证据里逐字出现。复核阶段会保留首轮已选定的更新目标,避免“状态变更”被降级成新增记忆。Markdown 仍是唯一事实源。
|
|
31
31
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
32
32
|
|
|
33
33
|
## 项目定位
|
|
@@ -100,8 +100,8 @@ memleaf 不把每句话都保存为记忆。处理一轮完整的 user + assista
|
|
|
100
100
|
|
|
101
101
|
- 通常一轮产生 0~1 条记忆,而不是按句子拆分;
|
|
102
102
|
- 新记忆必须有稳定标题、完整正文和合理 Scope;
|
|
103
|
-
-
|
|
104
|
-
- 相同未来用途优先 UPDATE/NO_CHANGE,不重复 CREATE
|
|
103
|
+
- 归属不明确时归一到证据里唯一被点名的项目,确实没有主体时落 `global`,不因为归属不明而丢掉这条记忆;
|
|
104
|
+
- 相同未来用途优先 UPDATE/NO_CHANGE,不重复 CREATE;同一件事只写一次;
|
|
105
105
|
- 用户显式要求保存时可调用 `remember`,但仍会整理、校验和去重;
|
|
106
106
|
- 模型、解析、写入或索引失败时保留 inbox 和处理水位,后续可重试;
|
|
107
107
|
- 自动清理有 24 小时安全期,不会因为一次处理失败就删除原始捕获。
|
|
@@ -4,11 +4,11 @@
|
|
|
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
|
-
>
|
|
9
|
-
>
|
|
10
|
-
>
|
|
11
|
-
>
|
|
7
|
+
> **Version: 0.2.63.**
|
|
8
|
+
> Automatic extraction and review share one future-value standard: the model weighs likely reuse, information gain, direct usability when read again, and the cost of forgetting. It keeps only the smallest core that can materially help future understanding, decisions, or actions; information without clear value is not extracted, and no business-specific exclusion rule is hard-coded.
|
|
9
|
+
> A single Chinese semantic contract is sent for extraction, topic selection, candidate repair, and `semantic_maintenance`; the older English B3 prompt has been deleted and B3 remains only as a compatibility read format. One topic is written once.
|
|
10
|
+
> Core no longer discards a whole candidate over ownership, dates, task basis or cross-project wording. A missing owner normalizes to the single project named by that candidate's own evidence, otherwise `global`. A deadline is accepted only when it appears in the candidate's own evidence and can be anchored to ISO (`明天`, `周五`, `月底` all resolve); when it cannot, only the date field is dropped and the memory is still written. An unauthorized ownership change keeps the target's existing scope.
|
|
11
|
+
> A todo's `due_date` means only the explicit deadline of the todo action; dates describing its subject or desired outcome are not deadlines. Project ownership remains the model's semantic judgement and needs no literal name match. The review stage now keeps the update target chosen by the first pass, so a state change is no longer downgraded into a new memory. Markdown remains the sole source of truth.
|
|
12
12
|
> **The current release supports Hermes and Codex.** Antigravity is not detected, installed, or configured.
|
|
13
13
|
|
|
14
14
|
## Project scope
|
|
@@ -79,8 +79,8 @@ Additional rules:
|
|
|
79
79
|
|
|
80
80
|
- A normal turn typically produces 0–1 memory rather than one memory per sentence.
|
|
81
81
|
- New memories need a stable title, self-contained body, and appropriate Scope.
|
|
82
|
-
-
|
|
83
|
-
- Prefer UPDATE or NO_CHANGE over creating a sibling for the same future use.
|
|
82
|
+
- An unclear owner normalizes to the single project named by that candidate's evidence, or `global` when there is genuinely no subject; an unclear owner never costs the memory.
|
|
83
|
+
- Prefer UPDATE or NO_CHANGE over creating a sibling for the same future use; one topic is written once.
|
|
84
84
|
- An explicit user request can call `remember`, but the content is still normalized, checked, and deduplicated.
|
|
85
85
|
- Model, parsing, write, or index failures keep the inbox and processing watermark retryable.
|
|
86
86
|
- Automatic cleanup has a 24-hour safety period; a failed processing attempt does not delete the original capture.
|
|
@@ -4,11 +4,11 @@
|
|
|
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
|
-
>
|
|
9
|
-
> B3
|
|
10
|
-
>
|
|
11
|
-
>
|
|
7
|
+
> **版本:0.2.63。**
|
|
8
|
+
> 自动提炼和复核共用同一条“未来记忆价值”标准:模型综合未来复用、信息增量、再次读取时的直接可用性和忘记成本,只保留对未来理解、判断或行动有实质影响的最小核心;没有明确价值的信息不提炼,不按具体业务场景硬编码排除。
|
|
9
|
+
> 提炼契约现在只有一份中文语义契约(旧版英文 B3 提示词已删除),首轮、主题选择、候选修复和 `semantic_maintenance` 使用同一套保留价值判断;同一件事只写一次。
|
|
10
|
+
> Core 不再因为归属、日期、任务依据或跨项目而丢弃整条候选:归属缺失会归一到证据里唯一被点名的项目、否则落 `global`;日期只接受能在本候选证据里找到出处、且能锚定成 ISO 的写法(`明天`、`周五`、`月底` 都会被解析),无法解析时只丢掉日期字段,记忆照常写入;未获授权的改归属沿用目标原归属。
|
|
11
|
+
> 待办 `due_date` 只表示待办动作本身明确声明的截止日;属于待办主题或预期结果的日期不会被误作截止日。项目归属由模型按语义判断,不要求项目名在证据里逐字出现。复核阶段会保留首轮已选定的更新目标,避免“状态变更”被降级成新增记忆。Markdown 仍是唯一事实源。
|
|
12
12
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
13
13
|
|
|
14
14
|
## 项目定位
|
|
@@ -81,8 +81,8 @@ memleaf 不把每句话都保存为记忆。处理一轮完整的 user + assista
|
|
|
81
81
|
|
|
82
82
|
- 通常一轮产生 0~1 条记忆,而不是按句子拆分;
|
|
83
83
|
- 新记忆必须有稳定标题、完整正文和合理 Scope;
|
|
84
|
-
-
|
|
85
|
-
- 相同未来用途优先 UPDATE/NO_CHANGE,不重复 CREATE
|
|
84
|
+
- 归属不明确时归一到证据里唯一被点名的项目,确实没有主体时落 `global`,不因为归属不明而丢掉这条记忆;
|
|
85
|
+
- 相同未来用途优先 UPDATE/NO_CHANGE,不重复 CREATE;同一件事只写一次;
|
|
86
86
|
- 用户显式要求保存时可调用 `remember`,但仍会整理、校验和去重;
|
|
87
87
|
- 模型、解析、写入或索引失败时保留 inbox 和处理水位,后续可重试;
|
|
88
88
|
- 自动清理有 24 小时安全期,不会因为一次处理失败就删除原始捕获。
|
|
@@ -19,7 +19,7 @@ llm:
|
|
|
19
19
|
|
|
20
20
|
## 保留的调用与提交约束
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
普通自动提炼正常使用两次模型调用(一次提炼、一次按需复核),全部遵循 `single_pass` 思考配置(默认 `disabled`),包括候选修复。候选存在分歧或字段违规时允许一次局部修复;复核、修复与同目标协调共享总计三次的请求上限。固定安全路由保留实际 outbound 请求计数和持久化预占,不增加隐藏 host-to-API fallback。语义质量和耗时分别验收。
|
|
23
23
|
|
|
24
24
|
每轮仍先读取持久状态、规划、校验、提交,再处理下一轮;一轮超过目标不会占用下一轮的“时间许可”。compaction 仍不在普通提炼的关键路径上。
|
|
25
25
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# 自动记忆提炼:语义契约与 Core 写入
|
|
2
|
+
|
|
3
|
+
普通自动提炼只给模型一份中文语义契约(`semantic_protocol.FRAGMENT_SYSTEM`)。模型只引用 Core 提供的短编号片段,原文、偏移、日期和写入字段都由 Core 绑定。
|
|
4
|
+
|
|
5
|
+
## 正常路径
|
|
6
|
+
|
|
7
|
+
1. **Core 建立证据片段**:按通用标点和换行生成短编号,保留原始 unit、字符位置、角色和时间锚点。模型只引用编号;原文和偏移由 Core 绑定。
|
|
8
|
+
2. **一次提炼**:模型从完整证据生成记忆(`memories` / `no_memory` / `deferred`),自行决定主题、类型、归属与日期写法。
|
|
9
|
+
3. **按需复核**:候选存在分歧、缺证据或字段违规时,Core 用维护契约(`MAINTENANCE_SYSTEM`)再走一次:输入按项目和类型分组,`incoming` 是本轮增量,`existing` 是可更新的 catalog ID。**首轮已选定的更新目标会保留在该组的 `existing` 里,并以目标自身的类型和归属参与分组**,因此状态变更不会被复核降级成新增。
|
|
10
|
+
4. **Core 编译与校验**:生成候选 ID、写入决策和来源,校验引用、证据与结构后交给原有提交与恢复机制。
|
|
11
|
+
|
|
12
|
+
正常两次请求;候选修复与同目标协调共享第三次额度。
|
|
13
|
+
|
|
14
|
+
## 提炼判据
|
|
15
|
+
|
|
16
|
+
契约引导模型先判断信息是否有长期记忆价值,而不按具体业务场景列禁止项:综合未来复用、信息增量、再次读取时的直接可用性和忘记成本。只有对未来理解、判断或行动可能产生实质影响的信息才保留;没有明确价值就列入 `no_memory`,并只写产生该价值的最小核心。同一件事只写一次。
|
|
17
|
+
|
|
18
|
+
首轮提炼、主题选择、候选修复和 `semantic_maintenance` 共用这条判断标准。`retention` 与类型独立:`reusable` 表示符合上述标准,`session` 表示不符合,Core 会把 `session` 转成 `no_memory`。维护阶段将符合标准的 incoming 继续维护,不符合的丢弃,语义无法确定的延后;它负责维护已有候选,不重新发明主题。
|
|
19
|
+
|
|
20
|
+
## 降级而不是丢弃
|
|
21
|
+
|
|
22
|
+
Core 不再因为下列原因丢弃整条候选(每次丢弃都等于损失一条本可保留的记忆):
|
|
23
|
+
|
|
24
|
+
- **归属缺失**:模型给了项目名就保留;给了 `unscoped` 或空值时,归一到本候选证据里唯一被点名的项目,否则落 `global`。
|
|
25
|
+
- **日期无法锚定**:只校验 `due_date`。日期必须出现在本候选证据里、且能按该证据的时间戳锚定成 ISO(`明天`、`周五`、`下周一`、`月底` 都会被解析);不满足时**只丢掉日期字段**,记忆照常写入。
|
|
26
|
+
- **任务依据不足、正文涉及多个项目、NO_CHANGE 目标相关性无法证明**:由模型判断,Core 不再因此拒收。
|
|
27
|
+
- **未获授权的改归属**:沿用目标自身的归属继续写入,不再报错。
|
|
28
|
+
|
|
29
|
+
正文里的日期若无法用本轮证据锚定,只记入诊断计数,不再丢弃整条记忆。
|
|
30
|
+
|
|
31
|
+
## 保留价值与内容类型分离
|
|
32
|
+
|
|
33
|
+
主题和记忆候选支持 `retention: reusable | session`。模型判断内容是在描述本轮执行过程,还是已经成立的可复用状态、经验或后续事项;不根据工具名、错误码或来源角色做过滤。Core 将 `session` 引用转换为 `no_memory`,不产生写入候选。旧适配器省略该字段时保持原有行为。
|
|
34
|
+
|
|
35
|
+
## 验证边界
|
|
36
|
+
|
|
37
|
+
本地测试覆盖引用与覆盖、真正/虚假目标、日期解析与降级、跨项目正文、更新目标在复核阶段的保留,以及既有写入契约。真实模型验收另在隔离 Vault 中执行。十秒目标仍只是性能观测,不能用局部测试或一次成功重放宣称所有语义场景已通过。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Local-first Markdown memory core for AI agents."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.2.
|
|
3
|
+
__version__ = "0.2.63"
|
|
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
|
|
@@ -244,9 +244,14 @@ class MemoryWriter:
|
|
|
244
244
|
raise self._preflight_error("batch contains duplicate deterministic memory id")
|
|
245
245
|
if deterministic_id in duplicate_ids:
|
|
246
246
|
raise self._preflight_error("batch memory id collides with metadata merge target")
|
|
247
|
-
|
|
247
|
+
# An UPDATE keeps its target's identity. Its deterministic request
|
|
248
|
+
# ID can equal that target when a previously created candidate is
|
|
249
|
+
# planned again; it is not an attempt to create another memory.
|
|
250
|
+
# Still reject a request ID that aliases a different update target.
|
|
251
|
+
if deterministic_id in target_ids and deterministic_id != target_id:
|
|
248
252
|
raise self._preflight_error("batch memory id collides with update target")
|
|
249
|
-
if target_id is not None and
|
|
253
|
+
if (target_id is not None and deterministic_id != target_id
|
|
254
|
+
and target_id.casefold() == deterministic_id.casefold()):
|
|
250
255
|
raise self._preflight_error("batch memory id collides with update target")
|
|
251
256
|
memory_ids.add(deterministic_id)
|
|
252
257
|
if "/" in deterministic_id or "\\" in deterministic_id or deterministic_id in (".", ".."):
|
|
@@ -628,6 +628,40 @@ class PlanningContext:
|
|
|
628
628
|
native_query=visible,
|
|
629
629
|
return_bound_status=True,
|
|
630
630
|
)
|
|
631
|
+
# A session scope is conversational background, not a search fence.
|
|
632
|
+
# Include known projects named in this turn and this session's own
|
|
633
|
+
# active memories so a project switch cannot hide maintenance targets.
|
|
634
|
+
with self.service.vault.lock():
|
|
635
|
+
config = self.service.vault.config()
|
|
636
|
+
registry = config.get("scopes", {})
|
|
637
|
+
mentioned = [key for key in registry
|
|
638
|
+
if isinstance(key, str) and key.startswith("project:")
|
|
639
|
+
and self._scope_terms_present(visible, key, config)]
|
|
640
|
+
current_records = self.service._read_memories_unlocked("knowledge")
|
|
641
|
+
contextual = [record.memory.to_dict() for record in current_records
|
|
642
|
+
if (any(self._scope_terms_present(visible, key, config)
|
|
643
|
+
for key in record.memory.scopes if key.startswith("project:"))
|
|
644
|
+
or (record.memory.extra.get("source") == turn.source and any(
|
|
645
|
+
src.get("session_id") == turn.session_id
|
|
646
|
+
for src in record.memory.sources if isinstance(src, Mapping))))]
|
|
647
|
+
if explicit_scope is not None:
|
|
648
|
+
# Explicit caller scope remains a deliberate boundary.
|
|
649
|
+
contextual = [row for row in contextual
|
|
650
|
+
if filter_by_scope([Memory.from_mapping(row)], explicit_scope, config)]
|
|
651
|
+
elif mentioned:
|
|
652
|
+
scope_background = mentioned
|
|
653
|
+
if contextual:
|
|
654
|
+
contextual = self._overlay_related(contextual, overlay)
|
|
655
|
+
by_id = {row["memory_id"].casefold(): row for row in contextual}
|
|
656
|
+
combined = [row for row in related
|
|
657
|
+
if row.get("native") is True
|
|
658
|
+
or str(row.get("memory_id", "")).casefold() not in by_id]
|
|
659
|
+
related, complete = self._bound_related_with_status(
|
|
660
|
+
[*contextual, *combined], priority_memory_ids=by_id)
|
|
661
|
+
bound_complete = bool(bound_complete and complete)
|
|
662
|
+
# Several supplied targets are alternatives for the semantic
|
|
663
|
+
# matcher, not an incomplete lookup merely because they coexist.
|
|
664
|
+
scope_fallback = None
|
|
631
665
|
correction_rows, correction_complete = self._single_pass_scope_correction_context(turn)
|
|
632
666
|
if correction_rows:
|
|
633
667
|
existing_ids = {
|
|
@@ -832,6 +832,10 @@ def _summary_date_grounding_violations(
|
|
|
832
832
|
allowed_dates.add(canonical)
|
|
833
833
|
|
|
834
834
|
yearless_monthdays = source_monthdays | preserved_monthdays
|
|
835
|
+
# An absolute source date also supports a faithful yearless rendering.
|
|
836
|
+
# This does not authorize adding a year to an unanchored source date.
|
|
837
|
+
yearless_monthdays.update(value[5:] for value in allowed_dates
|
|
838
|
+
if isinstance(value, str) and re.fullmatch(r"\d{4}-\d{2}-\d{2}", value))
|
|
835
839
|
violations: list[str] = []
|
|
836
840
|
seen: set[str] = set()
|
|
837
841
|
for field in ("title", "body"):
|
|
@@ -974,12 +978,20 @@ def _native_result(value: Any) -> list[dict[str, Any]]:
|
|
|
974
978
|
def _merge_related(values: Iterable[Mapping[str, Any]]) -> list[dict[str, Any]]:
|
|
975
979
|
result: list[dict[str, Any]] = []
|
|
976
980
|
seen_bodies: set[str] = set()
|
|
981
|
+
seen_ids: set[str] = set()
|
|
977
982
|
for item in values:
|
|
978
983
|
if not isinstance(item, Mapping):
|
|
979
984
|
continue
|
|
980
985
|
body = item.get("body")
|
|
981
986
|
if not isinstance(body, str):
|
|
982
987
|
continue
|
|
988
|
+
memory_id = item.get("memory_id")
|
|
989
|
+
if item.get("native") is not True and isinstance(memory_id, str) and memory_id:
|
|
990
|
+
key = memory_id.casefold()
|
|
991
|
+
if key not in seen_ids:
|
|
992
|
+
seen_ids.add(key)
|
|
993
|
+
result.append(dict(item))
|
|
994
|
+
continue
|
|
983
995
|
normalized = normalize_term(body)
|
|
984
996
|
if normalized and normalized in seen_bodies:
|
|
985
997
|
continue
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Maintain scoped memory state from small, already evidence-bound deltas."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Mapping
|
|
5
|
+
from .semantic_protocol import RETENTION_GUIDANCE, expand_fragments, _invalid
|
|
6
|
+
from .validation import parse_strict_json
|
|
7
|
+
|
|
8
|
+
MAINTENANCE_SYSTEM = RETENTION_GUIDANCE + "\n" + '''维护长期记忆,而不是再次摘录对话。按上述标准复核 incoming:符合标准的继续维护,没有明确价值的放 discard,无法判断的放 deferred。groups 按项目和类型隔离,incoming 是本轮增量,existing 是可更新的 catalog ID。
|
|
9
|
+
同一事项的需求、进展、回复、附件位置和约定日期合并维护;后续状态替换旧状态,重复信息不新建。不同的独立事项保持分开。正文概括核心,不逐条转录文档或保存助手的临时建议。类型由输入确定,本阶段只维护同类型的状态。
|
|
10
|
+
返回 JSON {"memories":[{"from":[incoming ID],"target":"已有memory_id或null","title":"主题","body":"合并后的当前内容","type":"fact或todo等"}],"discard":[incoming ID],"deferred":[incoming ID]}。
|
|
11
|
+
每条仅合并同组 incoming;同一事项已有记忆时 target 必须选该组 existing 中的ID,保留其有效内容并更新变化;独立新事项 target=null。已有target保留原type;新任务没有同事项todo目标时新建todo,不借用fact ID。同一target只输出一次。无需修改的已有记忆可原样返回。todo 提供 status(active/completed/cancelled)和 due_date(原文日期,无则null);不能把任务变成一般事实而丢失动作。每个 incoming 由 from、discard 或 deferred 覆盖。无需处理原始片段ID、复制证据或生成记忆ID。'''
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def maintenance_input(raw: str, fragments: list[dict[str, Any]], catalog: list[dict[str, Any]], model_data: Mapping[str, Any]):
|
|
15
|
+
# Validate exact references before they become trusted input to maintenance.
|
|
16
|
+
expand_fragments(raw, fragments)
|
|
17
|
+
original = parse_strict_json(raw)
|
|
18
|
+
by_id = {
|
|
19
|
+
memory['memory_id'].casefold(): memory
|
|
20
|
+
for memory in catalog
|
|
21
|
+
if isinstance(memory.get('memory_id'), str)
|
|
22
|
+
}
|
|
23
|
+
groups = {}
|
|
24
|
+
incoming = {}
|
|
25
|
+
targeted: set[str] = set()
|
|
26
|
+
for index, row in enumerate(original['memories'], 1):
|
|
27
|
+
if row.get('retention') == 'session':
|
|
28
|
+
continue
|
|
29
|
+
row = dict(row)
|
|
30
|
+
target_id = row.get('target')
|
|
31
|
+
record = by_id.get(target_id.casefold()) if isinstance(target_id, str) else None
|
|
32
|
+
if record is not None:
|
|
33
|
+
# An UPDATE inherits its target's type and scope. The provisional
|
|
34
|
+
# type the model wrote must not move the candidate into another
|
|
35
|
+
# group, or the reviewer never sees the memory it is updating.
|
|
36
|
+
row['type'] = record.get('type', row.get('type', 'fact'))
|
|
37
|
+
scopes = record.get('scopes') or []
|
|
38
|
+
if len(scopes) == 1:
|
|
39
|
+
row['scope'] = scopes[0]
|
|
40
|
+
targeted.add(record['memory_id'])
|
|
41
|
+
scope = row['scope']
|
|
42
|
+
kind = row.get('type', 'fact')
|
|
43
|
+
group = groups.setdefault((scope, kind), {'scope': scope, 'type': kind, 'existing': [], 'incoming': []})
|
|
44
|
+
incoming[index] = row
|
|
45
|
+
group['incoming'].append({k:v for k,v in {'id':f'd{index}', **row}.items()
|
|
46
|
+
if k not in {'evidence','task_basis','retention'}})
|
|
47
|
+
if record is not None and record['memory_id'] not in group['existing']:
|
|
48
|
+
# The already-chosen target must stay selectable for the reviewer.
|
|
49
|
+
group['existing'].append(record['memory_id'])
|
|
50
|
+
related = [m for m in catalog if len(m.get('scopes', [])) == 1 and (m['scopes'][0], m['type']) in groups]
|
|
51
|
+
known = {m['memory_id'] for m in related}
|
|
52
|
+
for memory in catalog:
|
|
53
|
+
if memory['memory_id'] in targeted and memory['memory_id'] not in known:
|
|
54
|
+
# A target whose recorded type differs from the provisional one is
|
|
55
|
+
# still part of the comparison context.
|
|
56
|
+
related.append(memory)
|
|
57
|
+
known.add(memory['memory_id'])
|
|
58
|
+
# Small original snippets let the reviewer disambiguate a task or date,
|
|
59
|
+
# while the long source document is no longer a second extraction job.
|
|
60
|
+
snippets = [{**f, 'text': f['text'][:240]} for f in model_data['fragments']]
|
|
61
|
+
payload = {'groups': list(groups.values()), 'catalog': related, 'fragments': snippets}
|
|
62
|
+
return payload, (original, incoming, {m['memory_id'].casefold():m for m in related})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def expand_maintenance(raw: str, context) -> str:
|
|
66
|
+
value = parse_strict_json(raw)
|
|
67
|
+
original, incoming, catalog = context
|
|
68
|
+
# Older host adapters can still provide fully bound compact output.
|
|
69
|
+
if isinstance(value, dict) and set(value) == {'memories','no_memory','deferred'}:
|
|
70
|
+
return raw
|
|
71
|
+
if not isinstance(value, dict) or set(value) != {'memories','discard','deferred'}:
|
|
72
|
+
raise _invalid()
|
|
73
|
+
if any(not isinstance(v,list) for v in value.values()):
|
|
74
|
+
raise _invalid()
|
|
75
|
+
seen = set()
|
|
76
|
+
def resolve(refs):
|
|
77
|
+
if not isinstance(refs,list) or not refs:
|
|
78
|
+
raise _invalid('invalid_evidence')
|
|
79
|
+
normalized = []
|
|
80
|
+
for ref in refs:
|
|
81
|
+
r = ref[1:] if isinstance(ref, str) and ref.startswith('d') else ref
|
|
82
|
+
normalized.append(int(r) if isinstance(r, str) and len(r) <= 20 and r.isascii() and r.isdecimal() else r)
|
|
83
|
+
if any((type(r) is not int and not isinstance(r, str)) or r not in incoming for r in normalized):
|
|
84
|
+
raise _invalid('invalid_evidence')
|
|
85
|
+
seen.update(normalized)
|
|
86
|
+
return [incoming[r] for r in normalized]
|
|
87
|
+
result = {'memories':[], 'no_memory':list(original['no_memory']), 'deferred':list(original['deferred'])}
|
|
88
|
+
for row in original['memories']:
|
|
89
|
+
if row.get('retention')=='session':
|
|
90
|
+
result['no_memory'].extend(row['evidence'])
|
|
91
|
+
for row in value['memories']:
|
|
92
|
+
if not isinstance(row,dict) or 'target' not in row or not {'from','title','body'} <= set(row):
|
|
93
|
+
raise _invalid()
|
|
94
|
+
if row['from'] == []:
|
|
95
|
+
# A catalog echo without fresh evidence cannot authorize a write.
|
|
96
|
+
# Ignore it only when every supplied field is literally unchanged.
|
|
97
|
+
target = row['target']
|
|
98
|
+
record = catalog.get(target.casefold()) if isinstance(target, str) else None
|
|
99
|
+
fields = {k: v for k, v in row.items() if k not in {'from', 'target'} and v is not None}
|
|
100
|
+
if record is None or any(record.get(k) != v for k, v in fields.items()):
|
|
101
|
+
raise _invalid('invalid_evidence')
|
|
102
|
+
continue
|
|
103
|
+
sources = resolve(row['from'])
|
|
104
|
+
scopes = {s['scope'] for s in sources}
|
|
105
|
+
if len(scopes)!=1:
|
|
106
|
+
raise _invalid('scope_drift')
|
|
107
|
+
scope = next(iter(scopes))
|
|
108
|
+
kinds = {s.get('type', 'fact') for s in sources}
|
|
109
|
+
if len(kinds) != 1:
|
|
110
|
+
raise _invalid('invalid_type')
|
|
111
|
+
kind = next(iter(kinds))
|
|
112
|
+
target = row['target']
|
|
113
|
+
if target is not None:
|
|
114
|
+
record = catalog.get(target.casefold()) if isinstance(target,str) else None
|
|
115
|
+
if record is None or record['scopes'] != [scope]:
|
|
116
|
+
raise _invalid('scope_drift')
|
|
117
|
+
if kind != record['type']:
|
|
118
|
+
raise _invalid('invalid_type')
|
|
119
|
+
memory = {k:v for k,v in row.items() if k!='from'}
|
|
120
|
+
if 'scope' in memory and memory['scope']!=scope:
|
|
121
|
+
raise _invalid('scope_drift')
|
|
122
|
+
memory['scope']=scope
|
|
123
|
+
memory['type']=kind
|
|
124
|
+
if kind != 'todo':
|
|
125
|
+
for field in ('status', 'due_date', 'completed_at'):
|
|
126
|
+
memory.pop(field, None)
|
|
127
|
+
memory['evidence']=list(dict.fromkeys(int(r) for s in sources for r in s['evidence']))
|
|
128
|
+
bases = list(dict.fromkeys(int(r) for s in sources for r in (s.get('task_basis') or [])))
|
|
129
|
+
if bases:
|
|
130
|
+
memory['task_basis']=bases
|
|
131
|
+
result['memories'].append(memory)
|
|
132
|
+
for key,dest in (('discard','no_memory'),('deferred','deferred')):
|
|
133
|
+
if value[key]:
|
|
134
|
+
for row in resolve(value[key]):
|
|
135
|
+
result[dest].extend(row['evidence'])
|
|
136
|
+
if seen != set(incoming):
|
|
137
|
+
raise _invalid('invalid_evidence')
|
|
138
|
+
for key in ('no_memory','deferred'):
|
|
139
|
+
result[key]=list(dict.fromkeys(int(r) for r in result[key]))
|
|
140
|
+
return json.dumps(result,ensure_ascii=False)
|