memleaf 0.2.66__tar.gz → 0.2.68__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 (240) hide show
  1. {memleaf-0.2.66 → memleaf-0.2.68}/CHANGELOG.md +30 -0
  2. {memleaf-0.2.66/src/memleaf.egg-info → memleaf-0.2.68}/PKG-INFO +1 -1
  3. {memleaf-0.2.66 → memleaf-0.2.68}/docs/acceptance-evidence-guide.md +11 -10
  4. {memleaf-0.2.66 → memleaf-0.2.68}/docs/automatic-processing-route.md +26 -26
  5. {memleaf-0.2.66 → memleaf-0.2.68}/docs/migration-preflight.md +6 -5
  6. {memleaf-0.2.66 → memleaf-0.2.68}/docs/remember-incremental-route.md +19 -19
  7. {memleaf-0.2.66 → memleaf-0.2.68}/pyproject.toml +1 -1
  8. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/__init__.py +1 -1
  9. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/cli.py +2 -2
  10. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/config.py +8 -6
  11. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/hermes_provider/_provider.py +15 -3
  12. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/hermes_provider/_shared.py +20 -2
  13. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/hermes_provider/plugin.yaml +1 -1
  14. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/incremental_protocol.py +29 -1
  15. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/installer.py +71 -6
  16. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/mcp_server.py +2 -2
  17. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/migration.py +12 -5
  18. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/process_jobs.py +18 -12
  19. memleaf-0.2.68/src/memleaf/processing.py +22 -0
  20. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/processing_route.py +31 -12
  21. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/service.py +17 -46
  22. {memleaf-0.2.66 → memleaf-0.2.68/src/memleaf.egg-info}/PKG-INFO +1 -1
  23. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_a083_regressions.py +73 -73
  24. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_a47_regressions.py +14 -8
  25. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_artifact_verification.py +35 -6
  26. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_control_absence_contract.py +12 -6
  27. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_hermes_source_metadata.py +84 -0
  28. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_incremental_execution.py +7 -3
  29. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_incremental_protocol.py +34 -0
  30. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_migration_backup.py +18 -1
  31. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_processed_state_contract.py +15 -9
  32. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_processing_entrypoints.py +23 -19
  33. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_processing_route.py +25 -17
  34. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_provider_compatibility.py +53 -2
  35. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_remember_route.py +24 -19
  36. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_source_followup_contract.py +60 -42
  37. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/verify_distribution.py +70 -0
  38. memleaf-0.2.66/src/memleaf/processing.py +0 -535
  39. {memleaf-0.2.66 → memleaf-0.2.68}/LICENSE +0 -0
  40. {memleaf-0.2.66 → memleaf-0.2.68}/MANIFEST.in +0 -0
  41. {memleaf-0.2.66 → memleaf-0.2.68}/README.en.md +0 -0
  42. {memleaf-0.2.66 → memleaf-0.2.68}/README.md +0 -0
  43. {memleaf-0.2.66 → memleaf-0.2.68}/docs/body-compaction.md +0 -0
  44. {memleaf-0.2.66 → memleaf-0.2.68}/docs/candidate-reconciliation.md +0 -0
  45. {memleaf-0.2.66 → memleaf-0.2.68}/docs/capture-budget-design.md +0 -0
  46. {memleaf-0.2.66 → memleaf-0.2.68}/docs/config-migrations.md +0 -0
  47. {memleaf-0.2.66 → memleaf-0.2.68}/docs/core-refactor.md +0 -0
  48. {memleaf-0.2.66 → memleaf-0.2.68}/docs/development-closeout.md +0 -0
  49. {memleaf-0.2.66 → memleaf-0.2.68}/docs/evidence-retention.md +0 -0
  50. {memleaf-0.2.66 → memleaf-0.2.68}/docs/explicit-memory-update.md +0 -0
  51. {memleaf-0.2.66 → memleaf-0.2.68}/docs/extraction-latency.md +0 -0
  52. {memleaf-0.2.66 → memleaf-0.2.68}/docs/gate-evidence-boundary.md +0 -0
  53. {memleaf-0.2.66 → memleaf-0.2.68}/docs/general-processing.md +0 -0
  54. {memleaf-0.2.66 → memleaf-0.2.68}/docs/hermes-mcp-runtime.md +0 -0
  55. {memleaf-0.2.66 → memleaf-0.2.68}/docs/hermes-provider-compatibility.md +0 -0
  56. {memleaf-0.2.66 → memleaf-0.2.68}/docs/host-acceptance-runbook.md +0 -0
  57. {memleaf-0.2.66 → memleaf-0.2.68}/docs/immutable-release-assets.md +0 -0
  58. {memleaf-0.2.66 → memleaf-0.2.68}/docs/incremental-commit-contract.md +0 -0
  59. {memleaf-0.2.66 → memleaf-0.2.68}/docs/incremental-execution.md +0 -0
  60. {memleaf-0.2.66 → memleaf-0.2.68}/docs/incremental-native-comparison.md +0 -0
  61. {memleaf-0.2.66 → memleaf-0.2.68}/docs/incremental-partial-recovery.md +0 -0
  62. {memleaf-0.2.66 → memleaf-0.2.68}/docs/incremental-planner-preview.md +0 -0
  63. {memleaf-0.2.66 → memleaf-0.2.68}/docs/incremental-runtime-contract.md +0 -0
  64. {memleaf-0.2.66 → memleaf-0.2.68}/docs/incremental-scope-registration.md +0 -0
  65. {memleaf-0.2.66 → memleaf-0.2.68}/docs/incremental-selected-retention.md +0 -0
  66. {memleaf-0.2.66 → memleaf-0.2.68}/docs/installed-artifact-verification.md +0 -0
  67. {memleaf-0.2.66 → memleaf-0.2.68}/docs/memory-state-contract.md +0 -0
  68. {memleaf-0.2.66 → memleaf-0.2.68}/docs/processing-quality-acceptance.md +0 -0
  69. {memleaf-0.2.66 → memleaf-0.2.68}/docs/public-query-integrity.md +0 -0
  70. {memleaf-0.2.66 → memleaf-0.2.68}/docs/query-performance.md +0 -0
  71. {memleaf-0.2.66 → memleaf-0.2.68}/docs/runtime-state-retention.md +0 -0
  72. {memleaf-0.2.66 → memleaf-0.2.68}/docs/semantic-extraction-protocol.md +0 -0
  73. {memleaf-0.2.66 → memleaf-0.2.68}/docs/source-work-contract.md +0 -0
  74. {memleaf-0.2.66 → memleaf-0.2.68}/docs/v0.2.26-processing-status.md +0 -0
  75. {memleaf-0.2.66 → memleaf-0.2.68}/examples/README.md +0 -0
  76. {memleaf-0.2.66 → memleaf-0.2.68}/examples/basic_usage.py +0 -0
  77. {memleaf-0.2.66 → memleaf-0.2.68}/examples/incremental_acceptance.json +0 -0
  78. {memleaf-0.2.66 → memleaf-0.2.68}/examples/mcp_stdio.ndjson +0 -0
  79. {memleaf-0.2.66 → memleaf-0.2.68}/examples/query_benchmark.py +0 -0
  80. {memleaf-0.2.66 → memleaf-0.2.68}/install.ps1 +0 -0
  81. {memleaf-0.2.66 → memleaf-0.2.68}/install.sh +0 -0
  82. {memleaf-0.2.66 → memleaf-0.2.68}/setup.cfg +0 -0
  83. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/__main__.py +0 -0
  84. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/acceptance.py +0 -0
  85. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/adapters/__init__.py +0 -0
  86. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/adapters/antigravity.py +0 -0
  87. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/adapters/base.py +0 -0
  88. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/adapters/codex.py +0 -0
  89. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/adapters/hermes.py +0 -0
  90. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/admission.py +0 -0
  91. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/batch_review.py +0 -0
  92. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/budget.py +0 -0
  93. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/capture.py +0 -0
  94. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/compaction.py +0 -0
  95. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/create_coordinator.py +0 -0
  96. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/credentials.py +0 -0
  97. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/evidence_budget.py +0 -0
  98. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/evidence_policy.py +0 -0
  99. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/evidence_structure.py +0 -0
  100. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/evidence_syntax.py +0 -0
  101. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/explicit_text_source.py +0 -0
  102. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/extraction_budget.py +0 -0
  103. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/extraction_capability.py +0 -0
  104. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/extraction_work_state.py +0 -0
  105. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/frontmatter.py +0 -0
  106. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/hermes_provider/README.md +0 -0
  107. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/hermes_provider/__init__.py +0 -0
  108. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/hermes_provider/_mcp_client.py +0 -0
  109. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/hermes_provider/evidence_budget.py +0 -0
  110. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/hermes_provider/provider_compatibility.py +0 -0
  111. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/hermes_runtime.py +0 -0
  112. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/host_events.py +0 -0
  113. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/host_runtime.py +0 -0
  114. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/inbox.py +0 -0
  115. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/incremental_commit.py +0 -0
  116. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/incremental_dates.py +0 -0
  117. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/incremental_execution.py +0 -0
  118. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/incremental_journal.py +0 -0
  119. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/incremental_native.py +0 -0
  120. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/incremental_partial.py +0 -0
  121. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/incremental_preview.py +0 -0
  122. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/incremental_prompts.py +0 -0
  123. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/incremental_recovery.py +0 -0
  124. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/incremental_run_state.py +0 -0
  125. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/incremental_runtime.py +0 -0
  126. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/incremental_scopes.py +0 -0
  127. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/incremental_selection.py +0 -0
  128. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/index.py +0 -0
  129. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/inspection.py +0 -0
  130. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/legacy_update_journal.py +0 -0
  131. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/llm/__init__.py +0 -0
  132. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/llm/base.py +0 -0
  133. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/llm/claude_compatible.py +0 -0
  134. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/llm/gemini.py +0 -0
  135. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/llm/openai_compatible.py +0 -0
  136. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/llm/router.py +0 -0
  137. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/llm/thinking.py +0 -0
  138. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/locking.py +0 -0
  139. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/memory_commit.py +0 -0
  140. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/memory_planner.py +0 -0
  141. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/memory_retraction.py +0 -0
  142. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/memory_update.py +0 -0
  143. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/memory_writer.py +0 -0
  144. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/model_capabilities.py +0 -0
  145. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/model_discovery.py +0 -0
  146. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/model_execution.py +0 -0
  147. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/models.py +0 -0
  148. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/native_index.py +0 -0
  149. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/native_registration.py +0 -0
  150. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/parallel_model.py +0 -0
  151. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/planning_context.py +0 -0
  152. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/process_common.py +0 -0
  153. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/process_journal.py +0 -0
  154. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/process_owner.py +0 -0
  155. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/prompts.py +0 -0
  156. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/provenance.py +0 -0
  157. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/provider_compatibility.py +0 -0
  158. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/query_clock.py +0 -0
  159. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/query_progress.py +0 -0
  160. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/query_scan.py +0 -0
  161. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/receipt_codec.py +0 -0
  162. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/recording_policy.py +0 -0
  163. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/redaction.py +0 -0
  164. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/remember_route.py +0 -0
  165. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/retention.py +0 -0
  166. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/retrieval.py +0 -0
  167. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/retrieval_gate.py +0 -0
  168. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/runtime_retention.py +0 -0
  169. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/scope_maintenance.py +0 -0
  170. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/scope_state.py +0 -0
  171. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/semantic_maintenance.py +0 -0
  172. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/semantic_protocol.py +0 -0
  173. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/single_pass_memory_planner.py +0 -0
  174. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/single_pass_plan.py +0 -0
  175. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/source_policy.py +0 -0
  176. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/state_layout.py +0 -0
  177. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/subprocess_flags.py +0 -0
  178. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/summary_batch.py +0 -0
  179. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/target_reconciliation.py +0 -0
  180. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/turn_audit.py +0 -0
  181. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/turn_plan.py +0 -0
  182. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/update_coordinator.py +0 -0
  183. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/update_review.py +0 -0
  184. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/validation.py +0 -0
  185. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf/vault.py +0 -0
  186. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf.egg-info/SOURCES.txt +0 -0
  187. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf.egg-info/dependency_links.txt +0 -0
  188. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf.egg-info/entry_points.txt +0 -0
  189. {memleaf-0.2.66 → memleaf-0.2.68}/src/memleaf.egg-info/top_level.txt +0 -0
  190. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/acceptance_oracles.py +0 -0
  191. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/candidate_update_fixtures.py +0 -0
  192. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/incremental_test_support.py +0 -0
  193. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/platform_smoke.py +0 -0
  194. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/release_assets.py +0 -0
  195. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/run_contracts.py +0 -0
  196. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_acceptance.py +0 -0
  197. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_acceptance_boundaries.py +0 -0
  198. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_body_compaction.py +0 -0
  199. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_candidate_reconciliation.py +0 -0
  200. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_contract_reporter.py +0 -0
  201. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_create_memory_contract.py +0 -0
  202. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_development_closeout.py +0 -0
  203. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_explicit_memory_update.py +0 -0
  204. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_explicit_update_contract.py +0 -0
  205. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_g1_memory_contract.py +0 -0
  206. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_g2_source_contract.py +0 -0
  207. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_hermes_native_provision.py +0 -0
  208. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_incremental_commit.py +0 -0
  209. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_incremental_configured_runtime.py +0 -0
  210. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_incremental_dates.py +0 -0
  211. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_incremental_execution_recovery.py +0 -0
  212. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_incremental_native.py +0 -0
  213. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_incremental_partial.py +0 -0
  214. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_incremental_partial_boundaries.py +0 -0
  215. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_incremental_partial_recovery.py +0 -0
  216. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_incremental_preview.py +0 -0
  217. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_incremental_recovery.py +0 -0
  218. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_incremental_remember.py +0 -0
  219. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_incremental_remember_recovery.py +0 -0
  220. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_incremental_scope_recovery.py +0 -0
  221. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_incremental_scopes.py +0 -0
  222. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_inspection_read_budget.py +0 -0
  223. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_mcp_retrieval_fallback.py +0 -0
  224. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_migration_runtime.py +0 -0
  225. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_native_read_stability.py +0 -0
  226. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_planner_provenance_contract.py +0 -0
  227. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_platform_contracts.py +0 -0
  228. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_processing_route_recovery.py +0 -0
  229. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_public_query_integrity.py +0 -0
  230. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_query_benchmark.py +0 -0
  231. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_query_clock.py +0 -0
  232. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_query_progress.py +0 -0
  233. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_query_scan_reuse.py +0 -0
  234. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_release_assets.py +0 -0
  235. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_remember_route_recovery.py +0 -0
  236. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_revision_target_integrity.py +0 -0
  237. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_runtime_retention.py +0 -0
  238. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_runtime_retention_observation.py +0 -0
  239. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_runtime_retention_recovery.py +0 -0
  240. {memleaf-0.2.66 → memleaf-0.2.68}/tests_public/test_vault_binding_contract.py +0 -0
