memleaf 0.2.88__tar.gz → 0.2.89__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 (183) hide show
  1. {memleaf-0.2.88 → memleaf-0.2.89}/CHANGELOG.md +25 -0
  2. {memleaf-0.2.88/src/memleaf.egg-info → memleaf-0.2.89}/PKG-INFO +4 -4
  3. {memleaf-0.2.88 → memleaf-0.2.89}/README.en.md +3 -3
  4. {memleaf-0.2.88 → memleaf-0.2.89}/README.md +3 -3
  5. {memleaf-0.2.88 → memleaf-0.2.89}/docs/hermes-source-comparison-context.md +7 -3
  6. {memleaf-0.2.88 → memleaf-0.2.89}/pyproject.toml +1 -1
  7. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/__init__.py +1 -1
  8. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/hermes_provider/README.md +2 -2
  9. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/hermes_provider/_provider.py +12 -11
  10. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/hermes_provider/plugin.yaml +1 -1
  11. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/incremental_commit.py +1 -1
  12. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/incremental_preview.py +13 -12
  13. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/incremental_prompts.py +4 -3
  14. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/incremental_protocol.py +2 -2
  15. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/incremental_semantics.py +27 -6
  16. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/mcp_server.py +53 -11
  17. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/retrieval_gate.py +49 -1
  18. {memleaf-0.2.88 → memleaf-0.2.89/src/memleaf.egg-info}/PKG-INFO +4 -4
  19. {memleaf-0.2.88 → memleaf-0.2.89}/LICENSE +0 -0
  20. {memleaf-0.2.88 → memleaf-0.2.89}/MANIFEST.in +0 -0
  21. {memleaf-0.2.88 → memleaf-0.2.89}/docs/acceptance-evidence-guide.md +0 -0
  22. {memleaf-0.2.88 → memleaf-0.2.89}/docs/automatic-processing-route.md +0 -0
  23. {memleaf-0.2.88 → memleaf-0.2.89}/docs/body-compaction.md +0 -0
  24. {memleaf-0.2.88 → memleaf-0.2.89}/docs/candidate-reconciliation.md +0 -0
  25. {memleaf-0.2.88 → memleaf-0.2.89}/docs/capture-budget-design.md +0 -0
  26. {memleaf-0.2.88 → memleaf-0.2.89}/docs/config-migrations.md +0 -0
  27. {memleaf-0.2.88 → memleaf-0.2.89}/docs/core-refactor.md +0 -0
  28. {memleaf-0.2.88 → memleaf-0.2.89}/docs/development-closeout.md +0 -0
  29. {memleaf-0.2.88 → memleaf-0.2.89}/docs/evidence-retention.md +0 -0
  30. {memleaf-0.2.88 → memleaf-0.2.89}/docs/explicit-memory-update.md +0 -0
  31. {memleaf-0.2.88 → memleaf-0.2.89}/docs/extraction-latency.md +0 -0
  32. {memleaf-0.2.88 → memleaf-0.2.89}/docs/failed-run-recovery.md +0 -0
  33. {memleaf-0.2.88 → memleaf-0.2.89}/docs/gate-evidence-boundary.md +0 -0
  34. {memleaf-0.2.88 → memleaf-0.2.89}/docs/general-processing.md +0 -0
  35. {memleaf-0.2.88 → memleaf-0.2.89}/docs/hermes-mcp-runtime.md +0 -0
  36. {memleaf-0.2.88 → memleaf-0.2.89}/docs/hermes-provider-compatibility.md +0 -0
  37. {memleaf-0.2.88 → memleaf-0.2.89}/docs/host-acceptance-runbook.md +0 -0
  38. {memleaf-0.2.88 → memleaf-0.2.89}/docs/immutable-release-assets.md +0 -0
  39. {memleaf-0.2.88 → memleaf-0.2.89}/docs/incremental-commit-contract.md +0 -0
  40. {memleaf-0.2.88 → memleaf-0.2.89}/docs/incremental-execution.md +0 -0
  41. {memleaf-0.2.88 → memleaf-0.2.89}/docs/incremental-native-comparison.md +0 -0
  42. {memleaf-0.2.88 → memleaf-0.2.89}/docs/incremental-partial-recovery.md +0 -0
  43. {memleaf-0.2.88 → memleaf-0.2.89}/docs/incremental-planner-preview.md +0 -0
  44. {memleaf-0.2.88 → memleaf-0.2.89}/docs/incremental-runtime-contract.md +0 -0
  45. {memleaf-0.2.88 → memleaf-0.2.89}/docs/incremental-scope-registration.md +0 -0
  46. {memleaf-0.2.88 → memleaf-0.2.89}/docs/incremental-selected-retention.md +0 -0
  47. {memleaf-0.2.88 → memleaf-0.2.89}/docs/installed-artifact-verification.md +0 -0
  48. {memleaf-0.2.88 → memleaf-0.2.89}/docs/memory-lifecycle-tools.md +0 -0
  49. {memleaf-0.2.88 → memleaf-0.2.89}/docs/memory-state-contract.md +0 -0
  50. {memleaf-0.2.88 → memleaf-0.2.89}/docs/migration-preflight.md +0 -0
  51. {memleaf-0.2.88 → memleaf-0.2.89}/docs/processing-quality-acceptance.md +0 -0
  52. {memleaf-0.2.88 → memleaf-0.2.89}/docs/public-query-integrity.md +0 -0
  53. {memleaf-0.2.88 → memleaf-0.2.89}/docs/query-performance.md +0 -0
  54. {memleaf-0.2.88 → memleaf-0.2.89}/docs/remember-incremental-route.md +0 -0
  55. {memleaf-0.2.88 → memleaf-0.2.89}/docs/runtime-state-retention.md +0 -0
  56. {memleaf-0.2.88 → memleaf-0.2.89}/docs/semantic-extraction-protocol.md +0 -0
  57. {memleaf-0.2.88 → memleaf-0.2.89}/docs/source-work-contract.md +0 -0
  58. {memleaf-0.2.88 → memleaf-0.2.89}/docs/v0.2.26-processing-status.md +0 -0
  59. {memleaf-0.2.88 → memleaf-0.2.89}/examples/README.md +0 -0
  60. {memleaf-0.2.88 → memleaf-0.2.89}/examples/basic_usage.py +0 -0
  61. {memleaf-0.2.88 → memleaf-0.2.89}/examples/incremental_acceptance.json +0 -0
  62. {memleaf-0.2.88 → memleaf-0.2.89}/examples/mcp_stdio.ndjson +0 -0
  63. {memleaf-0.2.88 → memleaf-0.2.89}/examples/query_benchmark.py +0 -0
  64. {memleaf-0.2.88 → memleaf-0.2.89}/install.ps1 +0 -0
  65. {memleaf-0.2.88 → memleaf-0.2.89}/install.sh +0 -0
  66. {memleaf-0.2.88 → memleaf-0.2.89}/setup.cfg +0 -0
  67. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/__main__.py +0 -0
  68. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/acceptance.py +0 -0
  69. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/adapters/__init__.py +0 -0
  70. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/adapters/antigravity.py +0 -0
  71. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/adapters/base.py +0 -0
  72. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/adapters/codex.py +0 -0
  73. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/adapters/hermes.py +0 -0
  74. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/admission.py +0 -0
  75. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/batch_review.py +0 -0
  76. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/budget.py +0 -0
  77. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/capture.py +0 -0
  78. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/cli.py +0 -0
  79. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/compaction.py +0 -0
  80. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/config.py +0 -0
  81. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/create_coordinator.py +0 -0
  82. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/credentials.py +0 -0
  83. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/evidence_budget.py +0 -0
  84. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/evidence_policy.py +0 -0
  85. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/evidence_structure.py +0 -0
  86. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/evidence_syntax.py +0 -0
  87. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/explicit_text_source.py +0 -0
  88. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/extraction_budget.py +0 -0
  89. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/extraction_capability.py +0 -0
  90. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/extraction_work_state.py +0 -0
  91. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/frontmatter.py +0 -0
  92. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/hermes_provider/__init__.py +0 -0
  93. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/hermes_provider/_mcp_client.py +0 -0
  94. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/hermes_provider/_shared.py +0 -0
  95. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/hermes_provider/evidence_budget.py +0 -0
  96. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/hermes_provider/provider_compatibility.py +0 -0
  97. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/hermes_runtime.py +0 -0
  98. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/host_events.py +0 -0
  99. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/host_retention.py +0 -0
  100. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/host_runtime.py +0 -0
  101. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/host_turn_identity.py +0 -0
  102. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/inbox.py +0 -0
  103. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/incremental_dates.py +0 -0
  104. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/incremental_execution.py +0 -0
  105. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/incremental_explicit.py +0 -0
  106. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/incremental_failed_recovery.py +0 -0
  107. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/incremental_journal.py +0 -0
  108. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/incremental_merge.py +0 -0
  109. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/incremental_native.py +0 -0
  110. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/incremental_partial.py +0 -0
  111. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/incremental_recovery.py +0 -0
  112. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/incremental_run_state.py +0 -0
  113. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/incremental_runtime.py +0 -0
  114. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/incremental_scopes.py +0 -0
  115. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/incremental_selection.py +0 -0
  116. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/index.py +0 -0
  117. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/inspection.py +0 -0
  118. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/installer.py +0 -0
  119. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/legacy_update_journal.py +0 -0
  120. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/llm/__init__.py +0 -0
  121. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/llm/base.py +0 -0
  122. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/llm/claude_compatible.py +0 -0
  123. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/llm/gemini.py +0 -0
  124. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/llm/openai_compatible.py +0 -0
  125. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/llm/router.py +0 -0
  126. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/llm/thinking.py +0 -0
  127. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/locking.py +0 -0
  128. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/memory_commit.py +0 -0
  129. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/memory_planner.py +0 -0
  130. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/memory_retraction.py +0 -0
  131. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/memory_update.py +0 -0
  132. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/memory_writer.py +0 -0
  133. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/migration.py +0 -0
  134. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/model_capabilities.py +0 -0
  135. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/model_discovery.py +0 -0
  136. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/model_execution.py +0 -0
  137. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/models.py +0 -0
  138. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/native_index.py +0 -0
  139. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/native_registration.py +0 -0
  140. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/parallel_model.py +0 -0
  141. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/planning_candidates.py +0 -0
  142. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/planning_context.py +0 -0
  143. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/process_common.py +0 -0
  144. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/process_jobs.py +0 -0
  145. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/process_journal.py +0 -0
  146. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/process_owner.py +0 -0
  147. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/processing.py +0 -0
  148. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/processing_route.py +0 -0
  149. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/prompts.py +0 -0
  150. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/provenance.py +0 -0
  151. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/provider_compatibility.py +0 -0
  152. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/query_clock.py +0 -0
  153. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/query_progress.py +0 -0
  154. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/query_scan.py +0 -0
  155. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/receipt_codec.py +0 -0
  156. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/recording_policy.py +0 -0
  157. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/redaction.py +0 -0
  158. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/remember_route.py +0 -0
  159. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/retention.py +0 -0
  160. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/retrieval.py +0 -0
  161. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/runtime_retention.py +0 -0
  162. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/scope_maintenance.py +0 -0
  163. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/scope_state.py +0 -0
  164. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/semantic_maintenance.py +0 -0
  165. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/semantic_protocol.py +0 -0
  166. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/service.py +0 -0
  167. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/single_pass_memory_planner.py +0 -0
  168. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/single_pass_plan.py +0 -0
  169. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/source_policy.py +0 -0
  170. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/state_layout.py +0 -0
  171. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/subprocess_flags.py +0 -0
  172. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/summary_batch.py +0 -0
  173. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/target_reconciliation.py +0 -0
  174. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/turn_audit.py +0 -0
  175. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/turn_plan.py +0 -0
  176. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/update_coordinator.py +0 -0
  177. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/update_review.py +0 -0
  178. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/validation.py +0 -0
  179. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf/vault.py +0 -0
  180. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf.egg-info/SOURCES.txt +0 -0
  181. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf.egg-info/dependency_links.txt +0 -0
  182. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf.egg-info/entry_points.txt +0 -0
  183. {memleaf-0.2.88 → memleaf-0.2.89}/src/memleaf.egg-info/top_level.txt +0 -0
