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.
Files changed (118) hide show
  1. {memleaf-0.2.61 → memleaf-0.2.63}/CHANGELOG.md +18 -0
  2. {memleaf-0.2.61/src/memleaf.egg-info → memleaf-0.2.63}/PKG-INFO +8 -8
  3. {memleaf-0.2.61 → memleaf-0.2.63}/README.en.md +7 -7
  4. {memleaf-0.2.61 → memleaf-0.2.63}/README.md +7 -7
  5. {memleaf-0.2.61 → memleaf-0.2.63}/docs/extraction-latency.md +1 -1
  6. memleaf-0.2.63/docs/semantic-extraction-protocol.md +37 -0
  7. {memleaf-0.2.61 → memleaf-0.2.63}/pyproject.toml +1 -1
  8. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/__init__.py +1 -1
  9. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/hermes_provider/plugin.yaml +1 -1
  10. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/memory_writer.py +7 -2
  11. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/planning_context.py +34 -0
  12. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/process_common.py +12 -0
  13. memleaf-0.2.63/src/memleaf/semantic_maintenance.py +140 -0
  14. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/semantic_protocol.py +41 -20
  15. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/single_pass_memory_planner.py +208 -104
  16. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/single_pass_plan.py +147 -90
  17. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/turn_audit.py +4 -0
  18. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/validation.py +11 -9
  19. {memleaf-0.2.61 → memleaf-0.2.63/src/memleaf.egg-info}/PKG-INFO +8 -8
  20. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf.egg-info/SOURCES.txt +1 -0
  21. memleaf-0.2.61/docs/semantic-extraction-protocol.md +0 -33
  22. {memleaf-0.2.61 → memleaf-0.2.63}/LICENSE +0 -0
  23. {memleaf-0.2.61 → memleaf-0.2.63}/MANIFEST.in +0 -0
  24. {memleaf-0.2.61 → memleaf-0.2.63}/docs/capture-budget-design.md +0 -0
  25. {memleaf-0.2.61 → memleaf-0.2.63}/docs/config-migrations.md +0 -0
  26. {memleaf-0.2.61 → memleaf-0.2.63}/docs/core-refactor.md +0 -0
  27. {memleaf-0.2.61 → memleaf-0.2.63}/docs/evidence-retention.md +0 -0
  28. {memleaf-0.2.61 → memleaf-0.2.63}/docs/gate-evidence-boundary.md +0 -0
  29. {memleaf-0.2.61 → memleaf-0.2.63}/docs/general-processing.md +0 -0
  30. {memleaf-0.2.61 → memleaf-0.2.63}/docs/hermes-mcp-runtime.md +0 -0
  31. {memleaf-0.2.61 → memleaf-0.2.63}/docs/processing-quality-acceptance.md +0 -0
  32. {memleaf-0.2.61 → memleaf-0.2.63}/docs/v0.2.26-processing-status.md +0 -0
  33. {memleaf-0.2.61 → memleaf-0.2.63}/examples/README.md +0 -0
  34. {memleaf-0.2.61 → memleaf-0.2.63}/examples/basic_usage.py +0 -0
  35. {memleaf-0.2.61 → memleaf-0.2.63}/examples/mcp_stdio.ndjson +0 -0
  36. {memleaf-0.2.61 → memleaf-0.2.63}/install.ps1 +0 -0
  37. {memleaf-0.2.61 → memleaf-0.2.63}/install.sh +0 -0
  38. {memleaf-0.2.61 → memleaf-0.2.63}/setup.cfg +0 -0
  39. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/__main__.py +0 -0
  40. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/adapters/__init__.py +0 -0
  41. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/adapters/antigravity.py +0 -0
  42. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/adapters/base.py +0 -0
  43. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/adapters/codex.py +0 -0
  44. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/adapters/hermes.py +0 -0
  45. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/admission.py +0 -0
  46. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/batch_review.py +0 -0
  47. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/budget.py +0 -0
  48. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/capture.py +0 -0
  49. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/cli.py +0 -0
  50. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/compaction.py +0 -0
  51. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/config.py +0 -0
  52. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/create_coordinator.py +0 -0
  53. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/credentials.py +0 -0
  54. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/evidence_budget.py +0 -0
  55. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/evidence_policy.py +0 -0
  56. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/evidence_structure.py +0 -0
  57. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/evidence_syntax.py +0 -0
  58. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/extraction_budget.py +0 -0
  59. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/extraction_capability.py +0 -0
  60. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/extraction_work_state.py +0 -0
  61. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/frontmatter.py +0 -0
  62. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/hermes_provider/README.md +0 -0
  63. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/hermes_provider/__init__.py +0 -0
  64. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/hermes_provider/_mcp_client.py +0 -0
  65. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/hermes_provider/_provider.py +0 -0
  66. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/hermes_provider/_shared.py +0 -0
  67. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/hermes_provider/evidence_budget.py +0 -0
  68. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/hermes_runtime.py +0 -0
  69. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/host_events.py +0 -0
  70. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/host_runtime.py +0 -0
  71. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/inbox.py +0 -0
  72. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/index.py +0 -0
  73. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/inspection.py +0 -0
  74. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/installer.py +0 -0
  75. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/llm/__init__.py +0 -0
  76. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/llm/base.py +0 -0
  77. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/llm/claude_compatible.py +0 -0
  78. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/llm/gemini.py +0 -0
  79. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/llm/openai_compatible.py +0 -0
  80. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/llm/router.py +0 -0
  81. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/llm/thinking.py +0 -0
  82. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/locking.py +0 -0
  83. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/mcp_server.py +0 -0
  84. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/memory_commit.py +0 -0
  85. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/memory_planner.py +0 -0
  86. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/model_capabilities.py +0 -0
  87. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/model_discovery.py +0 -0
  88. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/model_execution.py +0 -0
  89. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/models.py +0 -0
  90. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/native_index.py +0 -0
  91. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/native_registration.py +0 -0
  92. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/parallel_model.py +0 -0
  93. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/process_jobs.py +0 -0
  94. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/process_journal.py +0 -0
  95. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/process_owner.py +0 -0
  96. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/processing.py +0 -0
  97. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/prompts.py +0 -0
  98. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/provenance.py +0 -0
  99. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/recording_policy.py +0 -0
  100. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/redaction.py +0 -0
  101. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/retention.py +0 -0
  102. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/retrieval.py +0 -0
  103. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/retrieval_gate.py +0 -0
  104. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/scope_maintenance.py +0 -0
  105. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/scope_state.py +0 -0
  106. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/service.py +0 -0
  107. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/source_policy.py +0 -0
  108. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/state_layout.py +0 -0
  109. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/subprocess_flags.py +0 -0
  110. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/summary_batch.py +0 -0
  111. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/target_reconciliation.py +0 -0
  112. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/turn_plan.py +0 -0
  113. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/update_coordinator.py +0 -0
  114. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/update_review.py +0 -0
  115. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf/vault.py +0 -0
  116. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf.egg-info/dependency_links.txt +0 -0
  117. {memleaf-0.2.61 → memleaf-0.2.63}/src/memleaf.egg-info/entry_points.txt +0 -0
  118. {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.61
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.61。**
27
- > 普通自动提炼改为“主题选择 → 内容提炼”的语义协议,模型只引用 Core 提供的短证据片段;Core 继续编译并校验内部 B3 写入协议。临时执行过程可标记为 `session` 而不写入长期记忆,正常路径两次模型调用,候选修复与同目标协调共享第三次预算。
28
- > B3 提取提示要求模型只聚焦当前任务、直接返回并保持记忆简短,减少无关分析和冗长输出;证据、Scope、日期和写入规则不变。
29
- > 待办 `due_date` 只表示待办动作本身明确声明的截止日;Core 不再从候选证据中的其他日期自动补填。属于待办主题或预期结果的日期不会被误作截止日,无法确认动作截止日时就省略。
30
- > 自动提取按助手回复中的 Markdown 结构建立候选级来源,过宽的整段引用和未被用户接受的助手意图不会形成记忆。项目归属由模型按语义判断,不再要求项目名在证据里逐字出现:同一个项目换个说法也能落到已有 Scope 上,名字也不再决定这条记忆的生死。日期和截止日期使用统一的边界安全解析,todo 只在候选自己的证据给出唯一明确截止日时填充。Markdown 仍是唯一事实源。
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
- - 项目、负责人等归属不明确时延后记录,不猜测为 `global`;
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.61.**
8
- > Ordinary automatic extraction now uses a “topic selection → content synthesis” semantic protocol. The model cites only short evidence fragments supplied by Core, while Core still compiles and validates the internal B3 write contract. Transient execution can be marked `session` without entering long-term memory; the normal path uses two model requests, with candidate repair and same-target coordination sharing a third budget.
9
- > The B3 extraction prompt now tells the model to focus only on the task, decide directly, return promptly, and keep memories brief; evidence, Scope, date and write semantics are unchanged.
10
- > A todo's `due_date` now means only the explicit deadline of the todo action. Core no longer fills it from other dates in candidate evidence; dates describing the todo's subject or desired outcome are not deadlines, and an unconfirmed action deadline is omitted.
11
- > Automatic extraction now creates candidate-local source units from Markdown structure in assistant replies; broad whole-report citations and assistant-only intent are not allowed to create memories. Project ownership is the model's semantic judgement and no longer requires the project name to appear literally in the evidence: one project referred to in different words lands on the existing Scope, and the name no longer decides whether the memory survives. Dates and deadlines use one boundary-safe parser, and a todo receives a due date only when its own evidence supplies one unambiguous deadline. 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.
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
- - If project or ownership attribution is unclear, defer the memory instead of guessing `global`.
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.61。**
8
- > 普通自动提炼改为“主题选择 → 内容提炼”的语义协议,模型只引用 Core 提供的短证据片段;Core 继续编译并校验内部 B3 写入协议。临时执行过程可标记为 `session` 而不写入长期记忆,正常路径两次模型调用,候选修复与同目标协调共享第三次预算。
9
- > B3 提取提示要求模型只聚焦当前任务、直接返回并保持记忆简短,减少无关分析和冗长输出;证据、Scope、日期和写入规则不变。
10
- > 待办 `due_date` 只表示待办动作本身明确声明的截止日;Core 不再从候选证据中的其他日期自动补填。属于待办主题或预期结果的日期不会被误作截止日,无法确认动作截止日时就省略。
11
- > 自动提取按助手回复中的 Markdown 结构建立候选级来源,过宽的整段引用和未被用户接受的助手意图不会形成记忆。项目归属由模型按语义判断,不再要求项目名在证据里逐字出现:同一个项目换个说法也能落到已有 Scope 上,名字也不再决定这条记忆的生死。日期和截止日期使用统一的边界安全解析,todo 只在候选自己的证据给出唯一明确截止日时填充。Markdown 仍是唯一事实源。
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
- - 项目、负责人等归属不明确时延后记录,不猜测为 `global`;
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
- 普通自动提炼分为主题选择和内容提炼,正常使用两次模型调用,全部遵循 `single_pass` 思考配置(默认 `disabled`),包括候选修复。B3 是 Core 内部写入协议。候选校验失败时允许一次局部修复;结构修复和同目标协调共享总计三次的请求上限。固定安全路由保留实际 outbound 请求计数和持久化预占,不增加隐藏 host-to-API fallback。语义质量和耗时分别验收。
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 中执行。十秒目标仍只是性能观测,不能用局部测试或一次成功重放宣称所有语义场景已通过。
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memleaf"
7
- version = "0.2.61"
7
+ version = "0.2.63"
8
8
  description = "A local-first Markdown memory core for AI agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  """Local-first Markdown memory core for AI agents."""
2
2
 
3
- __version__ = "0.2.61"
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
@@ -1,5 +1,5 @@
1
1
  name: memleaf
2
- version: 0.2.61
2
+ version: 0.2.63
3
3
  description: "Hermes-native external MemoryProvider for local-first Markdown memory shared with the memleaf MCP server."
4
4
  hooks:
5
5
  - prefetch
@@ -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
- if deterministic_id in target_ids:
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 target_id.casefold() == deterministic_id.casefold():
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)