memleaf 0.2.67__tar.gz → 0.2.69__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 (239) hide show
  1. {memleaf-0.2.67 → memleaf-0.2.69}/CHANGELOG.md +25 -0
  2. {memleaf-0.2.67/src/memleaf.egg-info → memleaf-0.2.69}/PKG-INFO +2 -2
  3. {memleaf-0.2.67 → memleaf-0.2.69}/README.en.md +1 -1
  4. {memleaf-0.2.67 → memleaf-0.2.69}/README.md +1 -1
  5. {memleaf-0.2.67 → memleaf-0.2.69}/pyproject.toml +1 -1
  6. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/__init__.py +1 -1
  7. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/hermes_provider/_provider.py +15 -3
  8. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/hermes_provider/_shared.py +20 -2
  9. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/hermes_provider/plugin.yaml +1 -1
  10. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/incremental_protocol.py +110 -12
  11. {memleaf-0.2.67 → memleaf-0.2.69/src/memleaf.egg-info}/PKG-INFO +2 -2
  12. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_hermes_source_metadata.py +84 -0
  13. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_incremental_protocol.py +75 -0
  14. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_provider_compatibility.py +1 -1
  15. {memleaf-0.2.67 → memleaf-0.2.69}/LICENSE +0 -0
  16. {memleaf-0.2.67 → memleaf-0.2.69}/MANIFEST.in +0 -0
  17. {memleaf-0.2.67 → memleaf-0.2.69}/docs/acceptance-evidence-guide.md +0 -0
  18. {memleaf-0.2.67 → memleaf-0.2.69}/docs/automatic-processing-route.md +0 -0
  19. {memleaf-0.2.67 → memleaf-0.2.69}/docs/body-compaction.md +0 -0
  20. {memleaf-0.2.67 → memleaf-0.2.69}/docs/candidate-reconciliation.md +0 -0
  21. {memleaf-0.2.67 → memleaf-0.2.69}/docs/capture-budget-design.md +0 -0
  22. {memleaf-0.2.67 → memleaf-0.2.69}/docs/config-migrations.md +0 -0
  23. {memleaf-0.2.67 → memleaf-0.2.69}/docs/core-refactor.md +0 -0
  24. {memleaf-0.2.67 → memleaf-0.2.69}/docs/development-closeout.md +0 -0
  25. {memleaf-0.2.67 → memleaf-0.2.69}/docs/evidence-retention.md +0 -0
  26. {memleaf-0.2.67 → memleaf-0.2.69}/docs/explicit-memory-update.md +0 -0
  27. {memleaf-0.2.67 → memleaf-0.2.69}/docs/extraction-latency.md +0 -0
  28. {memleaf-0.2.67 → memleaf-0.2.69}/docs/gate-evidence-boundary.md +0 -0
  29. {memleaf-0.2.67 → memleaf-0.2.69}/docs/general-processing.md +0 -0
  30. {memleaf-0.2.67 → memleaf-0.2.69}/docs/hermes-mcp-runtime.md +0 -0
  31. {memleaf-0.2.67 → memleaf-0.2.69}/docs/hermes-provider-compatibility.md +0 -0
  32. {memleaf-0.2.67 → memleaf-0.2.69}/docs/host-acceptance-runbook.md +0 -0
  33. {memleaf-0.2.67 → memleaf-0.2.69}/docs/immutable-release-assets.md +0 -0
  34. {memleaf-0.2.67 → memleaf-0.2.69}/docs/incremental-commit-contract.md +0 -0
  35. {memleaf-0.2.67 → memleaf-0.2.69}/docs/incremental-execution.md +0 -0
  36. {memleaf-0.2.67 → memleaf-0.2.69}/docs/incremental-native-comparison.md +0 -0
  37. {memleaf-0.2.67 → memleaf-0.2.69}/docs/incremental-partial-recovery.md +0 -0
  38. {memleaf-0.2.67 → memleaf-0.2.69}/docs/incremental-planner-preview.md +0 -0
  39. {memleaf-0.2.67 → memleaf-0.2.69}/docs/incremental-runtime-contract.md +0 -0
  40. {memleaf-0.2.67 → memleaf-0.2.69}/docs/incremental-scope-registration.md +0 -0
  41. {memleaf-0.2.67 → memleaf-0.2.69}/docs/incremental-selected-retention.md +0 -0
  42. {memleaf-0.2.67 → memleaf-0.2.69}/docs/installed-artifact-verification.md +0 -0
  43. {memleaf-0.2.67 → memleaf-0.2.69}/docs/memory-state-contract.md +0 -0
  44. {memleaf-0.2.67 → memleaf-0.2.69}/docs/migration-preflight.md +0 -0
  45. {memleaf-0.2.67 → memleaf-0.2.69}/docs/processing-quality-acceptance.md +0 -0
  46. {memleaf-0.2.67 → memleaf-0.2.69}/docs/public-query-integrity.md +0 -0
  47. {memleaf-0.2.67 → memleaf-0.2.69}/docs/query-performance.md +0 -0
  48. {memleaf-0.2.67 → memleaf-0.2.69}/docs/remember-incremental-route.md +0 -0
  49. {memleaf-0.2.67 → memleaf-0.2.69}/docs/runtime-state-retention.md +0 -0
  50. {memleaf-0.2.67 → memleaf-0.2.69}/docs/semantic-extraction-protocol.md +0 -0
  51. {memleaf-0.2.67 → memleaf-0.2.69}/docs/source-work-contract.md +0 -0
  52. {memleaf-0.2.67 → memleaf-0.2.69}/docs/v0.2.26-processing-status.md +0 -0
  53. {memleaf-0.2.67 → memleaf-0.2.69}/examples/README.md +0 -0
  54. {memleaf-0.2.67 → memleaf-0.2.69}/examples/basic_usage.py +0 -0
  55. {memleaf-0.2.67 → memleaf-0.2.69}/examples/incremental_acceptance.json +0 -0
  56. {memleaf-0.2.67 → memleaf-0.2.69}/examples/mcp_stdio.ndjson +0 -0
  57. {memleaf-0.2.67 → memleaf-0.2.69}/examples/query_benchmark.py +0 -0
  58. {memleaf-0.2.67 → memleaf-0.2.69}/install.ps1 +0 -0
  59. {memleaf-0.2.67 → memleaf-0.2.69}/install.sh +0 -0
  60. {memleaf-0.2.67 → memleaf-0.2.69}/setup.cfg +0 -0
  61. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/__main__.py +0 -0
  62. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/acceptance.py +0 -0
  63. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/adapters/__init__.py +0 -0
  64. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/adapters/antigravity.py +0 -0
  65. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/adapters/base.py +0 -0
  66. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/adapters/codex.py +0 -0
  67. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/adapters/hermes.py +0 -0
  68. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/admission.py +0 -0
  69. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/batch_review.py +0 -0
  70. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/budget.py +0 -0
  71. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/capture.py +0 -0
  72. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/cli.py +0 -0
  73. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/compaction.py +0 -0
  74. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/config.py +0 -0
  75. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/create_coordinator.py +0 -0
  76. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/credentials.py +0 -0
  77. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/evidence_budget.py +0 -0
  78. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/evidence_policy.py +0 -0
  79. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/evidence_structure.py +0 -0
  80. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/evidence_syntax.py +0 -0
  81. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/explicit_text_source.py +0 -0
  82. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/extraction_budget.py +0 -0
  83. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/extraction_capability.py +0 -0
  84. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/extraction_work_state.py +0 -0
  85. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/frontmatter.py +0 -0
  86. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/hermes_provider/README.md +0 -0
  87. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/hermes_provider/__init__.py +0 -0
  88. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/hermes_provider/_mcp_client.py +0 -0
  89. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/hermes_provider/evidence_budget.py +0 -0
  90. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/hermes_provider/provider_compatibility.py +0 -0
  91. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/hermes_runtime.py +0 -0
  92. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/host_events.py +0 -0
  93. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/host_runtime.py +0 -0
  94. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/inbox.py +0 -0
  95. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/incremental_commit.py +0 -0
  96. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/incremental_dates.py +0 -0
  97. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/incremental_execution.py +0 -0
  98. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/incremental_journal.py +0 -0
  99. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/incremental_native.py +0 -0
  100. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/incremental_partial.py +0 -0
  101. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/incremental_preview.py +0 -0
  102. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/incremental_prompts.py +0 -0
  103. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/incremental_recovery.py +0 -0
  104. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/incremental_run_state.py +0 -0
  105. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/incremental_runtime.py +0 -0
  106. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/incremental_scopes.py +0 -0
  107. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/incremental_selection.py +0 -0
  108. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/index.py +0 -0
  109. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/inspection.py +0 -0
  110. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/installer.py +0 -0
  111. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/legacy_update_journal.py +0 -0
  112. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/llm/__init__.py +0 -0
  113. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/llm/base.py +0 -0
  114. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/llm/claude_compatible.py +0 -0
  115. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/llm/gemini.py +0 -0
  116. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/llm/openai_compatible.py +0 -0
  117. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/llm/router.py +0 -0
  118. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/llm/thinking.py +0 -0
  119. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/locking.py +0 -0
  120. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/mcp_server.py +0 -0
  121. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/memory_commit.py +0 -0
  122. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/memory_planner.py +0 -0
  123. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/memory_retraction.py +0 -0
  124. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/memory_update.py +0 -0
  125. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/memory_writer.py +0 -0
  126. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/migration.py +0 -0
  127. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/model_capabilities.py +0 -0
  128. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/model_discovery.py +0 -0
  129. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/model_execution.py +0 -0
  130. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/models.py +0 -0
  131. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/native_index.py +0 -0
  132. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/native_registration.py +0 -0
  133. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/parallel_model.py +0 -0
  134. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/planning_context.py +0 -0
  135. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/process_common.py +0 -0
  136. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/process_jobs.py +0 -0
  137. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/process_journal.py +0 -0
  138. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/process_owner.py +0 -0
  139. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/processing.py +0 -0
  140. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/processing_route.py +0 -0
  141. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/prompts.py +0 -0
  142. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/provenance.py +0 -0
  143. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/provider_compatibility.py +0 -0
  144. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/query_clock.py +0 -0
  145. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/query_progress.py +0 -0
  146. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/query_scan.py +0 -0
  147. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/receipt_codec.py +0 -0
  148. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/recording_policy.py +0 -0
  149. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/redaction.py +0 -0
  150. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/remember_route.py +0 -0
  151. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/retention.py +0 -0
  152. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/retrieval.py +0 -0
  153. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/retrieval_gate.py +0 -0
  154. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/runtime_retention.py +0 -0
  155. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/scope_maintenance.py +0 -0
  156. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/scope_state.py +0 -0
  157. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/semantic_maintenance.py +0 -0
  158. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/semantic_protocol.py +0 -0
  159. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/service.py +0 -0
  160. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/single_pass_memory_planner.py +0 -0
  161. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/single_pass_plan.py +0 -0
  162. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/source_policy.py +0 -0
  163. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/state_layout.py +0 -0
  164. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/subprocess_flags.py +0 -0
  165. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/summary_batch.py +0 -0
  166. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/target_reconciliation.py +0 -0
  167. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/turn_audit.py +0 -0
  168. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/turn_plan.py +0 -0
  169. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/update_coordinator.py +0 -0
  170. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/update_review.py +0 -0
  171. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/validation.py +0 -0
  172. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf/vault.py +0 -0
  173. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf.egg-info/SOURCES.txt +0 -0
  174. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf.egg-info/dependency_links.txt +0 -0
  175. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf.egg-info/entry_points.txt +0 -0
  176. {memleaf-0.2.67 → memleaf-0.2.69}/src/memleaf.egg-info/top_level.txt +0 -0
  177. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/acceptance_oracles.py +0 -0
  178. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/candidate_update_fixtures.py +0 -0
  179. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/incremental_test_support.py +0 -0
  180. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/platform_smoke.py +0 -0
  181. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/release_assets.py +0 -0
  182. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/run_contracts.py +0 -0
  183. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_a083_regressions.py +0 -0
  184. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_a47_regressions.py +0 -0
  185. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_acceptance.py +0 -0
  186. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_acceptance_boundaries.py +0 -0
  187. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_artifact_verification.py +0 -0
  188. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_body_compaction.py +0 -0
  189. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_candidate_reconciliation.py +0 -0
  190. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_contract_reporter.py +0 -0
  191. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_control_absence_contract.py +0 -0
  192. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_create_memory_contract.py +0 -0
  193. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_development_closeout.py +0 -0
  194. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_explicit_memory_update.py +0 -0
  195. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_explicit_update_contract.py +0 -0
  196. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_g1_memory_contract.py +0 -0
  197. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_g2_source_contract.py +0 -0
  198. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_hermes_native_provision.py +0 -0
  199. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_incremental_commit.py +0 -0
  200. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_incremental_configured_runtime.py +0 -0
  201. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_incremental_dates.py +0 -0
  202. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_incremental_execution.py +0 -0
  203. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_incremental_execution_recovery.py +0 -0
  204. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_incremental_native.py +0 -0
  205. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_incremental_partial.py +0 -0
  206. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_incremental_partial_boundaries.py +0 -0
  207. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_incremental_partial_recovery.py +0 -0
  208. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_incremental_preview.py +0 -0
  209. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_incremental_recovery.py +0 -0
  210. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_incremental_remember.py +0 -0
  211. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_incremental_remember_recovery.py +0 -0
  212. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_incremental_scope_recovery.py +0 -0
  213. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_incremental_scopes.py +0 -0
  214. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_inspection_read_budget.py +0 -0
  215. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_mcp_retrieval_fallback.py +0 -0
  216. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_migration_backup.py +0 -0
  217. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_migration_runtime.py +0 -0
  218. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_native_read_stability.py +0 -0
  219. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_planner_provenance_contract.py +0 -0
  220. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_platform_contracts.py +0 -0
  221. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_processed_state_contract.py +0 -0
  222. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_processing_entrypoints.py +0 -0
  223. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_processing_route.py +0 -0
  224. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_processing_route_recovery.py +0 -0
  225. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_public_query_integrity.py +0 -0
  226. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_query_benchmark.py +0 -0
  227. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_query_clock.py +0 -0
  228. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_query_progress.py +0 -0
  229. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_query_scan_reuse.py +0 -0
  230. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_release_assets.py +0 -0
  231. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_remember_route.py +0 -0
  232. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_remember_route_recovery.py +0 -0
  233. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_revision_target_integrity.py +0 -0
  234. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_runtime_retention.py +0 -0
  235. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_runtime_retention_observation.py +0 -0
  236. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_runtime_retention_recovery.py +0 -0
  237. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_source_followup_contract.py +0 -0
  238. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/test_vault_binding_contract.py +0 -0
  239. {memleaf-0.2.67 → memleaf-0.2.69}/tests_public/verify_distribution.py +0 -0