@@ -4,6 +4,31 @@ All notable changes to memleaf are documented here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.2.89 — 2026-10-04
8
+
9
+ ### Complete comparison context and bound tool continuation
10
+
11
+ - Remove the 20-memory comparison ceiling and capture truncation. Keep all
12
+ required read, receipt and revision targets, limiting only optional recall
13
+ and the existing total request size. Preserve strict source, scope and
14
+ missing-target validation; incomplete historical capsules remain protected.
15
+ - Use one discoverable Hermes MCP `save_turn` route for source-bound retention
16
+ and lifecycle requests. Stop exposing a competing native save schema while
17
+ preserving its legacy handler and internal completion interface. Queueing
18
+ remains idempotent and does not claim that changes have already been saved.
19
+ - Bind long-body continuation to server-owned memory IDs, versions, offsets
20
+ and history flags. Clients follow `body_continuation` without copying record
21
+ tuples; version changes require an explicit restart of the same target.
22
+ Preserve direct versioned reads and strict current-turn checks.
23
+ - Keep delivery dates and progress out of task titles, retaining years that
24
+ identify the subject. Verify body clauses against actual user-confirmed
25
+ facts and repair unsupported prose in the existing review pass without
26
+ changing evidence, ownership or deadlines. No model stage is added.
27
+ - Validation: 411 affected local checks, an isolated replay of the real
28
+ 21-action conversation, and three real DeepSeek extraction/review samples
29
+ passed. Formal Vault data and historical jobs were not changed; updated
30
+ Hermes desktop behavior still requires installation and a host restart.
31
+
7
32
  ## 0.2.88 — 2026-10-04