@@ -4,6 +4,36 @@ All notable changes to memleaf are documented here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.2.68 — 2026-09-20
8
+
9
+ ### Hermes live-path hardening
10
+
11
+ - Observe Hermes deferred `tool_call` single-entry `calls[]` wrappers so real
12
+ memleaf search results are recorded as `NO_MATCH`/found instead of
13
+ `SEARCH_UNKNOWN`.
14
+ - Deterministically normalize only unambiguous flattened CREATE rows back into
15
+ the required `memory` object before the unchanged strict incremental validator;
16
+ malformed, incomplete and unknown-field variants remain fail-closed.
17
+ - Strip the trailing Hermes `<!--CLEAN-->` control marker at the Provider capture
18
+ boundary so it does not enter Inbox or memory extraction input.
19
+ - Validate the fixes with a real Hermes + DeepSeek Flash turn: retrieval audit
20
+ `NO_MATCH`, completed incremental extraction, three settled CREATE operations,
21
+ zero unresolved evidence and no CLEAN marker in Inbox.
22
+
23
+
24
+ ## 0.2.67 — 2026-09-20
25
+
26
+ ### Incremental-only processing and release integrity
27
+
28
+ - Remove the executable legacy processing/remember engine. New Vaults use the
29
+ incremental runner for automatic processing, explicit remember, MCP/CLI and
30
+ detached jobs; retained legacy settings/jobs remain inspectable but fail closed.
31
+ - Fix the v0.2.66 Hermes Provider manifest drift by aligning Core and Provider at
32
+ 0.2.67 and rejecting an internally inconsistent package before host/Vault changes.
33
+ - Gate source and wheel/sdist verification on one version identity across package
34
+ metadata, Core `__version__`, Hermes `plugin.yaml` and sdist project metadata.
35
+
36
+
7
37
  ## 0.2.66 — 2026-09-20