@@ -4,6 +4,31 @@ All notable changes to memleaf are documented here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.2.69 — 2026-09-21
8
+
9
+ ### Incremental protocol compatibility hardening
10
+
11
+ - Advance the incremental semantic protocol to `incremental-turn-v5` and expose precise field-shape details for fail-closed rows.
12
+ - Normalize only unambiguous model serialization variants: null placeholders from other branches, a null CREATE wrapper, a single-item `scopes` alias, default `validity`, and flattened CREATE fields. Ambiguous aliases and unknown fields remain rejected.
13
+ - Keep the new normalization behavior covered by the public protocol and provider-compatibility tests.
14
+
15
+ ## 0.2.68 — 2026-09-20
16
+
17
+ ### Hermes live-path hardening
18
+
19
+ - Observe Hermes deferred `tool_call` single-entry `calls[]` wrappers so real
20
+ memleaf search results are recorded as `NO_MATCH`/found instead of
21
+ `SEARCH_UNKNOWN`.
22
+ - Deterministically normalize only unambiguous flattened CREATE rows back into
23
+ the required `memory` object before the unchanged strict incremental validator;
24
+ malformed, incomplete and unknown-field variants remain fail-closed.
25
+ - Strip the trailing Hermes `<!--CLEAN-->` control marker at the Provider capture
26
+ boundary so it does not enter Inbox or memory extraction input.
27
+ - Validate the fixes with a real Hermes + DeepSeek Flash turn: retrieval audit
28
+ `NO_MATCH`, completed incremental extraction, three settled CREATE operations,
29
+ zero unresolved evidence and no CLEAN marker in Inbox.
30
+
31
+
7
32
  ## 0.2.67 — 2026-09-20