8
33
 
9
34
  ### Reliable queued retention and bounded action queries
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memleaf
3
- Version: 0.2.88
3
+ Version: 0.2.89
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.88。**
26
+ > **版本:0.2.89。**
27
27
  > 自动提炼和复核共用同一条“未来记忆价值”标准:模型综合未来复用、信息增量、再次读取时的直接可用性和忘记成本,只保留对未来理解、判断或行动有实质影响的最小核心;没有明确价值的信息不提炼,不按具体业务场景硬编码排除。
28
28
  > 提炼先比较旧事项中的编号、目标、进展、责任和期限;补全未知信息也应维护原事项,另建独立待办不能替代项目事实更新。确认重复的记忆在现有处理请求中合并,保留历史与来源;项目和独立任务保持各自生命周期,Core 不按标题强行合并。
29
29
  > 新请求兼容等价的字段表示;未知执行人保持 `null`,新增或变更负责人和期限需要真实来源;等待条件在正文维护,不再提炼 `waiting_on`。行动及相关字段经过有界语义复核,提炼与复核共用每份工作的最多五次模型请求额度,成功即停止;复核失败不写入。来源时间用于解析相对日期,不补造事实日期。
@@ -166,13 +166,13 @@ python -m pip install -U memleaf && python -m memleaf install
166
166
  3. 发现并保存可用的聊天模型路由;Hermes CLI 返回的脱敏凭证不会被当成真实 API key,而会继续尝试环境变量和 Hermes `.env`;已有有效 memleaf 路由会保留;
167
167
  4. 激活 `memory.provider=memleaf`;
168
168
  5. 通过 Hermes 官方 CLI 配置 memleaf MCP;
169
- 6. 配置 MCP lazy/idle 生命周期,以及绑定真实用户消息的 Hermes 原生保存工具;
169
+ 6. 配置 MCP lazy/idle 生命周期,以及绑定真实用户消息的 Hermes MCP 保存入口;
170
170
  7. 验证 MCP Server 能发现当前版本声明的全部工具;
171
171
  8. 写入本地 Agent 状态索引。
172
172
 
173
173
  完成后重启 Hermes。
174
174
 
175
- Hermes 显式保存使用原生 `memleaf_remember`:先绑定本轮并排队,轮次完成后再以真实用户消息提炼,排队不表示已保存。安装器会在 Hermes 中屏蔽独立 MCP `remember`,保留其他工具选择;Core/Python API 和其他客户端的 `remember` 继续可用。仅升级 pip 包不足以更新 Provider 和这项工具配置。
175
+ Hermes 显式保存使用 MCP `save_turn`:先绑定本轮并排队,轮次完成后再以真实用户消息提炼,排队不表示已保存。它与检索工具共用 Hermes 的延迟工具入口,Provider 不再另外暴露原生保存工具。安装器会在 Hermes 中屏蔽独立 MCP `remember`,保留其他工具选择;Core/Python API 和其他客户端的 `remember` 继续可用。仅升级 pip 包不足以更新 Provider 和这项工具配置。
176
176
 
177
177
  如果没有检测到 Hermes、无法取得完整模型路由、Provider 激活失败或 MCP 工具发现验证失败,安装会明确返回失败,不会把未完成的接入报告为成功。
178
178
 
@@ -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.88.**
7
+ > **Version: 0.2.89.**
8
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
9
  > Extraction compares existing identifiers, objectives, progress, responsibility and deadlines first. Filling an unknown also maintains the original item; a separate task does not replace updating shared project facts. Confirmed duplicates can be merged in the existing processing requests with their sources and history preserved. Independent project and task lifecycles remain separate; Core does not merge matching titles.
10
10
  > New requests accept equivalent field representations. Unknown executors remain `null`; new or changed responsibility and deadlines require actual source evidence. Dependencies belong in the body; `waiting_on` is no longer extracted. Proposed actions and related fields undergo bounded semantic verification within a shared five-request limit per work item, stopping on success; failed verification blocks writes. Source time resolves relative dates without inventing factual dates.
@@ -147,13 +147,13 @@ Both installation paths automatically:
147
147
  3. Discover and save a callable chat-model route. Redacted credentials returned by the Hermes CLI are never treated as real API keys; discovery falls through to environment variables and Hermes `.env`, while preserving an existing valid memleaf route.
148
148
  4. Activate `memory.provider=memleaf`.
149
149
  5. Configure the memleaf MCP entry through Hermes' official CLI.
150
- 6. Configure MCP lazy/idle lifecycle settings and the source-bound native Hermes retention tool.
150
+ 6. Configure MCP lazy/idle lifecycle settings and the source-bound Hermes MCP save route.
151
151
  7. Verify that the MCP server exposes all tools declared by the installed version.
152
152
  8. Record the local Agent integration status.
153
153
 
154
154
  Restart Hermes after installation.
155
155
 
156
- Hermes uses native `memleaf_remember` to queue a request bound to the current turn, then retain the actual user messages after the turn completes. Queued does not mean saved. The installer excludes standalone MCP `remember` in Hermes while preserving other tool choices; the Core/Python API and other clients keep `remember`. Upgrading only the pip package does not refresh the Provider or this tool configuration.
156
+ Hermes uses MCP `save_turn` to queue a request bound to the current turn, then retain the actual user messages after the turn completes. Queued does not mean saved. It shares the deferred MCP tool space with retrieval; the Provider no longer exposes a separate native save schema. The installer excludes standalone MCP `remember` in Hermes while preserving other tool choices; the Core/Python API and other clients keep `remember`. Upgrading only the pip package does not refresh the Provider or this tool configuration.
157
157
 
158
158
  If Hermes cannot be detected, no complete model route can be configured, Provider activation fails, or MCP tool-discovery verification fails, the installer returns an explicit failure rather than reporting an incomplete integration as successful.
159
159
 
@@ -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.88。**
7
+ > **版本:0.2.89。**
8
8
  > 自动提炼和复核共用同一条“未来记忆价值”标准:模型综合未来复用、信息增量、再次读取时的直接可用性和忘记成本,只保留对未来理解、判断或行动有实质影响的最小核心;没有明确价值的信息不提炼,不按具体业务场景硬编码排除。
9
9
  > 提炼先比较旧事项中的编号、目标、进展、责任和期限;补全未知信息也应维护原事项,另建独立待办不能替代项目事实更新。确认重复的记忆在现有处理请求中合并,保留历史与来源;项目和独立任务保持各自生命周期,Core 不按标题强行合并。
10
10
  > 新请求兼容等价的字段表示;未知执行人保持 `null`,新增或变更负责人和期限需要真实来源;等待条件在正文维护,不再提炼 `waiting_on`。行动及相关字段经过有界语义复核,提炼与复核共用每份工作的最多五次模型请求额度,成功即停止;复核失败不写入。来源时间用于解析相对日期,不补造事实日期。