8
38
 
9
39
  ### Reconcile divergent local candidates
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memleaf
3
- Version: 0.2.66
3
+ Version: 0.2.68
4
4
  Summary: A local-first Markdown memory core for AI agents
5
5
  Author: memleaf contributors
6
6
  License-Expression: MIT
@@ -24,16 +24,17 @@ the suite count or from module-name references.
24
24
 
25
25
  ## Current route semantics
26
26
 
27
- Both `process.automatic_pipeline` and `process.remember_pipeline` default to
28
- `legacy`. Selecting one does not switch the other. A successfully installed
29
- candidate is not proof that either new route is active. Use the exact installed
30
- configuration and returned route/state when recording acceptance; no instructions
31
- here modify production defaults or grant a production switch.
32
-
33
- After isolation and authorization, the supported opt-in values are `incremental`
34
- for both keys. They are test configuration choices, not edits to package defaults.
35
- Existing queued work retains its route/authority contract. Do not erase old work,
36
- failed receipts or request counters to get a clean-looking preflight result.
27
+ The v0.2.67 candidate has one processing engine: incremental. New Vaults default
28
+ both `process.automatic_pipeline` and `process.remember_pipeline` to
29
+ `incremental`. A retained `legacy` value from an older Vault is not executable;
30
+ migration preflight reports it as `legacy_pipeline_configuration`, and runtime
31
+ entry points reject it with `legacy_pipeline_removed`.
32
+
33
+ A successfully installed candidate is still not proof that production migration is
34
+ safe. Existing legacy queue/control records retain their identity for diagnosis and
35
+ must be settled or explicitly reviewed; they are not reinterpreted as incremental
36
+ work. Do not erase old work, failed receipts or request counters to obtain a clean
37
+ preflight result.
37
38
 
38
39
  ## Preparation with no paid calls
39
40
 
@@ -1,31 +1,29 @@
1
- # Automatic processing route integration (G4a, opt-in)
1
+ # Automatic processing on the incremental engine
2
2
 
3
- This increment routes the **existing** `Memleaf.process()`, CLI `process`, MCP
4
- `process` and detached job worker to the already implemented incremental runner.
5
- It does not add another planner, model review, job queue or writer. Existing
6
- `process_incremental` and selected `remember_incremental` remain single-turn
7
- facades on that same runner, budget and commit journal.
3
+ As of the v0.2.67 candidate, `Memleaf.process()`, CLI `process`, MCP `process`
4
+ and the detached job worker all execute through the existing incremental runner.
5
+ There is no executable legacy processing engine and no runtime fallback to it.
6
+ `process_incremental` and selected `remember_incremental` remain facades on the
7
+ same runner, request budget and commit journal.
8
8
 
9
- The default is still `legacy`. This is an integration for isolated evaluation,
10
- not live Flash/Hermes acceptance, a release, or permission to change a production
11
- Vault. No existing config, memory, model route or version is automatically migrated.
12
- Text `remember()` has a separate, default-legacy `process.remember_pipeline`
13
- selection; see `remember-incremental-route.md`. Switching automatic processing
14
- alone does not enable the text route. Both use the same incremental runner.
9
+ New Vaults default to incremental processing. Existing Vaults that still contain a
10
+ `legacy` pipeline setting remain readable for migration inspection, but processing
11
+ fails closed with `legacy_pipeline_removed` until the operator explicitly updates
12
+ the configuration after the required stop/backup/preflight procedure.
15
13
 
16
- ## Select the route, not a second pipeline
14
+ ## One engine, compatibility selector only
17
15
 
18
- The optional configuration field is:
16
+ The configuration field is retained for migration visibility:
19
17
 
20
18
  ```yaml
21
19
  process:
22
- automatic_pipeline: legacy # legacy | incremental; default remains legacy
20
+ automatic_pipeline: incremental
23
21
  ```
24
22
 
25
- A missing field preserves legacy behavior. Unknown names, nonstrings and malformed
26
- configuration fail explicitly; they do not fall back to a different model pipeline.
27
- After operator-controlled preparation on an isolated copy, `incremental` may be
28
- selected in that copy's configuration. No command in this batch switches it for you.
23
+ A missing field resolves to incremental. `incremental` is the only executable
24
+ value. A retained `legacy` value is a migration blocker; unknown names, nonstrings
25
+ and malformed configuration fail explicitly. Nothing silently reinterprets an old
26
+ legacy queue or configuration as new incremental work.
29
27
 
30
28
  The optional per-call override affects that call only:
31
29
 
@@ -134,9 +132,11 @@ concurrent planners and stale responses.
134
132
 
135
133
  ## Queue binding and cutover
136
134
 
137
- An accepted job binds effective route, configured default, normalized scope and
138
- recover flag. A same-session request with different controls cannot silently
139
- coalesce into it. Legacy queue records missing these fields keep legacy semantics.
135
+ An accepted job binds the incremental engine, normalized scope and recover flag.
136
+ A same-session request with different controls cannot silently coalesce into it.
137
+ Historical queue records that are missing the incremental route, or explicitly
138
+ name `legacy`, are retained for diagnosis but blocked with
139
+ `legacy_pipeline_removed`; they are never executed by a compatibility engine.
140
140
  Malformed stored controls fail validation without resetting the queue.
141
141
 
142
142
  A waiting job refuses execution when the configured default has changed since its
@@ -185,8 +185,8 @@ model configuration absent. Queue errors do not become successful NO_MEMORY.
185
185
 
186
186
  ## Validation and outstanding work
187
187
 
188
- Public tests cover legacy defaults, route overrides, exact scope, source ordering,
189
- batch caps, redacted ID selection, CLI/MCP/queue controls, captured-route conflicts,
188
+ Public tests cover incremental defaults, explicit rejection of legacy execution,
189
+ exact scope, source ordering, batch caps, redacted ID selection, CLI/MCP/queue controls,
190
190
  old queue compatibility, safe summaries, native NO_CHANGE, index/cleanup failures,
191
191
  source/target edits, Forget, concurrent dispatch, and a real child process exiting
192
192
  after its saved response. All Vaults and model responses in tests are synthetic.
@@ -207,8 +207,8 @@ Native installed-artifact results must come from the exact candidate's CI
207
207
  reports, not from a phase document or a prior green build. A native package test
208
208
  with a host-interface stub is not actual host acceptance.
209
209
 
210
- The automatic and text-remember routes both remain default-legacy and are
211
- selected independently. `--dry-run` may still call the configured model;
210
+ Automatic processing and text remember both use the incremental engine by default.
211
+ `--dry-run` may still call the configured model;
212
212
  acceptance plan-only mode is the no-model preparation path. See
213
213
  [the acceptance evidence guide](acceptance-evidence-guide.md) for the remaining
214
214
  gates. The fixed 1,861-codepoint/3,587-byte main prompt is unchanged.
@@ -1,10 +1,11 @@
1
1
  # Migration preflight and private local backup (G4e)
2
2
 
3
- This increment implements the preparation portion of the controlled migration
4
- contract (FS178-179, FS188, FS192 and FS196). It neither switches a pipeline nor
5
- stops processes, rewrites business records, resets budgets or restores a Vault.
6
- A verified backup is not permission to activate the new routes. The fixed model
7
- prompt, request allowance, `legacy` defaults and package version are unchanged.
3
+ This command implements the preparation portion of the controlled migration
4
+ contract (FS178-179, FS188, FS192 and FS196). It does not stop processes, rewrite
5
+ business records, reset budgets, restore a Vault, or rewrite pipeline settings.
6
+ The v0.2.67 candidate itself has only the incremental engine; a retained
7
+ `legacy` setting is reported as `legacy_pipeline_configuration` and must be
8
+ handled explicitly after writers are stopped and a verified backup exists.
8
9
 
9
10
  ## Inspect before changing anything
10
11
 
@@ -1,10 +1,10 @@
1
- # Existing text remember route (G4b, opt-in)
1
+ # Text remember on the incremental engine
2
2
 
3
- The existing `Memleaf.remember()` and MCP `remember` tool can explicitly select
4
- incremental execution. This is an adapter to the same selected-retention runner,
5
- budget, native comparator, compiler and shared MemoryWriter; not another planner.
6
- The already implemented automatic route is independent. Neither route is enabled
7
- on an existing Vault by this change. Package version and fixed prompts are unchanged.
3
+ As of the v0.2.67 candidate, `Memleaf.remember()` and the MCP `remember` tool use
4
+ incremental execution as their only engine. This remains an adapter to the same
5
+ selected-retention runner, budget, native comparator, compiler and shared
6
+ MemoryWriter; it does not create a second planner. There is no executable legacy
7
+ remember path and no fallback to it.
8
8
 
9
9
  ## Selection and compatibility
10
10
 
@@ -26,13 +26,13 @@ if result["retry_available"]:
26
26
  )
27
27
  ```
28
28
 
29
- The optional `process.remember_pipeline` is `legacy` by default and accepts only
30
- `legacy` or `incremental`. The per-call `pipeline` overrides it. Selecting
31
- `process.automatic_pipeline=incremental` alone does not change text remember.
32
- Unknown values fail explicitly, not by falling back to another semantic pipeline.
33
- The MCP tool adds only the same optional `pipeline`, `recover` and `source_time`
34
- fields. There is no new MCP tool, CLI command, daemon or automatic live migration.
35
- A supplied model/router still must meet the existing single-dispatch contract.
29
+ The retained `process.remember_pipeline` field defaults to `incremental`.
30
+ `incremental` is the only executable value; an old `legacy` value is preserved
31
+ for migration inspection but is rejected with `legacy_pipeline_removed`. The
32
+ per-call `pipeline` argument is now only a compatibility selector for
33
+ `incremental`. Unknown values fail explicitly. The MCP tool exposes only
34
+ `incremental` plus the existing `recover` and `source_time` controls. A
35
+ supplied model/router still must meet the existing single-dispatch contract.
36
36
 
37
37
  `content` and its `text` alias contain the actual submitted user material. Supplying
38
38
  both with different values is rejected on the incremental route. One text request
@@ -145,12 +145,12 @@ consent or report a damaged state as NO_MEMORY.
145
145
 
146
146
  ## Cutover and validation
147
147
 
148
- Keep legacy defaults until controlled stop/backup/upgrade/restart and the planned
149
- live acceptance. Older code does not understand the standalone marker and cannot
150
- be expected to enforce its cleanup semantics. Changing a config key does not kill
151
- an old process or grant safe mixed-version writes. No default activation, production
152
- Vault mutation, release, real Flash calls or native Hermes/Windows/macOS acceptance
153
- was performed by this increment.
148
+ Production migration still requires controlled stop/backup/upgrade/restart and
149
+ live acceptance. Older binaries must be stopped before changing a retained legacy
150
+ configuration; changing a config key does not kill an old process or make
151
+ mixed-version writes safe. This candidate changes the package default to
152
+ incremental but does not itself mutate a production Vault, run a real model, or
153
+ prove native Hermes lifecycle acceptance.
154
154
 
155
155
  Deterministic tests cover one-user projection, origin provenance, same/new intents,
156
156
  legacy-route collisions, explicit and configured selection, unchanged originating
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memleaf"
7
- version = "0.2.66"
7
+ version = "0.2.68"
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.66"
3
+ __version__ = "0.2.68"
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
@@ -94,8 +94,8 @@ def build_parser() -> argparse.ArgumentParser:
94
94
  process.add_argument("--source", default=None)
95
95
  process.add_argument("--session-id", default=None)
96
96
  process.add_argument("--scope", default=None)
97
- process.add_argument("--pipeline", choices=("legacy", "incremental"), default=None,
98
- help="explicit automatic pipeline; otherwise use Vault configuration")
97
+ process.add_argument("--pipeline", choices=("incremental",), default=None,
98
+ help="compatibility override; incremental is the only processing engine")
99
99
  process.add_argument("--recover", action="store_true",
100
100
  help="allow remaining incremental transport recovery, not partial replan")
101
101
  process.add_argument("--dry-run", action="store_true", help="no source Vault writes; may call the configured model")
@@ -89,8 +89,8 @@ DEFAULT_CONFIG: dict[str, Any] = {
89
89
  "memory_compact_threshold_tokens": 100000,
90
90
  "memory_compact_candidate_ratio": 0.30,
91
91
  "inbox_cleanup_hours": 24,
92
- "automatic_pipeline": "legacy",
93
- "remember_pipeline": "legacy",
92
+ "automatic_pipeline": "incremental",
93
+ "remember_pipeline": "incremental",
94
94
  # Compatibility key: closed todo heads remain current identities.
95
95
  "closed_todo_retention_days": 30,
96
96
  "model_concurrency": DEFAULT_MODEL_CONCURRENCY,
@@ -225,10 +225,12 @@ def load_config(path: Path | str, *, vault: Path | str | None = None) -> dict[st
225
225
  cleanup_hours = process.get("inbox_cleanup_hours") if isinstance(process, Mapping) else None
226
226
  if type(cleanup_hours) is not int or cleanup_hours < 0:
227
227
  raise ValueError("invalid memleaf process.inbox_cleanup_hours")
228
- automatic_pipeline = process.get("automatic_pipeline", "legacy")
228
+ # Legacy values remain parseable only so old Vaults can be inspected and migrated.
229
+ # Runtime selection rejects them; new/default configurations are incremental-only.
230
+ automatic_pipeline = process.get("automatic_pipeline", "incremental")
229
231
  if not isinstance(automatic_pipeline, str) or automatic_pipeline not in {"legacy", "incremental"}:
230
232
  raise ValueError("invalid memleaf process.automatic_pipeline")
231
- remember_pipeline = process.get("remember_pipeline", "legacy")
233
+ remember_pipeline = process.get("remember_pipeline", "incremental")
232
234
  if not isinstance(remember_pipeline, str) or remember_pipeline not in {"legacy", "incremental"}:
233
235
  raise ValueError("invalid memleaf process.remember_pipeline")
234
236
  closed_todo_days = process.get("closed_todo_retention_days") if isinstance(process, Mapping) else None
@@ -296,11 +298,11 @@ def save_config(path: Path | str, config: Mapping[str, Any]) -> None:
296
298
  if not isinstance(process, Mapping):
297
299
  raise ValueError("invalid memleaf process settings")
298
300
  normalized_process = dict(process)
299
- pipeline = normalized_process.get("automatic_pipeline", "legacy")
301
+ pipeline = normalized_process.get("automatic_pipeline", "incremental")
300
302
  if not isinstance(pipeline, str) or pipeline not in {"legacy", "incremental"}:
301
303
  raise ValueError("invalid memleaf process.automatic_pipeline")
302
304
  normalized_process["automatic_pipeline"] = pipeline
303
- remember_pipeline = normalized_process.get("remember_pipeline", "legacy")
305
+ remember_pipeline = normalized_process.get("remember_pipeline", "incremental")
304
306
  if not isinstance(remember_pipeline, str) or remember_pipeline not in {"legacy", "incremental"}:
305
307
  raise ValueError("invalid memleaf process.remember_pipeline")
306
308
  normalized_process["remember_pipeline"] = remember_pipeline
@@ -19,6 +19,16 @@ except (ImportError, ValueError):
19
19
  _spec.loader.exec_module(_module)
20
20
  globals().update({name: getattr(_module, name) for name in getattr(_module, "__all__", ())})
21
21
 
22
+ def _capture_assistant_text(value: str) -> str:
23
+ """Remove Hermes' terminal internal cleanliness marker from memory input."""
24
+ import re
25
+
26
+ if not isinstance(value, str):
27
+ return value
28
+ return re.sub(r"(?:\r?\n)?\s*<!--\s*CLEAN\s*-->\s*$", "", value,
29
+ flags=re.IGNORECASE)
30
+
31
+
22
32
  def _sync_source_metadata(messages: Any, user_content: str, assistant_content: str) -> dict[str, dict[str, Any]]:
23
33
  """Read optional host metadata only from the exact visible tail pair.
24
34
 
@@ -1618,9 +1628,11 @@ class MemleafMemoryProvider(MemoryProvider):
1618
1628
  # Never capture the raw message list as business conversation content.
1619
1629
  if not self._write_enabled or self._client is None:
1620
1630
  return
1631
+ raw_assistant_content = assistant_content
1632
+ captured_assistant_content = _capture_assistant_text(assistant_content)
1621
1633
  visible_events = [
1622
1634
  ("user", user_content),
1623
- ("assistant", assistant_content),
1635
+ ("assistant", captured_assistant_content),
1624
1636
  ]
1625
1637
  if any(not isinstance(content, str) or not content.strip() for _, content in visible_events):
1626
1638
  return
@@ -1640,9 +1652,9 @@ class MemleafMemoryProvider(MemoryProvider):
1640
1652
  effective_session,
1641
1653
  resolved_turn_number,
1642
1654
  user_content,
1643
- assistant_content,
1655
+ captured_assistant_content,
1644
1656
  )
1645
- source_metadata = _sync_source_metadata(messages, user_content, assistant_content)
1657
+ source_metadata = _sync_source_metadata(messages, user_content, raw_assistant_content)
1646
1658
  user_source_id = source_metadata.get("user", {}).get("message_id")
1647
1659
  if user_source_id:
1648
1660
  # Stable host identity survives a revised body/final reply.
@@ -568,8 +568,26 @@ def _tool_call_parts(value: Any) -> Optional[dict[str, Any]]:
568
568
  wrapper = _decode_tool_value(arguments)
569
569
  if not isinstance(wrapper, Mapping):
570
570
  return None
571
- name = wrapper.get("name")
572
- arguments = wrapper.get("arguments")
571
+ # Hermes' deferred-tool bridge advertises a batch envelope even for one
572
+ # local tool: {"calls": [{"name": ..., "arguments": ...}]}. Local
573
+ # deferred tools are one entry per outer tool_call, so only unwrap a
574
+ # single-entry batch. Multi-entry wrapper results cannot be correlated
575
+ # safely to one inner call and therefore remain diagnostic-unknown.
576
+ raw_calls = _decode_tool_value(wrapper.get("calls")) if "calls" in wrapper else None
577
+ if raw_calls is not None:
578
+ if isinstance(raw_calls, Mapping):
579
+ raw_calls = [raw_calls]
580
+ if not isinstance(raw_calls, list) or len(raw_calls) != 1 or not isinstance(raw_calls[0], Mapping):
581
+ return None
582
+ inner = raw_calls[0]
583
+ name = inner.get("name")
584
+ arguments = inner.get("arguments")
585
+ inner_id = inner.get("id") or inner.get("call_id")
586
+ if isinstance(inner_id, str) and inner_id:
587
+ call_id = inner_id
588
+ else:
589
+ name = wrapper.get("name")
590
+ arguments = wrapper.get("arguments")
573
591
  if not isinstance(name, str):
574
592
  return None
575
593
  arguments = _decode_tool_value(arguments)
@@ -1,5 +1,5 @@
1
1
  name: memleaf
2
- version: 0.2.65
2
+ version: 0.2.68
3
3
  description: "Hermes-native external MemoryProvider for local-first Markdown memory shared with the memleaf MCP server."
4
4
  hooks:
5
5
  - prefetch
@@ -36,6 +36,30 @@ _BRANCHES = {
36
36
  "NO_MEMORY": (set(), set()),
37
37
  "DEFERRED": ({"reason", "need"}, {"reason", "need"}),
38
38
  }
39
+
40
+ _CREATE_ROW_META = frozenset(("action", "evidence", "at", "effective"))
41
+
42
+
43
+ def _normalize_flat_create_row(row: Any) -> tuple[Any, bool]:
44
+ """Repair one unambiguous model-only CREATE wrapper mistake.
45
+
46
+ This is structural normalization only: it never invents fields, changes
47
+ values, widens allowed keys, or bypasses the strict CREATE validator.
48
+ """
49
+ if not isinstance(row, dict) or "memory" in row:
50
+ return row, False
51
+ action = row.get("action")
52
+ if not isinstance(action, str) or action.strip().upper() != "CREATE":
53
+ return row, False
54
+ memory_keys = set(row) & set(_CREATE)
55
+ required = {"type", "scope", "title", "body"}
56
+ if not required <= memory_keys:
57
+ return row, False
58
+ if set(row) - (_CREATE_ROW_META | set(_CREATE)):
59
+ return row, False
60
+ normalized = {key: deepcopy(value) for key, value in row.items() if key not in _CREATE}
61
+ normalized["memory"] = {key: deepcopy(row[key]) for key in row if key in _CREATE}
62
+ return normalized, True
39
63
  _GROUP = {"status": "status", "validity": "validity", "scopes": "scope",
40
64
  "assignee": "responsibility", "waiting_on": "responsibility",
41
65
  "deadline": "deadline", "title": "content", "body": "content"}
@@ -553,7 +577,11 @@ def compile_incremental(raw: str, snapshot: PlanningSnapshot) -> dict[str, Any]:
553
577
  issues.append({"row": index, "code": code, "evidence": list(dict.fromkeys(refs))})
554
578
  for index, row in enumerate(value["items"]):
555
579
  try:
556
- parsed.append((index, _parse_row(row, state)))
580
+ normalized_row, normalized = _normalize_flat_create_row(row)
581
+ parsed_row = _parse_row(normalized_row, state)
582
+ if normalized:
583
+ parsed_row.setdefault("warnings", []).append("create_memory_wrapper_normalized")
584
+ parsed.append((index, parsed_row))
557
585
  except (ValueError, TypeError) as error:
558
586
  problem(index, str(error) if isinstance(error, ValueError) else "invalid_fields", row)
559
587
  if isinstance(row, dict) and isinstance(row.get("target"), str) and row["target"].strip() in state["targets"]:
@@ -902,6 +902,44 @@ _MODEL_ROUTE_INCOMPATIBLE_ACTION = (
902
902
  )
903
903
 
904
904
 
905
+ _PIPELINE_MIGRATION_REQUIRED_ACTION = (
906
+ "This Vault still selects the removed legacy processing engine. Stop all supported "
907
+ "writers, run migration-check, create and verify a migration backup, then set both "
908
+ "process.automatic_pipeline and process.remember_pipeline to incremental before "
909
+ "relying on automatic extraction or remember."
910
+ )
911
+
912
+ _PIPELINE_CONFIGURATION_INVALID_ACTION = (
913
+ "Repair the Vault process configuration before relying on memory processing. "
914
+ "v0.2.67 supports only the incremental execution engine."
915
+ )
916
+
917
+
918
+ def _pipeline_route_outcome(vault: Any) -> tuple[bool, str, str | None]:
919
+ """Report whether this Vault can execute the sole incremental engine."""
920
+ try:
921
+ config = vault.config()
922
+ except Exception:
923
+ return False, "pipeline_configuration_invalid", _PIPELINE_CONFIGURATION_INVALID_ACTION
924
+ process = config.get("process") if isinstance(config, Mapping) else None
925
+ if not isinstance(process, Mapping):
926
+ return False, "pipeline_configuration_invalid", _PIPELINE_CONFIGURATION_INVALID_ACTION
927
+ values = (
928
+ process.get("automatic_pipeline", "incremental"),
929
+ process.get("remember_pipeline", "incremental"),
930
+ )
931
+ if any(value == "legacy" for value in values):
932
+ return False, "pipeline_migration_required", _PIPELINE_MIGRATION_REQUIRED_ACTION
933
+ if any(value != "incremental" for value in values):
934
+ return False, "pipeline_configuration_invalid", _PIPELINE_CONFIGURATION_INVALID_ACTION
935
+ return True, "ready", None
936
+
937
+
938
+ def _join_user_actions(*actions: str | None) -> str | None:
939
+ values = [action.strip() for action in actions if isinstance(action, str) and action.strip()]
940
+ return " ".join(values) if values else None
941
+
942
+
905
943
  def _route_can_extract(vault: Any) -> bool:
906
944
  """Ask the production router whether this Vault's route can speak B3.
907
945
 
@@ -965,6 +1003,22 @@ def install_hermes(
965
1003
  if mcp_runtime not in _MCP_RUNTIME_POLICIES:
966
1004
  raise ValueError(f"unsupported Hermes MCP runtime policy: {mcp_runtime}")
967
1005
 
1006
+ # Reject an internally inconsistent package before reading or changing any
1007
+ # Hermes/Vault state. The post-copy check below remains as defense in depth.
1008
+ packaged_provider = Path(__file__).with_name("hermes_provider")
1009
+ packaged_provider_version = _provider_manifest_version(packaged_provider)
1010
+ if packaged_provider_version != core_version:
1011
+ return _failure_result(
1012
+ stage="provider_version",
1013
+ reason=(
1014
+ "packaged Hermes provider version mismatch: "
1015
+ f"core={core_version}, provider={packaged_provider_version or 'unknown'}"
1016
+ ),
1017
+ core_version=core_version,
1018
+ provider_version=packaged_provider_version,
1019
+ vault=None,
1020
+ )
1021
+
968
1022
  home = _home_from_environment()
969
1023
  hermes_home = _hermes_home(home)
970
1024
  try:
@@ -1082,11 +1136,16 @@ def install_hermes(
1082
1136
  # here used to leave core and provider versions out of step. A *present*
1083
1137
  # route that cannot speak B3 is reported separately, because advertising
1084
1138
  # ``ready`` for it would promise extraction that can never run.
1085
- model_ready, processing_status, model_action = _model_route_outcome(
1139
+ model_ready, model_processing_status, model_action = _model_route_outcome(
1086
1140
  model, can_extract=_route_can_extract(vault)
1087
1141
  )
1088
- # ``model_ready`` is reported through ``processing_status`` below; the host
1089
- # integration itself is configured either way.
1142
+ pipeline_ready, pipeline_processing_status, pipeline_action = _pipeline_route_outcome(vault)
1143
+ processing_status = (
1144
+ model_processing_status if pipeline_ready else pipeline_processing_status
1145
+ )
1146
+ install_action = _join_user_actions(pipeline_action, model_action)
1147
+ # Host integration is configured even when processing still needs an explicit
1148
+ # model route or a controlled legacy->incremental Vault migration.
1090
1149
 
1091
1150
  provider_target = hermes_home / "plugins" / "memleaf"
1092
1151
  provider_config = hermes_home / "memleaf.json"
@@ -1298,8 +1357,8 @@ def install_hermes(
1298
1357
  "native_sources": native_registration,
1299
1358
  "capture": capture_policy_status(vault.config()),
1300
1359
  "processing_status": processing_status,
1301
- "user_action_required": bool(model_action),
1302
- "user_action": model_action,
1360
+ "user_action_required": bool(install_action),
1361
+ "user_action": install_action,
1303
1362
  }
1304
1363
 
1305
1364
 
@@ -1357,9 +1416,12 @@ def install_codex(*, vault_path: Path | None = None) -> dict[str, Any]:
1357
1416
  "codex": configured.to_dict(),
1358
1417
  }
1359
1418
  model_ready = model.get("status") in {"configured", "already_configured"}
1419
+ pipeline_ready, pipeline_processing_status, pipeline_action = _pipeline_route_outcome(vault)
1360
1420
  actions: list[str] = []
1361
1421
  if configured.user_action_required and configured.user_action:
1362
1422
  actions.append(configured.user_action)
1423
+ if pipeline_action:
1424
+ actions.append(pipeline_action)
1363
1425
  if not model_ready:
1364
1426
  actions.append(
1365
1427
  "Configure an independent memleaf Model Route for this Vault before relying on "
@@ -1372,7 +1434,10 @@ def install_codex(*, vault_path: Path | None = None) -> dict[str, Any]:
1372
1434
  "vault": str(vault.root),
1373
1435
  "vault_source": vault_source,
1374
1436
  "model": model,
1375
- "processing_status": "ready" if model_ready else "model_route_required",
1437
+ "processing_status": (
1438
+ ("ready" if model_ready else "model_route_required")
1439
+ if pipeline_ready else pipeline_processing_status
1440
+ ),
1376
1441
  "codex": configured.to_dict(),
1377
1442
  "user_action_required": bool(actions),
1378
1443
  "user_action": " ".join(actions) if actions else None,
@@ -303,7 +303,7 @@ _TOOLS: tuple[dict[str, Any], ...] = (
303
303
  "session_id": {"type": "string"},
304
304
  "scope": _text_or_texts_schema(),
305
305
  "background": {"type": "boolean"},
306
- "pipeline": {"type": "string", "enum": ["legacy", "incremental"]},
306
+ "pipeline": {"type": "string", "enum": ["incremental"]},
307
307
  "recover": {"type": "boolean"},
308
308
  }
309
309
  ),
@@ -332,7 +332,7 @@ _TOOLS: tuple[dict[str, Any], ...] = (
332
332
  "event_id": {"type": "string"},
333
333
  "intent_id": {"type": "string"},
334
334
  "scopes": _text_or_texts_schema(),
335
- "pipeline": {"type": "string", "enum": ["legacy", "incremental"]},
335
+ "pipeline": {"type": "string", "enum": ["incremental"]},
336
336
  "recover": {"type": "boolean"},
337
337
  "source_time": {"type": "string"},
338
338
  },
@@ -12,7 +12,7 @@ import os
12
12
  from pathlib import Path
13
13
  import platform
14
14
  import re
15
- from typing import Any
15
+ from typing import Any, Mapping
16
16
 
17
17
  from . import __version__
18
18
  from .inspection import _checked_snapshot, _snapshot, _fingerprint, existing_root
@@ -94,6 +94,7 @@ def _checks(service: Any, snapshot: dict[str, bytes]) -> dict:
94
94
  warnings: set[str] = set()
95
95
  counts: dict[str, int] = {}
96
96
  config = None
97
+ configured: dict[str, Any] = {}
97
98
  binding = {"status": "unknown"}
98
99
  try:
99
100
  binding = service.vault.identity_status()
@@ -106,6 +107,14 @@ def _checks(service: Any, snapshot: dict[str, bytes]) -> dict:
106
107
  except (OSError, ValueError, UnicodeError, TypeError):
107
108
  blockers.add("invalid_configuration")
108
109
  if config is not None:
110
+ process_config = config.get("process", {}) if isinstance(config, Mapping) else {}
111
+ configured = {
112
+ key: process_config.get(key, "incremental") if isinstance(process_config, Mapping) else None
113
+ for key in ("automatic_pipeline", "remember_pipeline")
114
+ }
115
+ counts["legacy_pipeline_settings"] = sum(value == "legacy" for value in configured.values())
116
+ if counts["legacy_pipeline_settings"]:
117
+ blockers.add("legacy_pipeline_configuration")
109
118
  native_unavailable, native_blockers = _native_source_readiness(service, config)
110
119
  counts["unavailable_native_sources"] = native_unavailable
111
120
  blockers.update(native_blockers)
@@ -187,10 +196,8 @@ def _checks(service: Any, snapshot: dict[str, bytes]) -> dict:
187
196
  elif progress["status"] != "current":
188
197
  blockers.add("unsettled_sources_require_review")
189
198
  warnings.update(_EXTERNAL_CHECKS)
190
- # Even a disabled stage does not prove that an older installed binary will
191
- # never run. Report facts; never automatically flip either route.
192
- configured = {k: config["process"].get(k, "legacy") for k in
193
- ("automatic_pipeline", "remember_pipeline")} if config else {}
199
+ # Historical route settings are reported as migration facts. They are not
200
+ # executable by this release and are never silently rewritten here.
194
201
  return {"local_status": "blocked" if blockers else "clear",
195
202
  "blockers": sorted(blockers),
196
203
  "backup_blockers": sorted(blockers & {"processing_owner_live", "queue_not_quiescent"}),