8
33
 
9
34
  ### Incremental-only processing and release integrity
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memleaf
3
- Version: 0.2.67
3
+ Version: 0.2.69
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.65。**
26
+ > **版本:0.2.69。**
27
27
  > 自动提炼和复核共用同一条“未来记忆价值”标准:模型综合未来复用、信息增量、再次读取时的直接可用性和忘记成本,只保留对未来理解、判断或行动有实质影响的最小核心;没有明确价值的信息不提炼,不按具体业务场景硬编码排除。
28
28
  > 提炼契约现在只有一份中文语义契约(旧版英文 B3 提示词已删除),首轮、主题选择、候选修复和 `semantic_maintenance` 使用同一套保留价值判断;同一件事只写一次。
29
29
  > Core 不再因为归属、日期、任务依据或跨项目而丢弃整条候选:归属缺失会归一到证据里唯一被点名的项目、否则落 `global`;日期只接受能在本候选证据里找到出处、且能锚定成 ISO 的写法(`明天`、`周五`、`月底` 都会被解析),无法解析时只丢掉日期字段,记忆照常写入;未获授权的改归属沿用目标原归属。
@@ -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.65.**
7
+ > **Version: 0.2.69.**
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
  > A single Chinese semantic contract is sent for extraction, topic selection, candidate repair, and `semantic_maintenance`; the older English B3 prompt has been deleted and B3 remains only as a compatibility read format. One topic is written once.
10
10
  > Core no longer discards a whole candidate over ownership, dates, task basis or cross-project wording. A missing owner normalizes to the single project named by that candidate's own evidence, otherwise `global`. A deadline is accepted only when it appears in the candidate's own evidence and can be anchored to ISO (`明天`, `周五`, `月底` all resolve); when it cannot, only the date field is dropped and the memory is still written. An unauthorized ownership change keeps the target's existing scope.
@@ -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.65。**
7
+ > **版本:0.2.69。**
8
8
  > 自动提炼和复核共用同一条“未来记忆价值”标准:模型综合未来复用、信息增量、再次读取时的直接可用性和忘记成本,只保留对未来理解、判断或行动有实质影响的最小核心;没有明确价值的信息不提炼,不按具体业务场景硬编码排除。
9
9
  > 提炼契约现在只有一份中文语义契约(旧版英文 B3 提示词已删除),首轮、主题选择、候选修复和 `semantic_maintenance` 使用同一套保留价值判断;同一件事只写一次。
10
10
  > Core 不再因为归属、日期、任务依据或跨项目而丢弃整条候选:归属缺失会归一到证据里唯一被点名的项目、否则落 `global`;日期只接受能在本候选证据里找到出处、且能锚定成 ISO 的写法(`明天`、`周五`、`月底` 都会被解析),无法解析时只丢掉日期字段,记忆照常写入;未获授权的改归属沿用目标原归属。
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memleaf"
7
- version = "0.2.67"
7
+ version = "0.2.69"
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.67"
3
+ __version__ = "0.2.69"
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
@@ -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.67
2
+ version: 0.2.69
3
3
  description: "Hermes-native external MemoryProvider for local-first Markdown memory shared with the memleaf MCP server."
4
4
  hooks:
5
5
  - prefetch
@@ -22,7 +22,7 @@ from .turn_plan import revision_digest
22
22
  from .validation import ModelOutputError, parse_strict_json
23
23
 
24
24
  PROTOCOL_VERSION = "incremental-items-v1"
25
- SEMANTIC_PROTOCOL = "incremental-turn-v4"
25
+ SEMANTIC_PROTOCOL = "incremental-turn-v5"
26
26
  MAX_BYTES = 128 * 1024
27
27
  MAX_ITEMS = 64
28
28
  _TYPES = frozenset(("fact", "todo", "preference", "project", "event", "identity", "other"))
@@ -36,6 +36,86 @@ _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
+ _ROW_FIELDS = frozenset(
42
+ {"action", "evidence"}.union(*(required | optional for required, optional in _BRANCHES.values()))
43
+ )
44
+
45
+
46
+ class _FieldShapeError(ValueError):
47
+ def __init__(self, detail: str):
48
+ super().__init__("invalid_fields")
49
+ self.detail = detail
50
+
51
+
52
+ def _normalize_create_fields(fields: Any) -> tuple[Any, list[str]]:
53
+ """Normalize model serialization aliases that preserve one exact meaning."""
54
+ if not isinstance(fields, dict):
55
+ return fields, []
56
+ normalized = deepcopy(fields)
57
+ warnings = []
58
+ if "scope" not in normalized and isinstance(normalized.get("scopes"), list):
59
+ scopes = normalized["scopes"]
60
+ if len(scopes) == 1 and isinstance(scopes[0], str):
61
+ normalized["scope"] = scopes[0]
62
+ normalized.pop("scopes")
63
+ warnings.append("create_single_scope_alias_normalized")
64
+ if normalized.get("validity") == "valid":
65
+ normalized.pop("validity")
66
+ warnings.append("create_default_validity_normalized")
67
+ return normalized, warnings
68
+
69
+
70
+ def _normalize_model_row(row: Any) -> tuple[Any, list[str]]:
71
+ """Remove only semantically empty branch placeholders and normalize CREATE aliases."""
72
+ if not isinstance(row, dict) or not isinstance(row.get("action"), str):
73
+ return row, []
74
+ action = row["action"].strip().upper()
75
+ if action not in _BRANCHES:
76
+ return row, []
77
+ normalized = deepcopy(row)
78
+ warnings = []
79
+ required, optional = _BRANCHES[action]
80
+ allowed = {"action", "evidence"} | required | optional
81
+ for key in sorted(_ROW_FIELDS - allowed):
82
+ if key in normalized and normalized[key] is None:
83
+ normalized.pop(key)
84
+ warnings.append("null_branch_placeholder_normalized")
85
+ if action == "CREATE":
86
+ if "memory" in normalized and normalized["memory"] is None:
87
+ normalized.pop("memory")
88
+ warnings.append("null_memory_wrapper_normalized")
89
+ if "memory" in normalized:
90
+ normalized["memory"], field_warnings = _normalize_create_fields(normalized["memory"])
91
+ warnings.extend(field_warnings)
92
+ else:
93
+ normalized, field_warnings = _normalize_create_fields(normalized)
94
+ warnings.extend(field_warnings)
95
+ normalized, flattened = _normalize_flat_create_row(normalized)
96
+ if flattened:
97
+ warnings.append("create_memory_wrapper_normalized")
98
+ return normalized, list(dict.fromkeys(warnings))
99
+
100
+
101
+ def _normalize_flat_create_row(row: Any) -> tuple[Any, bool]:
102
+ """Repair one unambiguous model-only CREATE wrapper mistake.
103
+
104
+ This is structural normalization only: it never invents fields, changes
105
+ values, widens allowed keys, or bypasses the strict CREATE validator.
106
+ """
107
+ if not isinstance(row, dict) or "memory" in row:
108
+ return row, False
109
+ action = row.get("action")
110
+ if not isinstance(action, str) or action.strip().upper() != "CREATE":
111
+ return row, False
112
+ memory_keys = set(row) & set(_CREATE)
113
+ required = {"type", "scope", "title", "body"}
114
+ if not required <= memory_keys:
115
+ return row, False
116
+ normalized = {key: deepcopy(value) for key, value in row.items() if key not in _CREATE}
117
+ normalized["memory"] = {key: deepcopy(row[key]) for key in row if key in _CREATE}
118
+ return normalized, True
39
119
  _GROUP = {"status": "status", "validity": "validity", "scopes": "scope",
40
120
  "assignee": "responsibility", "waiting_on": "responsibility",
41
121
  "deadline": "deadline", "title": "content", "body": "content"}
@@ -51,9 +131,18 @@ def _text(value: Any, maximum: int = 16384, *, empty: bool = False) -> str:
51
131
  return value
52
132
 
53
133
 
54
- def _keys(value: Any, required: set[str], allowed: set[str] | frozenset[str]) -> None:
55
- if not isinstance(value, dict) or not required <= value.keys() or value.keys() - allowed:
56
- raise ValueError("invalid_fields")
134
+ def _keys(value: Any, required: set[str], allowed: set[str] | frozenset[str], *, path: str) -> None:
135
+ if not isinstance(value, dict):
136
+ raise _FieldShapeError(f"{path}:not_object")
137
+ missing = sorted(required - value.keys())
138
+ unexpected = sorted(value.keys() - allowed)
139
+ if missing or unexpected:
140
+ parts = []
141
+ if missing:
142
+ parts.append("missing=" + ",".join(missing))
143
+ if unexpected:
144
+ parts.append("unexpected=" + ",".join(unexpected))
145
+ raise _FieldShapeError(path + ":" + ";".join(parts))
57
146
 
58
147
 
59
148
  def _ref(value: Any, table: Mapping[str, Any]) -> str:
@@ -281,7 +370,7 @@ def _selected(value: Any, evidence: Mapping[str, Any], refs: list[str], *, clear
281
370
  if ref not in refs or evidence[ref]["use"] != "new":
282
371
  raise ValueError("invalid_reference")
283
372
  return {"clear": True, "anchor": source_basis(evidence[ref])}
284
- _keys(value, {"ref", "text"}, {"ref", "text"})
373
+ _keys(value, {"ref", "text"}, {"ref", "text"}, path="selected_time")
285
374
  ref = _ref(value["ref"], evidence)
286
375
  if ref not in refs:
287
376
  raise ValueError("invalid_reference")
@@ -345,7 +434,7 @@ def _parse_row(row: Any, state: Mapping[str, Any]) -> dict[str, Any]:
345
434
  required_keys = {"action"} | required
346
435
  if action != "NO_MEMORY":
347
436
  required_keys.add("evidence")
348
- _keys(row, required_keys, {"action", "evidence"} | optional)
437
+ _keys(row, required_keys, {"action", "evidence"} | optional, path="row")
349
438
  evidence = {e["ref"]: e for e in state["evidence"]}
350
439
  if action == "NO_MEMORY" and state["request_kind"] == "explicit_remember":
351
440
  raise ValueError("explicit_retention_required")
@@ -388,7 +477,8 @@ def _parse_row(row: Any, state: Mapping[str, Any]) -> dict[str, Any]:
388
477
  if action in {"CREATE", "UPDATE"}:
389
478
  fields = deepcopy(row["memory"] if action == "CREATE" else row["patch"])
390
479
  _keys(fields, {"type", "scope", "title", "body"} if action == "CREATE" else set(),
391
- _CREATE if action == "CREATE" else _PATCH)
480
+ _CREATE if action == "CREATE" else _PATCH,
481
+ path="memory" if action == "CREATE" else "patch")
392
482
  if not fields:
393
483
  raise ValueError("empty_patch")
394
484
  if action == "CREATE":
@@ -540,22 +630,30 @@ def compile_incremental(raw: str, snapshot: PlanningSnapshot) -> dict[str, Any]:
540
630
  value = parse_strict_json(raw)
541
631
  except (ModelOutputError, RecursionError) as error:
542
632
  raise ValueError("invalid_json") from error
543
- _keys(value, {"items"}, {"items"})
633
+ _keys(value, {"items"}, {"items"}, path="root")
544
634
  if not isinstance(value["items"], list) or len(value["items"]) > MAX_ITEMS:
545
635
  raise ValueError("invalid_items")
546
636
  state = snapshot.state()
547
637
  known = {e["ref"] for e in state["evidence"]}
548
638
  new = {e["ref"] for e in state["evidence"] if e["use"] == "new"}
549
639
  parsed, issues, poisoned = [], [], set()
550
- def problem(index: int | None, code: str, row: Any) -> None:
640
+ def problem(index: int | None, code: str, row: Any, detail: str | None = None) -> None:
551
641
  refs = row.get("evidence", []) if isinstance(row, dict) else []
552
642
  refs = [r.strip() for r in refs if isinstance(r, str) and r.strip() in known] if isinstance(refs, list) else []
553
- issues.append({"row": index, "code": code, "evidence": list(dict.fromkeys(refs))})
643
+ issue = {"row": index, "code": code, "evidence": list(dict.fromkeys(refs))}
644
+ if detail:
645
+ issue["detail"] = detail
646
+ issues.append(issue)
554
647
  for index, row in enumerate(value["items"]):
555
648
  try:
556
- parsed.append((index, _parse_row(row, state)))
649
+ normalized_row, warnings = _normalize_model_row(row)
650
+ parsed_row = _parse_row(normalized_row, state)
651
+ if warnings:
652
+ parsed_row.setdefault("warnings", []).extend(warnings)
653
+ parsed.append((index, parsed_row))
557
654
  except (ValueError, TypeError) as error:
558
- problem(index, str(error) if isinstance(error, ValueError) else "invalid_fields", row)
655
+ problem(index, str(error) if isinstance(error, ValueError) else "invalid_fields", row,
656
+ getattr(error, "detail", None))
559
657
  if isinstance(row, dict) and isinstance(row.get("target"), str) and row["target"].strip() in state["targets"]:
560
658
  poisoned.add(row["target"].strip())
561
659
  groups: dict[str, list[tuple[int, dict[str, Any]]]] = defaultdict(list)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memleaf
3
- Version: 0.2.67
3
+ Version: 0.2.69
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.65。**
26
+ > **版本:0.2.69。**
27
27
  > 自动提炼和复核共用同一条“未来记忆价值”标准:模型综合未来复用、信息增量、再次读取时的直接可用性和忘记成本,只保留对未来理解、判断或行动有实质影响的最小核心;没有明确价值的信息不提炼,不按具体业务场景硬编码排除。
28
28
  > 提炼契约现在只有一份中文语义契约(旧版英文 B3 提示词已删除),首轮、主题选择、候选修复和 `semantic_maintenance` 使用同一套保留价值判断;同一件事只写一次。
29
29
  > Core 不再因为归属、日期、任务依据或跨项目而丢弃整条候选:归属缺失会归一到证据里唯一被点名的项目、否则落 `global`;日期只接受能在本候选证据里找到出处、且能锚定成 ISO 的写法(`明天`、`周五`、`月底` 都会被解析),无法解析时只丢掉日期字段,记忆照常写入;未获授权的改归属沿用目标原归属。
@@ -2,6 +2,7 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import importlib
5
+ import json
5
6
  import sys
6
7
  import tempfile
7
8
  import types
@@ -99,6 +100,89 @@ class HermesMetadataTests(unittest.TestCase):
99
100
  for invocation in call.call_args_list:
100
101
  self.assertNotIn('source_time', invocation.args[1])
101
102
 
103
+ def test_single_entry_tool_call_batch_is_observed_as_no_match(self):
104
+ retrieval_id = 'retrieval-1'
105
+ messages = [
106
+ {
107
+ 'role': 'assistant',
108
+ 'tool_calls': [{
109
+ 'id': 'bridge-call',
110
+ 'function': {
111
+ 'name': 'tool_call',
112
+ 'arguments': json.dumps({
113
+ 'calls': [{
114
+ 'name': 'mcp__memleaf__search',
115
+ 'arguments': {'query': 'mail status', 'retrieval_id': retrieval_id},
116
+ }]
117
+ }),
118
+ },
119
+ }],
120
+ },
121
+ {
122
+ 'role': 'tool',
123
+ 'tool_call_id': 'bridge-call',
124
+ 'content': json.dumps({'status': 'no_match', 'results': []}),
125
+ },
126
+ ]
127
+ audit = {}
128
+ status = self.module.MemleafMemoryProvider._observe_search_messages(
129
+ messages, retrieval_id, audit_state=audit,
130
+ )
131
+ self.assertEqual(status, 'no_match')
132
+ self.assertEqual(audit['status'], 'NO_MATCH')
133
+
134
+ def test_multi_entry_tool_call_batch_is_not_misattributed(self):
135
+ retrieval_id = 'retrieval-1'
136
+ messages = [
137
+ {
138
+ 'role': 'assistant',
139
+ 'tool_calls': [{
140
+ 'id': 'bridge-call',
141
+ 'function': {
142
+ 'name': 'tool_call',
143
+ 'arguments': json.dumps({
144
+ 'calls': [
145
+ {'name': 'mcp__memleaf__search',
146
+ 'arguments': {'query': 'mail status', 'retrieval_id': retrieval_id}},
147
+ {'name': 'another_tool', 'arguments': {}},
148
+ ]
149
+ }),
150
+ },
151
+ }],
152
+ },
153
+ {
154
+ 'role': 'tool',
155
+ 'tool_call_id': 'bridge-call',
156
+ 'content': json.dumps({'status': 'no_match', 'results': []}),
157
+ },
158
+ ]
159
+ audit = {}
160
+ status = self.module.MemleafMemoryProvider._observe_search_messages(
161
+ messages, retrieval_id, audit_state=audit,
162
+ )
163
+ self.assertEqual(status, 'unknown')
164
+ self.assertEqual(audit['status'], 'SEARCH_UNKNOWN')
165
+
166
+ def test_clean_marker_is_removed_before_assistant_capture(self):
167
+ with tempfile.TemporaryDirectory() as root:
168
+ service = Memleaf.initialize(Path(root) / 'vault')
169
+ provider = self.module.MemleafMemoryProvider()
170
+ provider._client = object(); provider._auto_process = False
171
+ raw_final = 'No new mail.\n\n<!--CLEAN-->'
172
+ rows = [
173
+ dict(role='user', content='Check mail', id='u', source_sequence=1),
174
+ dict(role='assistant', content=raw_final, id='a', source_sequence=2),
175
+ ]
176
+ def call(name, args, **kw):
177
+ self.assertEqual(name, 'capture')
178
+ result = service.capture(**args)
179
+ return {'stored': result.stored, 'duplicate': result.duplicate}
180
+ with patch.object(provider, '_call', side_effect=call):
181
+ provider.sync_turn('Check mail', raw_final, session_id='s', turn_number=3, messages=rows)
182
+ turn = parse_inbox(service.vault)[0]
183
+ self.assertEqual(turn.events[1].content, 'No new mail.')
184
+ self.assertNotIn('<!--CLEAN-->', turn.events[1].content)
185
+
102
186
 
103
187
  if __name__ == '__main__':
104
188
  unittest.main()
@@ -117,6 +117,81 @@ class IncrementalProtocolTests(unittest.TestCase):
117
117
  op = self.run_rows(self.create(type="todo", status="active", assignee="user:1", waiting_on=None))["operations"][0]
118
118
  self.assertEqual(op["memory"]["assignee"], "user:1")
119
119
 
120
+ def test_flattened_create_wrapper_is_normalized_before_strict_validation(self):
121
+ row = {
122
+ "action": "CREATE", "evidence": ["e1"],
123
+ "type": "fact", "scope": "s1",
124
+ "title": "Known requirement", "body": "Keep this requirement.",
125
+ }
126
+ out = self.run_rows(row)
127
+ self.assertFalse(out["issues"])
128
+ self.assertEqual(len(out["operations"]), 1)
129
+ op = out["operations"][0]
130
+ self.assertEqual(op["memory"]["type"], "fact")
131
+ self.assertEqual(op["memory"]["scopes"], ["project:Atlas"])
132
+ self.assertIn("create_memory_wrapper_normalized", op["warnings"])
133
+
134
+ def test_flattened_create_with_unknown_field_stays_rejected(self):
135
+ row = {
136
+ "action": "CREATE", "evidence": ["e1"],
137
+ "type": "fact", "scope": "s1",
138
+ "title": "Known requirement", "body": "Keep this requirement.",
139
+ "unexpected": "must not be accepted",
140
+ }
141
+ out = self.run_rows(row)
142
+ self.assertFalse(out["operations"])
143
+ self.assertEqual(out["issues"][0]["code"], "invalid_fields")
144
+ self.assertEqual(out["issues"][0]["detail"], "row:unexpected=unexpected")
145
+
146
+ def test_null_fields_from_other_branches_are_ignored(self):
147
+ row = self.create()
148
+ row.update(target=None, patch=None, reason=None, need=None, reopen=None)
149
+ out = self.run_rows(row)
150
+ self.assertFalse(out["issues"])
151
+ self.assertEqual(len(out["operations"]), 1)
152
+ self.assertIn("null_branch_placeholder_normalized", out["operations"][0]["warnings"])
153
+
154
+ def test_flattened_create_with_null_memory_wrapper_is_normalized(self):
155
+ row = {
156
+ "action": "CREATE", "evidence": ["e1"], "memory": None,
157
+ "type": "fact", "scope": "s1",
158
+ "title": "Known requirement", "body": "Keep this requirement.",
159
+ }
160
+ out = self.run_rows(row)
161
+ self.assertFalse(out["issues"])
162
+ warnings = out["operations"][0]["warnings"]
163
+ self.assertIn("null_memory_wrapper_normalized", warnings)
164
+ self.assertIn("create_memory_wrapper_normalized", warnings)
165
+
166
+ def test_single_scopes_alias_and_default_validity_are_normalized(self):
167
+ row = self.create()
168
+ row["memory"].pop("scope")
169
+ row["memory"].update(scopes=["s1"], validity="valid")
170
+ out = self.run_rows(row)
171
+ self.assertFalse(out["issues"])
172
+ op = out["operations"][0]
173
+ self.assertEqual(op["memory"]["scopes"], ["project:Atlas"])
174
+ self.assertIn("create_single_scope_alias_normalized", op["warnings"])
175
+ self.assertIn("create_default_validity_normalized", op["warnings"])
176
+
177
+ def test_ambiguous_scopes_alias_stays_rejected_with_detail(self):
178
+ row = self.create()
179
+ row["memory"].pop("scope")
180
+ row["memory"]["scopes"] = ["s1", "s2"]
181
+ out = self.run_rows(row)
182
+ self.assertFalse(out["operations"])
183
+ self.assertEqual(out["issues"][0]["code"], "invalid_fields")
184
+ self.assertEqual(out["issues"][0]["detail"], "memory:missing=scope;unexpected=scopes")
185
+
186
+ def test_flattened_create_missing_required_field_stays_rejected(self):
187
+ row = {
188
+ "action": "CREATE", "evidence": ["e1"],
189
+ "type": "fact", "scope": "s1", "title": "Known requirement",
190
+ }
191
+ out = self.run_rows(row)
192
+ self.assertFalse(out["operations"])
193
+ self.assertEqual(out["issues"][0]["code"], "invalid_fields")
194
+
120
195
  def test_target_type_cannot_be_reclassified_in_patch(self):
121
196
  out = self.run_rows(self.update(type="fact", status="completed"))
122
197
  self.assertFalse(out["operations"])
@@ -283,7 +283,7 @@ class ServerCompatibilityTests(unittest.TestCase):
283
283
  with patch.object(self.service,'stats',side_effect=AssertionError('must not scan')):
284
284
  result=self.hello(self.good)['result']
285
285
  self.assertEqual(result['_meta'][BUILD_META],self.good)
286
- self.assertEqual(result['serverInfo']['version'],'0.2.67')
286
+ self.assertEqual(result['serverInfo']['version'],'0.2.69')
287
287
 
288
288
  def test_compatible_bridge_dispatches_capture(self):
289
289
  self.hello(self.good)
File without changes
File without changes
File without changes