@@ -147,13 +147,13 @@ python -m pip install -U memleaf && python -m memleaf install
147
147
  3. 发现并保存可用的聊天模型路由;Hermes CLI 返回的脱敏凭证不会被当成真实 API key,而会继续尝试环境变量和 Hermes `.env`;已有有效 memleaf 路由会保留;
148
148
  4. 激活 `memory.provider=memleaf`;
149
149
  5. 通过 Hermes 官方 CLI 配置 memleaf MCP;
150
- 6. 配置 MCP lazy/idle 生命周期,以及绑定真实用户消息的 Hermes 原生保存工具;
150
+ 6. 配置 MCP lazy/idle 生命周期,以及绑定真实用户消息的 Hermes MCP 保存入口;
151
151
  7. 验证 MCP Server 能发现当前版本声明的全部工具;
152
152
  8. 写入本地 Agent 状态索引。
153
153
 
154
154
  完成后重启 Hermes。
155
155
 
156
- Hermes 显式保存使用原生 `memleaf_remember`:先绑定本轮并排队,轮次完成后再以真实用户消息提炼,排队不表示已保存。安装器会在 Hermes 中屏蔽独立 MCP `remember`,保留其他工具选择;Core/Python API 和其他客户端的 `remember` 继续可用。仅升级 pip 包不足以更新 Provider 和这项工具配置。
156
+ Hermes 显式保存使用 MCP `save_turn`:先绑定本轮并排队,轮次完成后再以真实用户消息提炼,排队不表示已保存。它与检索工具共用 Hermes 的延迟工具入口,Provider 不再另外暴露原生保存工具。安装器会在 Hermes 中屏蔽独立 MCP `remember`,保留其他工具选择;Core/Python API 和其他客户端的 `remember` 继续可用。仅升级 pip 包不足以更新 Provider 和这项工具配置。
157
157
 
158
158
  如果没有检测到 Hermes、无法取得完整模型路由、Provider 激活失败或 MCP 工具发现验证失败,安装会明确返回失败,不会把未完成的接入报告为成功。
159
159
 
@@ -40,9 +40,13 @@ assistant event, including the immutable capture receipt and payload digest.
40
40
  It accepts no caller-supplied list of IDs through this interface. The capsule is
41
41
  comparison metadata, not factual evidence, and its token is not projected into
42
42
  the model request. Automatic preparation gives these required targets priority
43
- alongside explicit-write receipts before filling the bounded candidate budget.
44
- A missing target or insufficient budget blocks preparation instead of silently
45
- replacing or dropping the required target. No model call is added to capture.
43
+ alongside explicit-write receipts. There is no 20-ID truncation or target-count
44
+ ceiling: required targets expand the preferred candidate count; optional recall
45
+ only fills remaining slots. A missing target or an oversized request still blocks
46
+ preparation instead of silently dropping required context. The existing request
47
+ byte budget remains; no model call is added to capture. Legacy capsules already
48
+ marked overflow remain incomplete and cannot be silently reconstructed from a
49
+ later live gate.
46
50
 
47
51
  A delayed callback can use its own unexpired gate even after a later turn starts.
48
52
  Once the capsule is captured, gate expiry cannot erase it or break a duplicate
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memleaf"
7
- version = "0.2.88"
7
+ version = "0.2.89"
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.88"
3
+ __version__ = "0.2.89"
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
@@ -10,14 +10,14 @@ Automatic capture and processing use only the visible user and assistant text.
10
10
  Tool calls and results, email or attachment bodies, and other hidden payloads are
11
11
  not automatic memory input.
12
12
  Hermes uses MCP `search` and `read` with the current `retrieval_id` for recall.
13
- The native `memleaf_remember` tool queues explicit retention bound to the current
13
+ The deferred MCP `save_turn` tool queues explicit retention bound to the current
14
14
  turn; the completed capture supplies the original user messages and source times.
15
15
  A queued response is not a saved memory. The installer excludes standalone MCP
16
16
  `remember` in Hermes while preserving other configured tool choices. MCP still
17
17
  supports `remember` for other clients, plus `forget` and maintenance operations.
18
18
  This is a Soft Gate, not a guarantee that every answer has performed retrieval.
19
19
 
20
- The bound native route also handles conversational corrections, completion,
20
+ The bound MCP route also handles conversational corrections, completion,
21
21
  cancellation, reopening and withdrawal. A successful full-turn retention receipt
22
22
  settles automatic extraction without another model request. Partial selections
23
23
  remain separate. MCP lifecycle updates do not accept model-supplied source times;
@@ -1371,7 +1371,8 @@ class MemleafMemoryProvider(MemoryProvider):
1371
1371
  return (
1372
1372
  "# Memleaf Memory\n"
1373
1373
  "For a request to save, correct, complete, cancel, reopen, or withdraw memory, "
1374
- "call memleaf_remember once with this turn's retrieval_id. It queues the actual "
1374
+ "discover and call the deferred MCP tool mcp__memleaf__save_turn once with this turn's retrieval_id, "
1375
+ "using the exact name returned by tool_search through tool_call. It queues the actual "
1375
1376
  "whole turn; no rewritten content, target revision, or message time is needed. "
1376
1377
  "Use the returned user_message as the save acknowledgement. A queued request "
1377
1378
  "starts processing only after your final reply, so its changes are pending.\n"
@@ -1390,8 +1391,8 @@ class MemleafMemoryProvider(MemoryProvider):
1390
1391
  "todos, all unfinished work, urgent work, or work due in a time range, call memleaf MCP "
1391
1392
  "list_todos(view=full) instead of relevance search; omit scope for a global query. "
1392
1393
  "Use the returned continuation arguments until has_more=false, without copying a cursor. "
1393
- "Each row includes a body; read only truncated rows with offset=body_next_offset "
1394
- "and expected_version=version, following their remaining body pages. "
1394
+ "Each row includes a body; after list pagination, follow the returned body_continuation "
1395
+ "with MCP read until it is absent. The server owns the memory ID, version and offset. "
1395
1396
  "Never exclude a todo because another Hermes session or another Agent created it. Hermes has a soft "
1396
1397
  "observer only: do not claim a search happened unless the visible tool "
1397
1398
  "messages show it. Visible Hermes "
@@ -2067,8 +2068,10 @@ class MemleafMemoryProvider(MemoryProvider):
2067
2068
  if not tool_name.startswith(prefix):
2068
2069
  return None
2069
2070
  name = tool_name[len(prefix):]
2070
- if name not in {"search", "read", "list_todos", "scope_catalog", "remember", "update_memory"}:
2071
+ if name not in {"search", "read", "list_todos", "scope_catalog", "remember", "update_memory", "save_turn"}:
2071
2072
  return None
2073
+ if name == "save_turn" and (not self._write_enabled or not self._gate_enabled):
2074
+ raise ValueError("memory_host_retention_disabled")
2072
2075
  turn_id = _host_turn_id(turn_context)
2073
2076
  session = turn_context.get("session_id") if isinstance(turn_context, Mapping) else None
2074
2077
  ordinal = turn_context.get("turn_number") if isinstance(turn_context, Mapping) else None
@@ -2105,13 +2108,11 @@ class MemleafMemoryProvider(MemoryProvider):
2105
2108
  return bound
2106
2109
 
2107
2110
  def get_tool_schemas(self) -> List[Dict[str, Any]]:
2108
- # Hermes registers schemas before initialize_all. Execution remains
2109
- # gated by the initialized host identity and write policy.
2110
- if not self._write_enabled:
2111
- return []
2112
- return [{"name": "memleaf_remember", "description": "Queue the actual complete user turn for a request to save/correct memory, complete/cancel/reopen a task, or withdraw a fact/preference. Call once, with only the current retrieval_id. Reply with the returned user_message to acknowledge the queued request; processing starts after the final reply, so no changes are completed yet.",
2113
- "parameters": {"type": "object", "properties": {"retrieval_id": {"type": "string"}},
2114
- "required": ["retrieval_id"], "additionalProperties": False}}]
2111
+ # One discoverable MCP save_turn route avoids the host's split between
2112
+ # native provider tools and deferred tool_call dispatch. Keep the old
2113
+ # handler below for already registered native clients, without exposing
2114
+ # two competing ways to save the same turn.
2115
+ return []
2115
2116
 
2116
2117
  def handle_tool_call(self, tool_name: str, args: Dict[str, Any], **kwargs: Any) -> str:
2117
2118
  if (tool_name != "memleaf_remember" or not self._write_enabled or not self._gate_enabled
@@ -1,5 +1,5 @@
1
1
  name: memleaf
2
- version: 0.2.88
2
+ version: 0.2.89
3
3
  description: "Hermes-native external MemoryProvider for local-first Markdown memory shared with the memleaf MCP server."
4
4
  hooks:
5
5
  - prefetch
@@ -56,7 +56,7 @@ def _arguments(source, session_id, turn_id, scope, priority_memory_ids, candidat
56
56
  source, session_id = safe_component(source, "source"), safe_component(session_id, "session id")
57
57
  if not isinstance(turn_id, str) or not turn_id or len(turn_id) > 800:
58
58
  raise ValueError("invalid_turn_id")
59
- if type(candidate_limit) is not int or not 1 <= candidate_limit <= 20:
59
+ if type(candidate_limit) is not int or candidate_limit < 1:
60
60
  raise ValueError("invalid_candidate_limit")
61
61
  if type(allow_new_scopes) is not bool:
62
62
  raise ValueError("invalid_context_flags")
@@ -30,13 +30,14 @@ def _prepare_incremental_unlocked(service: Any, *, source: str, session_id: str,
30
30
 
31
31
  At this stage evidence units are complete visible messages, not per-sentence
32
32
  heuristic fragments. Caller-provided target IDs are required context and are
33
- never dropped to fit the budget. Other candidates share the budget by query.
33
+ never dropped to fit a count limit. candidate_limit bounds optional recall;
34
+ required comparison targets expand that count within the request byte budget.
34
35
  """
35
36
  source = safe_component(source, "source")
36
37
  session_id = safe_component(session_id, "session id")
37
38
  if not isinstance(turn_id, str) or not turn_id:
38
39
  raise ValueError("invalid_turn_id")
39
- if type(candidate_limit) is not int or not 1 <= candidate_limit <= 20:
40
+ if type(candidate_limit) is not int or candidate_limit < 1:
40
41
  raise ValueError("invalid_candidate_limit")
41
42
  from .incremental_scopes import registry_view, resolve_scope
42
43
  registry, scope_guard, scope_aliases = registry_view(service.vault.config())
@@ -47,8 +48,6 @@ def _prepare_incremental_unlocked(service: Any, *, source: str, session_id: str,
47
48
  if isinstance(priority_memory_ids, (str, bytes)):
48
49
  raise ValueError("invalid_priority_ids")
49
50
  priority = list(dict.fromkeys(priority_memory_ids))
50
- if len(priority) > candidate_limit:
51
- raise ValueError("blocked_context")
52
51
  for identity in priority:
53
52
  safe_component(identity, "memory id")
54
53
  # Do not use the mutation boundary: it would resume compaction writes.
@@ -149,15 +148,13 @@ def _prepare_incremental_unlocked(service: Any, *, source: str, session_id: str,
149
148
  if context.get("overflow") is not False:
150
149
  raise ValueError("blocked_context")
151
150
  identities = context.get("memory_ids")
152
- if not isinstance(identities, list) or len(identities) > 20:
151
+ if not isinstance(identities, list):
153
152
  raise ValueError("invalid_comparison_context")
154
153
  for identity in identities:
155
154
  safe_component(identity, "memory id")
156
155
  read_ids.extend(identities)
157
156
  priority = list(dict.fromkeys(explicit_ids + read_ids + priority))
158
157
  priority += sorted(x for x in prior_ids if x.casefold() in records and x not in priority)
159
- if len(priority) > candidate_limit:
160
- raise ValueError("blocked_context")
161
158
  chosen = []
162
159
  for identity in priority:
163
160
  if identity.casefold() not in records:
@@ -165,15 +162,19 @@ def _prepare_incremental_unlocked(service: Any, *, source: str, session_id: str,
165
162
  chosen.append(records[identity.casefold()])
166
163
  from .planning_candidates import ComparisonRanker
167
164
  ranker = ComparisonRanker(records)
165
+ # Required reads/receipts are supplied in full. Recall adds only enough
166
+ # optional targets to reach the caller's preferred count, without ranking
167
+ # unrelated memories when required context already exceeds it.
168
+ optional_limit = min(max(0, candidate_limit - len(chosen)), len(records))
169
+ seen = {memory.memory_id.casefold() for memory in chosen}
168
170
  query_rows = []
169
- for event in evidence:
171
+ for event in evidence if optional_limit else ():
170
172
  for is_native in (False, True):
171
- keys = [key for key in records if (key in native_keys) == is_native]
172
- query_rows.append([records[key] for key in ranker.rank(event["text"], keys, candidate_limit)])
173
+ keys = [key for key in records if key not in seen and (key in native_keys) == is_native]
174
+ query_rows.append([records[key] for key in ranker.rank(event["text"], keys, optional_limit)])
173
175
  # Reuse the pure retrieval functions, not service._search_unlocked(),
174
176
  # whose index accessor may rebuild files or recover compaction.
175
- seen = {memory.memory_id.casefold() for memory in chosen}
176
- for rank in range(candidate_limit):
177
+ for rank in range(optional_limit):
177
178
  for candidates in query_rows:
178
179
  if len(chosen) >= candidate_limit:
179
180
  break
@@ -6,9 +6,10 @@ INCREMENTAL_SYSTEM = """从完整可见对话提炼长期记忆,返回严格 J
6
6
  - 用户明确撤回旧事实或偏好:UPDATE 原 target,patch.validity=retracted;撤回说明转入历史,不作为仍有效的偏好保留。用户明确给出替代事实/新偏好:UPDATE 原 target 为新的有效内容。撤回后“需要时另说”不代表已建立替代偏好。
7
7
  - user 的已确认事实、决定、持续目标、责任、期限、进展、偏好可保留。临时操作请求及普通应答不自动产生长期待办。
8
8
  - assistant 仅能解释已确认事实、报告有依据的已执行结果。assistant 的建议、计划、推测、额外安排不产生用户/第三人义务、承诺、偏好或期限;用户确认一件事不等于确认回复的其他建议。不要把“建议以后做”改写成“需要做”。
9
+ - 逐分句核验正文:assistant 增加的原因、影响、条件或后续步骤,user 未确认则省略;其余已确认事实保留。
9
10
  - use=new 是本轮变化依据;context、旧目标、explicit_writes 仅供比较,不能独立产生新事实。逐项核对证据是否支持拟写事实、归属、执行人和日期。
10
11
 
11
- 维护前先比较 new 与目标 observed 的先后。new 已被较新 context/目标覆盖时,不重放较新变化,本轮无变化用 NO_CHANGE;不得借 context 推动 deadline 清空。按身份/编号、主体、目标和业务上下文比较所有候选旧事项,包含已完成项和跨会话来源。缺编号的旧目标得到编号、旧状态被纠正、结构字段得到明确依据,都 UPDATE 原目标;完整涵盖且无变化才 NO_CHANGE。独立新事项 CREATE,不凭同标题或词语相似合并。同一事实变更同步维护全部相关候选;项目/任务旧职责不能遗漏为 NO_CHANGE。项目与独立任务各自保留生命周期。只有确认为同一事项的重复记录才 MERGE,不凭标题相似合并。保留旧目标其余仍有效信息;当前正文表达当前状态,不追加日志。
12
+ 比较 new 与目标 observed 的先后;已被较新 context/目标覆盖的变化不重放,无变化 NO_CHANGE,不借 context 清空 deadline。按身份/编号、主体、目标和业务上下文比较全部候选,包含已完成项和跨会话来源。补编号、纠正状态、补有依据的结构字段均 UPDATE 原目标;完整覆盖才 NO_CHANGE。独立新事项 CREATE;同一变化同步维护全部相关候选,旧职责不可遗漏。项目与独立任务各保留生命周期;确为同一事项的重复记录才 MERGE,不凭标题相似合并。保留其余有效旧事实,正文表达当前状态,不追加日志。
12
13
 
13
14
  动作:
14
15
  CREATE:action、非空 evidence、memory;memory 必填 type/scope/title/body。
@@ -18,13 +19,13 @@ NO_CHANGE:action、非空 evidence、target;正文及结构化字段均已
18
19
  DEFERRED:action、非空 evidence、reason、need;reason=missing_identity|missing_context|conflict,need 说明无法确定的内容。
19
20
  NO_MEMORY:仅 action;自动模式整轮无值得维护/新增的内容时独占 items。
20
21
  每项 evidence 是引用字符串数组,例如 ["e1","e2"],不能复制整个证据对象。至少一个 use=new 引用,必须实际支持本项,不要求覆盖每条消息。
21
- 结构示例(省略号替换为实际内容):
22
+ 结构示例:
22
23
  {"action":"CREATE","evidence":["e1"],"memory":{"type":"fact","scope":"unscoped","title":"…","body":"…"}}
23
24
  {"action":"UPDATE","evidence":["e1"],"target":"m1","patch":{"body":"…"}}
24
- {"action":"NO_CHANGE","evidence":["e1"],"target":"m1"}
25
25
  {"items":[{"action":"NO_MEMORY"}]}
26
26
 
27
27
  字段:
28
+ title 标识事项,不放交期/发生日/进度,改存正文与结构字段;旧标题交期一并清理。名称固有年度等身份日期保留。
28
29
  type=fact|todo|preference|project|event|identity|other。有独立完成条件的持续行动设 actionable:true(todo 隐含);事实类别与行动属性可分开表达。
29
30
  status=active|completed|cancelled;完成/取消/交接同时纠正冲突的 title/body;独立未完行动不能被兄弟项完成吞掉。重新开启 UPDATE 使用 reopen:true 和 patch.status=active,需要较新明确依据。
30
31
  assignee 是实际执行人,只有明确由用户本人执行才 user,其他执行人用明确名称,未知为 null。用户记录/转发/协调不证明本人执行。等待依赖、卡点仅在正文维护,按最新事实替换过时内容,不生成等待字段。
@@ -24,7 +24,7 @@ from .turn_plan import revision_digest
24
24
  from .validation import ModelOutputError, parse_strict_json
25
25
 
26
26
  PROTOCOL_VERSION = "incremental-items-v1"
27
- SEMANTIC_PROTOCOL = "incremental-turn-v16"
27
+ SEMANTIC_PROTOCOL = "incremental-turn-v17"
28
28
  EXTRACTION_CONTRACT = "field-reviewed-v1"
29
29
  MAX_BYTES = 128 * 1024
30
30
  MAX_ITEMS = 64
@@ -341,7 +341,7 @@ class PlanningSnapshot:
341
341
  for status in basis_statuses.values())):
342
342
  raise ValueError("invalid_basis_status")
343
343
  targets = targets or {}
344
- if len(targets) > 20 or (writable and set(writable) - set(targets)):
344
+ if writable and set(writable) - set(targets):
345
345
  raise ValueError("invalid_targets")
346
346
  for ref, memory in targets.items():
347
347
  if not re.fullmatch(r"m[1-9][0-9]*", ref) or not isinstance(memory, Memory):
@@ -15,10 +15,11 @@ REVIEW_SYSTEM = """核验拟写入的持续行动。original_input 是唯一事
15
15
  每个 draft.items 都必须有且只有一个 decision,item 是从 0 开始的位置。keep 判断该事项本身是否由原文确立且持续值得保留。临时操作及助手额外建议不能产生用户长期待办,原话只证明另一个操作时 keep:false。独立事实不因其他候选有误而拒绝。
16
16
  keep:true 时,field_support 必须逐个列出候选 memory/patch 中的业务字段,每个布尔值判断该字段的拟议值是否受来源支持;不列 responsibility_basis、effective、at、reopen 等引用/控制字段。没有 replace 的 NO_CHANGE/DEFERRED/NO_MEMORY 的 field_support 为 {}。keep:false 时 field_support 为 {}。unverified_fields 是系统发现未能验证引用的可选责任字段,必须判为 false;不能补造引用。这不影响其他有依据的事实或任务。归属/客户尚未确定不自动表示行动被阻塞。
17
17
  不能仅检查引文存在:必须判断原话是否确立该具体行动、责任、状态和期限。assistant 的建议/计划不能赋予 user 新义务。转发、协调、记录不等于本人执行;支持旧任务不等于接受新安排。
18
+ body 必须逐个事实分句核验;assistant 自行增加的原因、影响判断、条件或后续步骤不受 user 原话支持,不能因整段主题相符就判 true。剔除无依据分句,保留其余已确认事实,可用 replace 修正 CREATE 的 title/body;没有修正时 body:false。title 只标识事项,交期/发生日/进度放正文与结构字段;名称固有的年度等身份信息保留。UPDATE/NO_CHANGE 的旧标题仍含交期时,在现有 replace/updates 中一起维护。
18
19
  尤其检查拟改 deadline:依赖条件、交接执行人、进度、尚未完成都不证明原期限已取消;只有明确取消/替换这个期限才支持变更。没有明确期限变化时 deadline:false,原期限沿用。卡点和等待条件仅在正文中维护,并清除已经解决的旧卡点。
19
20
  新引用只能来自 new;context 和旧目标只供比较,不能重新推动旧变化。候选若仅重新陈述旧目标已覆盖的内容,不应作为新 UPDATE。同一事项补充/纠正维护原目标,独立事项允许 CREATE,不能凭标题相似合并。
20
21
  逐字段判断后输出 decisions;对共同事实变更,检查所有候选记忆中是否仍有旧职责或旧状态,不能认可只更新一条而遗留冲突。确认 MERGE 各方确为同一事项且完整保留有效内容;独立生命周期的项目/任务不可合并。
21
- 修正遗漏时,decision 可增加 replace(同 target 的完整 UPDATE),field_support 判断替换后的字段;不得替换 DEFERRED,也不得新增义务或猜测未知。还可输出 updates:[{"row":UPDATE,"field_support":{...}}] 更新原 draft 未涉及的可写候选目标,证据仍只取原输入 new。没有修正就省略 replace/updates。每条修正必须保留仍有效的旧事实与原期限;撤回用 validity=retracted。不要输出解释。
22
+ 修正遗漏时,decision 可增加 replace(同 target 的完整 UPDATE,或原 CREATE 仅改 title/body 的完整副本),field_support 判断替换后的字段;CREATE 修正不得改变其 evidence、scope、type、责任、期限或引用。不得替换 DEFERRED,也不得新增义务或猜测未知。还可输出 updates:[{"row":UPDATE,"field_support":{...}}] 更新原 draft 未涉及的可写候选目标,证据仍只取原输入 new。没有修正就省略 replace/updates。每条修正必须保留仍有效的旧事实与原期限;撤回用 validity=retracted。不要输出解释。
22
23
  """
23
24
 
24
25
  _METADATA = {"responsibility_basis", "effective", "at", "reopen"}
@@ -165,11 +166,30 @@ def apply_review(request, response):
165
166
  raise ValueError
166
167
  row = rows[i]
167
168
  if "replace" in decision:
168
- if (i in data.get("invalid_representation_items", []) or not decision["keep"]
169
- or row.get("action") not in {"UPDATE", "NO_CHANGE"}):
169
+ if i in data.get("invalid_representation_items", []) or not decision["keep"]:
170
170
  raise ValueError
171
- replacement = _maintenance_update(decision["replace"], support, data["original_input"])
172
- if replacement["target"] != row["target"]:
171
+ if row.get("action") == "CREATE":
172
+ replacement = deepcopy(decision["replace"])
173
+ if (not isinstance(replacement, dict) or replacement.get("action") != "CREATE"
174
+ or not isinstance(replacement.get("memory"), dict)
175
+ or not isinstance(row.get("memory"), dict)
176
+ or set(support) != fields(replacement)
177
+ or any(v is not True for v in support.values())):
178
+ raise ValueError
179
+ # Review can repair prose, not rewrite the draft's source,
180
+ # authority, identity, lifecycle or field annotations.
181
+ def without_prose(value):
182
+ value = deepcopy(value)
183
+ for key in ("title", "body"):
184
+ value["memory"].pop(key, None)
185
+ return value
186
+ if without_prose(row) != without_prose(replacement):
187
+ raise ValueError
188
+ elif row.get("action") in {"UPDATE", "NO_CHANGE"}:
189
+ replacement = _maintenance_update(decision["replace"], support, data["original_input"])
190
+ if replacement["target"] != row["target"]:
191
+ raise ValueError
192
+ else:
173
193
  raise ValueError
174
194
  row = rows[i] = replacement
175
195
  data.get("unverified_fields", [[] for _ in rows])[i] = []
@@ -224,7 +244,8 @@ def apply_review(request, response):
224
244
  rows[i] = None
225
245
  rows = [r for r in rows if r is not None]
226
246
  additions = review.get("updates", [])
227
- if not isinstance(additions, list) or len(additions) > 20:
247
+ from .incremental_protocol import MAX_ITEMS
248
+ if not isinstance(additions, list) or len(rows) + len(additions) > MAX_ITEMS:
228
249
  raise ValueError
229
250
  used = {row.get("target") for row in rows if isinstance(row, dict)}
230
251
  for entry in additions:
@@ -34,6 +34,7 @@ from .retrieval_gate import (
34
34
  RetrievalGateError,
35
35
  begin_turn,
36
36
  guarded_read,
37
+ body_continuation,
37
38
  observe_search,
38
39
  observe_todo_list,
39
40
  todo_filter_key,
@@ -80,7 +81,7 @@ INSTRUCTIONS = (
80
81
  "and a current FOUND search or list_todos result; NO_MATCH, ERROR, and DEGRADED turns cannot read. "
81
82
  "For global current-action questions use list_todos rather than relevance search, omit scope to cover "
82
83
  "all scopes. Use view=full on server-managed turns, follow returned continuation arguments "
83
- "until has_more=false, and read only truncated bodies using body_next_offset/version. "
84
+ "until has_more=false, then follow body_continuation with read until it is absent. "
84
85
  "Directory clients pass next_cursor as cursor, then read each relevant action body. "
85
86
  "For 'my work' use list_todos responsibility=mine; use delegated or all for others' work. "
86
87
  "Unassigned ownership is uncertain. A dependency alone does not create a user follow-up task; "
@@ -103,7 +104,7 @@ INSTRUCTIONS = (
103
104
  "Use remember only when the user explicitly asks to remember something. If the user has "
104
105
  "previously or currently explicitly said not to record corresponding text, skip capture for it. "
105
106
  "For conversation-based memory corrections, task completion/cancellation/reopening or "
106
- "withdrawal of a fact/preference, use the host's memleaf_remember with its current retrieval_id. "
107
+ "withdrawal of a fact/preference in Hermes, use MCP save_turn with its current retrieval_id. "
107
108
  "It binds the actual user request and preserves history. If unavailable, update_memory "
108
109
  "with retrieval_id queues the same source-bound edit. Bare explicit edits may omit the token. "
109
110
  "Withdraw facts/preferences with validity=retracted, not task status=cancelled. "
@@ -270,7 +271,7 @@ _TOOLS: tuple[dict[str, Any], ...] = (
270
271
  "page; memleaf returns one that must be reused for pagination and read. Server-managed clients continue with "
271
272
  "continue=true with only retrieval_id until has_more=false; the server carries the exact cursor and filters. "
272
273
  "Use view=full to receive bounded action bodies in the list; read only rows with body_has_more=true "
273
- "using offset=body_next_offset and expected_version=version. Directory clients may copy next_cursor into cursor, then read the matching action bodies with the same "
274
+ "by following body_continuation with read(continue=true,retrieval_id); the server binds the exact memory, version and offset. Directory clients may copy next_cursor into cursor, then read the matching action bodies with the same "
274
275
  "retrieval_id. Displayed list numbers are turn-local; resolve a later numbered update "
275
276
  "against the previously shown title and memory_id, and clarify if that mapping is ambiguous. "
276
277
  "responsibility=mine selects only explicit assignee=user; delegated selects other named owners, "
@@ -305,6 +306,8 @@ _TOOLS: tuple[dict[str, Any], ...] = (
305
306
  "characters and contains only identifiers, scopes, body, and paging/version fields; "
306
307
  "retrieval_id is required, and the retrieval turn must have a current FOUND search; "
307
308
  "NO_MATCH, ERROR, and DEGRADED turns are rejected. "
309
+ "Prefer returned body_continuation: pass only continue=true and retrieval_id to read the next unfinished body, with its ID/version/offset held by the server. "
310
+ "On memory_version_changed, add restart=true to that continuation to restart the same bound memory at offset 0 with its new version. "
308
311
  "continue with next_offset only when has_more is true, passing the returned version "
309
312
  "as expected_version. If the version changes, restart at offset=0 without it."
310
313
  ),
@@ -315,11 +318,18 @@ _TOOLS: tuple[dict[str, Any], ...] = (
315
318
  "offset": {"type": "integer", "minimum": 0},
316
319
  "max_chars": {"type": "integer", "minimum": 1},
317
320
  "expected_version": {"type": "string"},
321
+ "continue": {"type": "boolean", "description": "Use only continue=true and retrieval_id to follow a returned body_continuation."},
322
+ "restart": {"type": "boolean", "description": "With continue=true only, restart the bound unfinished body at offset 0 after memory_version_changed."},
318
323
  "retrieval_id": {"type": "string"},
319
324
  },
320
- required=["memory_id"],
325
+ any_of=[{"required": ["memory_id"]}, {"required": ["continue"]}],
321
326
  ),
322
327
  },
328
+ {
329
+ "name": "save_turn",
330
+ "description": "Hermes only: queue the actual user turn to save/correct memory, complete/cancel/reopen an action or withdraw a preference. Call once with the current retrieval_id. No rewritten text or target IDs. Return user_message as the acknowledgement; queued is not completed. This is a deferred MCP tool, invoked with the exact name returned by tool_search.",
331
+ "inputSchema": _object_schema({"retrieval_id": {"type": "string"}}, required=["retrieval_id"]),
332
+ },
323
333
  {
324
334
  "name": "process",
325
335
  "description": "Process complete inbox turns using the configured Core model route.",
@@ -1152,10 +1162,36 @@ def _invoke_tool(
1152
1162
  if args.get("view") == "full":
1153
1163
  for row in value["results"]:
1154
1164
  guarded_read(service.vault, retrieval_id, row["memory_id"],
1155
- lambda allowed, row=row: {"body": row["body"]}, current_source=managed_state["source"])
1165
+ lambda allowed, row=row: {"body": row["body"], "has_more": row["body_has_more"],
1166
+ "next_offset": row["body_next_offset"], "version": row["version"], "history": row["history"]},
1167
+ current_source=managed_state["source"])
1168
+ if body_continuation(service.vault, retrieval_id, current_source=managed_state["source"]):
1169
+ value["body_continuation"] = {"continue": True, "retrieval_id": retrieval_id}
1156
1170
  if value.get("has_more") and managed_state is not None:
1157
1171
  value["continuation"] = {"continue": True, "retrieval_id": retrieval_id}
1158
1172
  elif name == "read":
1173
+ continuation = args.pop("continue", False)
1174
+ restart = args.pop("restart", False)
1175
+ retrieval_id = args.pop("retrieval_id")
1176
+ state = validate_turn(service.vault, retrieval_id)
1177
+ current_source = state.get("source")
1178
+ if not isinstance(current_source, str) or not current_source:
1179
+ raise RetrievalGateError("retrieval_identity_invalid")
1180
+ expected = None
1181
+ if continuation:
1182
+ if args:
1183
+ raise RetrievalGateError("retrieval_body_continuation_mismatch")
1184
+ expected = body_continuation(service.vault, retrieval_id, current_source=current_source)
1185
+ if expected is None:
1186
+ raise RetrievalGateError("retrieval_body_continuation_mismatch")
1187
+ args = dict(expected)
1188
+ if restart:
1189
+ args["offset"] = 0
1190
+ args.pop("expected_version")
1191
+ elif restart:
1192
+ raise RetrievalGateError("retrieval_body_continuation_mismatch")
1193
+ elif "memory_id" not in args:
1194
+ raise ValueError("memory_id_required")
1159
1195
  # Keep the protocol boundary hard-capped even if a caller sends a
1160
1196
  # larger value. Core read_page performs the same authoritative
1161
1197
  # validation; this prevents a future adapter regression from
@@ -1165,23 +1201,29 @@ def _invoke_tool(
1165
1201
  args["max_chars"] = min(requested_max_chars, _MAX_READ_PAGE_CHARS)
1166
1202
  else:
1167
1203
  args["max_chars"] = _MAX_READ_PAGE_CHARS
1168
- retrieval_id = args.pop("retrieval_id")
1169
1204
 
1170
1205
  def read_page(allowed_chars: int) -> dict[str, Any] | None:
1171
1206
  page_args = {**args, "max_chars": min(args["max_chars"], allowed_chars)}
1172
- return _read_page_result(service.read_page(**page_args))
1207
+ page = _read_page_result(service.read_page(**page_args))
1208
+ if page is not None:
1209
+ page["history"] = page_args.get("include_history") is True
1210
+ return page
1173
1211
 
1174
- state = validate_turn(service.vault, retrieval_id)
1175
- current_source = state.get("source")
1176
- if not isinstance(current_source, str) or not current_source:
1177
- raise RetrievalGateError("retrieval_identity_invalid")
1178
1212
  value = guarded_read(
1179
1213
  service.vault,
1180
1214
  retrieval_id,
1181
1215
  args["memory_id"],
1182
1216
  read_page,
1183
1217
  current_source=current_source,
1218
+ expected_continuation=expected,
1184
1219
  )
1220
+ if value is not None:
1221
+ value.pop("history", None)
1222
+ if value is not None and body_continuation(service.vault, retrieval_id, current_source=current_source):
1223
+ value["body_continuation"] = {"continue": True, "retrieval_id": retrieval_id}
1224
+ elif name == "save_turn":
1225
+ from .host_retention import remember_turn
1226
+ value = remember_turn(service, phase="queue", retrieval_id=args["retrieval_id"])
1185
1227
  elif name == "process":
1186
1228
  background = args.pop("background", False)
1187
1229
  if background is True: