memleaf 0.2.37__tar.gz → 0.2.38__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 (191) hide show
  1. {memleaf-0.2.37 → memleaf-0.2.38}/CHANGELOG.md +8 -0
  2. {memleaf-0.2.37/src/memleaf.egg-info → memleaf-0.2.38}/PKG-INFO +2 -2
  3. {memleaf-0.2.37 → memleaf-0.2.38}/README.en.md +1 -1
  4. {memleaf-0.2.37 → memleaf-0.2.38}/README.md +1 -1
  5. {memleaf-0.2.37 → memleaf-0.2.38}/pyproject.toml +1 -1
  6. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/__init__.py +1 -1
  7. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/config.py +21 -0
  8. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/hermes_provider/plugin.yaml +1 -1
  9. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/llm/base.py +10 -0
  10. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/llm/openai_compatible.py +36 -0
  11. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/llm/router.py +17 -0
  12. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/memory_planner.py +55 -77
  13. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/model_execution.py +136 -12
  14. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/planning_context.py +1 -31
  15. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/process_jobs.py +83 -0
  16. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/prompts.py +84 -57
  17. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/update_review.py +8 -1
  18. {memleaf-0.2.37 → memleaf-0.2.38/src/memleaf.egg-info}/PKG-INFO +2 -2
  19. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf.egg-info/SOURCES.txt +1 -0
  20. memleaf-0.2.38/tests/test_gate_scope_latency_v038.py +201 -0
  21. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_maintenance_v2.py +2 -0
  22. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_new_scope_source_grounding.py +22 -6
  23. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_processing_observability_concurrency.py +6 -1
  24. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_session_lineage.py +8 -4
  25. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_stage_b1.py +12 -5
  26. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_stage_b3d_scope_maintenance.py +3 -3
  27. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_v023_scope_correction.py +1 -1
  28. {memleaf-0.2.37 → memleaf-0.2.38}/IMPLEMENTATION_PLAN.md +0 -0
  29. {memleaf-0.2.37 → memleaf-0.2.38}/LICENSE +0 -0
  30. {memleaf-0.2.37 → memleaf-0.2.38}/MANIFEST.in +0 -0
  31. {memleaf-0.2.37 → memleaf-0.2.38}/RELEASE_CHECKLIST.md +0 -0
  32. {memleaf-0.2.37 → memleaf-0.2.38}/docs/capture-budget-design.md +0 -0
  33. {memleaf-0.2.37 → memleaf-0.2.38}/docs/config-migrations.md +0 -0
  34. {memleaf-0.2.37 → memleaf-0.2.38}/docs/core-refactor.md +0 -0
  35. {memleaf-0.2.37 → memleaf-0.2.38}/docs/evidence-retention.md +0 -0
  36. {memleaf-0.2.37 → memleaf-0.2.38}/docs/gate-evidence-boundary.md +0 -0
  37. {memleaf-0.2.37 → memleaf-0.2.38}/docs/general-processing.md +0 -0
  38. {memleaf-0.2.37 → memleaf-0.2.38}/docs/hermes-mcp-runtime.md +0 -0
  39. {memleaf-0.2.37 → memleaf-0.2.38}/docs/performance.md +0 -0
  40. {memleaf-0.2.37 → memleaf-0.2.38}/docs/processing-quality-acceptance.md +0 -0
  41. {memleaf-0.2.37 → memleaf-0.2.38}/docs/v0.2.26-processing-status.md +0 -0
  42. {memleaf-0.2.37 → memleaf-0.2.38}/examples/README.md +0 -0
  43. {memleaf-0.2.37 → memleaf-0.2.38}/examples/basic_usage.py +0 -0
  44. {memleaf-0.2.37 → memleaf-0.2.38}/examples/live_core_lifecycle_acceptance.py +0 -0
  45. {memleaf-0.2.37 → memleaf-0.2.38}/examples/live_processing_acceptance.py +0 -0
  46. {memleaf-0.2.37 → memleaf-0.2.38}/examples/mcp_stdio.ndjson +0 -0
  47. {memleaf-0.2.37 → memleaf-0.2.38}/install.ps1 +0 -0
  48. {memleaf-0.2.37 → memleaf-0.2.38}/install.sh +0 -0
  49. {memleaf-0.2.37 → memleaf-0.2.38}/setup.cfg +0 -0
  50. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/__main__.py +0 -0
  51. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/adapters/__init__.py +0 -0
  52. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/adapters/antigravity.py +0 -0
  53. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/adapters/base.py +0 -0
  54. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/adapters/codex.py +0 -0
  55. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/adapters/hermes.py +0 -0
  56. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/admission.py +0 -0
  57. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/budget.py +0 -0
  58. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/capture.py +0 -0
  59. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/cli.py +0 -0
  60. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/compaction.py +0 -0
  61. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/create_coordinator.py +0 -0
  62. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/credentials.py +0 -0
  63. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/evidence_budget.py +0 -0
  64. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/evidence_policy.py +0 -0
  65. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/frontmatter.py +0 -0
  66. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/hermes_provider/README.md +0 -0
  67. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/hermes_provider/__init__.py +0 -0
  68. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/hermes_provider/evidence_budget.py +0 -0
  69. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/hermes_runtime.py +0 -0
  70. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/host_events.py +0 -0
  71. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/host_runtime.py +0 -0
  72. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/inbox.py +0 -0
  73. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/index.py +0 -0
  74. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/inspection.py +0 -0
  75. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/installer.py +0 -0
  76. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/llm/__init__.py +0 -0
  77. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/llm/claude_compatible.py +0 -0
  78. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/llm/gemini.py +0 -0
  79. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/locking.py +0 -0
  80. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/mcp_server.py +0 -0
  81. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/memory_commit.py +0 -0
  82. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/memory_writer.py +0 -0
  83. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/model_discovery.py +0 -0
  84. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/models.py +0 -0
  85. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/native_index.py +0 -0
  86. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/native_registration.py +0 -0
  87. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/parallel_model.py +0 -0
  88. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/process_common.py +0 -0
  89. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/process_journal.py +0 -0
  90. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/process_owner.py +0 -0
  91. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/processing.py +0 -0
  92. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/provenance.py +0 -0
  93. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/recording_policy.py +0 -0
  94. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/redaction.py +0 -0
  95. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/retention.py +0 -0
  96. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/retrieval.py +0 -0
  97. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/retrieval_gate.py +0 -0
  98. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/scope_maintenance.py +0 -0
  99. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/scope_state.py +0 -0
  100. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/service.py +0 -0
  101. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/source_policy.py +0 -0
  102. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/state_layout.py +0 -0
  103. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/target_reconciliation.py +0 -0
  104. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/turn_audit.py +0 -0
  105. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/turn_plan.py +0 -0
  106. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/update_coordinator.py +0 -0
  107. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/validation.py +0 -0
  108. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf/vault.py +0 -0
  109. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf.egg-info/dependency_links.txt +0 -0
  110. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf.egg-info/entry_points.txt +0 -0
  111. {memleaf-0.2.37 → memleaf-0.2.38}/src/memleaf.egg-info/top_level.txt +0 -0
  112. {memleaf-0.2.37 → memleaf-0.2.38}/tests/__init__.py +0 -0
  113. {memleaf-0.2.37 → memleaf-0.2.38}/tests/semantic_fixtures.py +0 -0
  114. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_admission_noise.py +0 -0
  115. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_automatic_duplicate_noop_collision.py +0 -0
  116. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_candidate_polarity.py +0 -0
  117. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_codex_install.py +0 -0
  118. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_codex_native_cli.py +0 -0
  119. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_config_migrations_v028.py +0 -0
  120. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_context_budget.py +0 -0
  121. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_conversation_only.py +0 -0
  122. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_credential_safety.py +0 -0
  123. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_cross_host_acceptance.py +0 -0
  124. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_cross_turn_dedupe_regressions.py +0 -0
  125. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_due_date_grounding.py +0 -0
  126. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_due_date_grounding_retry.py +0 -0
  127. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_email_actionable_coverage.py +0 -0
  128. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_evidence_budget.py +0 -0
  129. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_evidence_retention_policy.py +0 -0
  130. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_external_source_dates.py +0 -0
  131. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_extraction_quality_regressions.py +0 -0
  132. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_gate_capacity.py +0 -0
  133. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_gate_schema_repair.py +0 -0
  134. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_general_evidence_admission.py +0 -0
  135. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_general_tool_provenance.py +0 -0
  136. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_global_todo_acceptance.py +0 -0
  137. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_global_todo_query_no_write.py +0 -0
  138. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_global_todo_retrieval.py +0 -0
  139. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_hermes_native_registration.py +0 -0
  140. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_hermes_provider.py +0 -0
  141. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_hermes_runtime_install.py +0 -0
  142. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_hermes_stdio_transport.py +0 -0
  143. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_hermes_transport_evidence.py +0 -0
  144. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_hermes_windows_subprocess.py +0 -0
  145. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_host_events.py +0 -0
  146. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_host_runtime_contract.py +0 -0
  147. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_inspection_state_v028.py +0 -0
  148. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_install.py +0 -0
  149. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_long_run_hygiene.py +0 -0
  150. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_model_discovery.py +0 -0
  151. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_model_owned_fields.py +0 -0
  152. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_partial_retry_idempotency.py +0 -0
  153. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_phase2_model_decisions.py +0 -0
  154. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_process_jobs.py +0 -0
  155. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_process_owner_locking.py +0 -0
  156. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_processing_contract_v026.py +0 -0
  157. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_pypi_install.py +0 -0
  158. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_read_only_deferred_isolation.py +0 -0
  159. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_retrieval_gate.py +0 -0
  160. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_retrieval_v2.py +0 -0
  161. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_review_source_context.py +0 -0
  162. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_revision_digest.py +0 -0
  163. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_shared_memory_refactor.py +0 -0
  164. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_source_neutral_todos_v028.py +0 -0
  165. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_stage_a.py +0 -0
  166. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_stage_b2a.py +0 -0
  167. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_stage_b2b.py +0 -0
  168. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_stage_b3a_commit.py +0 -0
  169. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_stage_b3a_contract.py +0 -0
  170. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_stage_b3b_native_context.py +0 -0
  171. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_stage_b3b_native_index.py +0 -0
  172. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_stage_b3b_scope.py +0 -0
  173. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_stage_b3c_retrieval.py +0 -0
  174. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_stage_c1_mcp.py +0 -0
  175. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_stage_c2_init.py +0 -0
  176. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_stage_c3_packaging.py +0 -0
  177. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_state_layout_v028.py +0 -0
  178. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_summary_date_grounding_integration.py +0 -0
  179. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_target_reconciliation.py +0 -0
  180. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_target_reconciliation_integration.py +0 -0
  181. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_todo_state_recovery.py +0 -0
  182. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_update_review.py +0 -0
  183. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_update_target_recovery.py +0 -0
  184. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_upgrade_preserves_vault.py +0 -0
  185. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_v2_gate_limits.py +0 -0
  186. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_v2_host_flow.py +0 -0
  187. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_v2_mcp_flow.py +0 -0
  188. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_v2_nomatch_semantics.py +0 -0
  189. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_v2_search_gate_acceptance.py +0 -0
  190. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_whole_unit_bindings.py +0 -0
  191. {memleaf-0.2.37 → memleaf-0.2.38}/tests/test_windows_public_mcp_launcher.py +0 -0
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to memleaf are documented here.
4
4
 
5
+ ## 0.2.38 — 2026-09-09
6
+
7
+ - Unify automatic project-Scope grounding: registered and newly named model-selected projects now use the same exact candidate-bound source check. Remove the later registered-name occurrence conflict scan that could misclassify an implementation platform/product mention as ownership and reject the correct new project.
8
+ - Strengthen final CREATE/UPDATE semantic review so a `project:<name>` Scope with `scope_source=model` is itself treated as an affiliation claim; product/platform/system/notification/implementation mentions cannot authorize project ownership, and an explicit contradictory owner defers instead of silently changing Scope.
9
+ - Add safe per-model-call telemetry with fixed operation classes (`gate_primary`, `gate_coverage_repair`, format repair, summarize/review/coordination variants), request duration, input/output size, provider token usage, DeepSeek cache-hit/miss tokens and reasoning-token counts when supplied. Prompt/response text and credentials are never persisted.
10
+ - Add explicit `llm.thinking` configuration for Gate/summarize/compact. The default is `low`, retaining reasoning at the lowest supported effort; users may select `disabled`, `default`, `high`, or `max` explicitly. DeepSeek OpenAI-format calls send the corresponding thinking controls.
11
+ - Reduce Gate input cost by removing a duplicated system-policy tail and replace coverage re-checks with a narrow unresolved-evidence protocol instead of rerunning the full Gate prompt. Deterministic validation does not claim a specific real-provider latency reduction.
12
+
5
13
  ## 0.2.37 — 2026-09-09
6
14
 
7
15
  - Add a dedicated `memleaf-mcpw` GUI entry point for the Hermes public MCP on Windows. The GUI-subsystem launcher does not allocate a console window even when an older Hermes/MCP SDK starts it without `CREATE_NO_WINDOW`; it enters the same `memleaf.mcp_server:main` implementation and keeps the same stdio JSON-RPC protocol.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memleaf
3
- Version: 0.2.37
3
+ Version: 0.2.38
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.37。**
26
+ > **版本:0.2.38。**
27
27
  > 记忆只从用户与 Agent 的可见对话提炼。Agent 已在回复中整理的事实、项目进展和明确待办可作为来源;邮件、附件、网页和终端等工具原文不进入记忆提炼。模型负责保留原话中的不确定性,现有记忆仅用于比较、去重和更新。Markdown 仍是唯一事实源。
28
28
  > **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
29
29
 
@@ -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.37.**
7
+ > **Version: 0.2.38.**
8
8
  > Automatic extraction now uses only the current turn's visible user input and final assistant reply. Raw tool output, attachments, web/file/terminal payloads and legacy tool-evidence bodies are not new source evidence; existing or retrieved memory remains comparison context rather than source authority. Background processing is persisted as a local job and can be checked through the read-only `process_status` MCP tool; failed work remains retryable and fail closed. The release also tightens source/date grounding, target reconciliation, duplicate/no-op handling and semantic review before writes. Markdown remains the sole source of truth with no SQLite runtime dependency. Acceptance covers deterministic regression suites and synthetic inputs; it does not claim real-mail or customer-business acceptance.
9
9
  > **The current release supports Hermes and Codex.** Antigravity is not detected, installed, or configured.
10
10
 
@@ -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.37。**
7
+ > **版本:0.2.38。**
8
8
  > 记忆只从用户与 Agent 的可见对话提炼。Agent 已在回复中整理的事实、项目进展和明确待办可作为来源;邮件、附件、网页和终端等工具原文不进入记忆提炼。模型负责保留原话中的不确定性,现有记忆仅用于比较、去重和更新。Markdown 仍是唯一事实源。
9
9
  > **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
10
10
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memleaf"
7
- version = "0.2.37"
7
+ version = "0.2.38"
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.37"
3
+ __version__ = "0.2.38"
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,9 @@ MAX_REQUEST_TIMEOUT = 240
19
19
  DEFAULT_MODEL_CONCURRENCY = 3
20
20
  MIN_MODEL_CONCURRENCY = 1
21
21
  MAX_MODEL_CONCURRENCY = 8
22
+ THINKING_PURPOSES = ("gate", "summarize", "compact")
23
+ THINKING_MODES = frozenset({"default", "disabled", "low", "high", "max"})
24
+ DEFAULT_THINKING = {purpose: "low" for purpose in THINKING_PURPOSES}
22
25
 
23
26
 
24
27
  def _normalize_request_timeout(value: Any) -> int | float:
@@ -41,6 +44,21 @@ def _normalize_model_concurrency(value: Any) -> int:
41
44
  return value
42
45
 
43
46
 
47
+ def _normalize_thinking_settings(value: Any) -> dict[str, str]:
48
+ if value is None:
49
+ value = {}
50
+ if not isinstance(value, Mapping):
51
+ raise ValueError("invalid memleaf llm.thinking settings")
52
+ if set(value) - set(THINKING_PURPOSES):
53
+ raise ValueError("invalid memleaf llm.thinking settings")
54
+ result = dict(DEFAULT_THINKING)
55
+ for purpose, mode in value.items():
56
+ if not isinstance(mode, str) or mode not in THINKING_MODES:
57
+ raise ValueError("invalid memleaf llm.thinking settings")
58
+ result[purpose] = mode
59
+ return result
60
+
61
+
44
62
  DEFAULT_CONFIG: dict[str, Any] = {
45
63
  "vault": "~/.memleaf",
46
64
  "agents": {"codex": True, "hermes": True, "antigravity": False},
@@ -75,6 +93,7 @@ DEFAULT_CONFIG: dict[str, Any] = {
75
93
  "context_window": 200000,
76
94
  "request_timeout": DEFAULT_REQUEST_TIMEOUT,
77
95
  "diagnostic_logging": False,
96
+ "thinking": dict(DEFAULT_THINKING),
78
97
  },
79
98
  }
80
99
 
@@ -188,6 +207,7 @@ def load_config(path: Path | str, *, vault: Path | str | None = None) -> dict[st
188
207
  raise ValueError("invalid memleaf llm settings")
189
208
  llm = dict(llm)
190
209
  llm["request_timeout"] = _normalize_request_timeout(llm.get("request_timeout", DEFAULT_REQUEST_TIMEOUT))
210
+ llm["thinking"] = _normalize_thinking_settings(llm.get("thinking"))
191
211
  if type(llm.get("diagnostic_logging", False)) is not bool:
192
212
  raise ValueError("invalid memleaf llm.diagnostic_logging")
193
213
  merged["llm"] = llm
@@ -231,6 +251,7 @@ def save_config(path: Path | str, config: Mapping[str, Any]) -> None:
231
251
  normalized_llm["request_timeout"] = _normalize_request_timeout(
232
252
  normalized_llm.get("request_timeout", DEFAULT_REQUEST_TIMEOUT)
233
253
  )
254
+ normalized_llm["thinking"] = _normalize_thinking_settings(normalized_llm.get("thinking"))
234
255
  diagnostic_logging = normalized_llm.get("diagnostic_logging", False)
235
256
  if type(diagnostic_logging) is not bool:
236
257
  raise ValueError("invalid memleaf llm.diagnostic_logging")
@@ -1,5 +1,5 @@
1
1
  name: memleaf
2
- version: 0.2.37
2
+ version: 0.2.38
3
3
  description: "Hermes-native external MemoryProvider for local-first Markdown memory shared with the memleaf MCP server."
4
4
  hooks:
5
5
  - prefetch
@@ -6,6 +6,7 @@ import json
6
6
  import inspect
7
7
  import math
8
8
  import socket
9
+ import threading
9
10
  import urllib.error
10
11
  import urllib.request
11
12
  from typing import Any, Callable, Mapping, Optional, Protocol
@@ -246,10 +247,19 @@ class HTTPModelBackend:
246
247
  self.model = model
247
248
  self.timeout = normalize_request_timeout(timeout)
248
249
  self._opener = opener or urllib.request.urlopen
250
+ self._call_metrics_local = threading.local()
249
251
  # The built-in stateless urllib transport can be used concurrently.
250
252
  # An injected opener is caller-owned and therefore defaults to serial.
251
253
  self.parallel_safe = opener is None
252
254
 
255
+ def _set_call_metrics(self, value: Mapping[str, Any] | None) -> None:
256
+ self._call_metrics_local.value = dict(value) if isinstance(value, Mapping) else {}
257
+
258
+ def consume_call_metrics(self) -> dict[str, Any]:
259
+ value = getattr(self._call_metrics_local, "value", {})
260
+ self._call_metrics_local.value = {}
261
+ return dict(value) if isinstance(value, Mapping) else {}
262
+
253
263
  @staticmethod
254
264
  def _is_timeout_reason(value: Any) -> bool:
255
265
  if isinstance(value, (TimeoutError, socket.timeout)):
@@ -20,10 +20,12 @@ class OpenAICompatibleBackend(HTTPModelBackend):
20
20
  opener: Optional[Callable[..., Any]] = None,
21
21
  json_mode: bool = False,
22
22
  provider_name: str = "openai",
23
+ thinking: Mapping[str, Any] | None = None,
23
24
  ):
24
25
  super().__init__(base_url=base_url, api_key=api_key, model=model, timeout=timeout, opener=opener)
25
26
  self.json_mode = bool(json_mode)
26
27
  self.provider_name = provider_name.casefold() if isinstance(provider_name, str) else "openai"
28
+ self.thinking = dict(thinking) if isinstance(thinking, Mapping) else {}
27
29
 
28
30
  @staticmethod
29
31
  def _response_text_chars(value: Any) -> int:
@@ -83,7 +85,33 @@ class OpenAICompatibleBackend(HTTPModelBackend):
83
85
  "reasoning_chars": reasoning_chars,
84
86
  }
85
87
 
88
+ def _thinking_mode(self, purpose: str) -> str:
89
+ if purpose not in {"gate", "summarize", "compact"}:
90
+ return "default"
91
+ value = self.thinking.get(purpose, "low")
92
+ return value if value in {"default", "disabled", "low", "high", "max"} else "low"
93
+
94
+ @staticmethod
95
+ def _usage_metrics(value: Mapping[str, Any], *, thinking_mode: str) -> dict[str, Any]:
96
+ usage = value.get("usage")
97
+ result: dict[str, Any] = {"thinking_mode": thinking_mode}
98
+ if not isinstance(usage, Mapping):
99
+ return result
100
+ for key in (
101
+ "prompt_tokens", "completion_tokens", "total_tokens",
102
+ "prompt_cache_hit_tokens", "prompt_cache_miss_tokens",
103
+ ):
104
+ item = usage.get(key)
105
+ if isinstance(item, int) and not isinstance(item, bool) and 0 <= item <= 10_000_000:
106
+ result[key] = item
107
+ details = usage.get("completion_tokens_details")
108
+ reasoning = details.get("reasoning_tokens") if isinstance(details, Mapping) else None
109
+ if isinstance(reasoning, int) and not isinstance(reasoning, bool) and 0 <= reasoning <= 10_000_000:
110
+ result["reasoning_tokens"] = reasoning
111
+ return result
112
+
86
113
  def complete(self, prompt: str, *, system: str = "", purpose: str = "", temperature: float = 0.0) -> str:
114
+ self._set_call_metrics({})
87
115
  messages = []
88
116
  if system:
89
117
  messages.append({"role": "system", "content": system})
@@ -93,6 +121,13 @@ class OpenAICompatibleBackend(HTTPModelBackend):
93
121
  "messages": messages,
94
122
  "temperature": temperature,
95
123
  }
124
+ thinking_mode = self._thinking_mode(purpose)
125
+ if self.provider_name == "deepseek" and thinking_mode != "default":
126
+ if thinking_mode == "disabled":
127
+ payload["thinking"] = {"type": "disabled"}
128
+ else:
129
+ payload["thinking"] = {"type": "enabled"}
130
+ payload["reasoning_effort"] = thinking_mode
96
131
  if self.json_mode and purpose in {"gate", "summarize", "compact"}:
97
132
  payload["response_format"] = {"type": "json_object"}
98
133
  value = self._post_json(
@@ -117,6 +152,7 @@ class OpenAICompatibleBackend(HTTPModelBackend):
117
152
  stage=purpose,
118
153
  validation_reason="response_shape",
119
154
  )
155
+ self._set_call_metrics(self._usage_metrics(value, thinking_mode=thinking_mode))
120
156
  try:
121
157
  return self._text(message.get("content"), stage=purpose)
122
158
  except ModelError as error:
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import logging
6
6
  import os
7
+ import threading
7
8
  from typing import Any, Callable, Mapping, Optional
8
9
 
9
10
  from .base import (
@@ -44,6 +45,7 @@ class ModelRouter:
44
45
  self.host = self._coerce_host(host)
45
46
  self.api = self._coerce_api(api) if api is not None else self._build_api()
46
47
  self.diagnostics: list[dict[str, str]] = []
48
+ self._call_metrics_local = threading.local()
47
49
 
48
50
  @classmethod
49
51
  def from_config(cls, config: Mapping[str, Any], **kwargs: Any) -> "ModelRouter":
@@ -122,6 +124,7 @@ class ModelRouter:
122
124
  **kwargs,
123
125
  json_mode=provider in _JSON_MODE_PROVIDERS,
124
126
  provider_name=provider or "openai",
127
+ thinking=config.get("thinking") if isinstance(config.get("thinking"), Mapping) else None,
125
128
  )
126
129
  except (ModelError, ValueError, TypeError):
127
130
  return None
@@ -142,6 +145,7 @@ class ModelRouter:
142
145
  return str(getattr(backend, "provider", "unknown")), str(getattr(backend, "model", "unknown"))
143
146
 
144
147
  def _call(self, backend: ModelBackend, prompt: str, *, system: str, purpose: str, temperature: float) -> str:
148
+ self._call_metrics_local.value = {}
145
149
  try:
146
150
  value = backend.complete(prompt, system=system, purpose=purpose, temperature=temperature)
147
151
  except ModelError as error:
@@ -149,10 +153,23 @@ class ModelRouter:
149
153
  raise
150
154
  except Exception as error:
151
155
  raise ModelError("model backend failed", stage=purpose) from error
156
+ finally:
157
+ consume = getattr(backend, "consume_call_metrics", None)
158
+ if callable(consume):
159
+ try:
160
+ metrics = consume()
161
+ except Exception:
162
+ metrics = {}
163
+ self._call_metrics_local.value = dict(metrics) if isinstance(metrics, Mapping) else {}
152
164
  if not isinstance(value, str):
153
165
  raise ModelError("model backend returned non-text output", code="model_invalid_response", stage=purpose)
154
166
  return value
155
167
 
168
+ def consume_call_metrics(self) -> dict[str, Any]:
169
+ value = getattr(self._call_metrics_local, "value", {})
170
+ self._call_metrics_local.value = {}
171
+ return dict(value) if isinstance(value, Mapping) else {}
172
+
156
173
  def complete(self, prompt: str, *, system: str = "", purpose: str = "", temperature: float = 0.0) -> str:
157
174
  if not isinstance(prompt, str):
158
175
  raise TypeError("model prompt must be text")
@@ -15,7 +15,7 @@ from .update_coordinator import UpdateCoordinator
15
15
  from .target_reconciliation import reconcile_candidate_target
16
16
  from .evidence_policy import retain_tool_evidence
17
17
  from .parallel_model import run_ordered_keyed_jobs
18
- from .prompts import COVERAGE_ALREADY_COMPLETED_CORRECTION, COVERAGE_CORRECTION, GATE_SYSTEM, SUMMARIZE_SYSTEM, gate_prompt, summarize_prompt
18
+ from .prompts import COVERAGE_ALREADY_COMPLETED_CORRECTION, COVERAGE_CORRECTION, GATE_COVERAGE_SYSTEM, GATE_SYSTEM, SUMMARIZE_SYSTEM, coverage_repair_prompt, gate_prompt, summarize_prompt
19
19
  from .retrieval import normalize_term
20
20
  from .scope_state import ScopeError, normalize_scopes
21
21
  from .validation import ModelOutputError, NO_CHANGE_DECISION, _model_scope_grounding_evidence, parse_gate_output, parse_strict_json, parse_summarize_output
@@ -37,48 +37,6 @@ def _explicit_project_scope_authorizations(scope: Any) -> tuple[str, ...]:
37
37
  )
38
38
 
39
39
 
40
- def _unregistered_model_project_scopes(
41
- candidate: Mapping[str, Any],
42
- scope_registry: Mapping[str, Any] | None,
43
- authorized_scopes: Iterable[str] = (),
44
- ) -> tuple[str, ...]:
45
- """Return model-selected project scopes absent from the local registry.
46
-
47
- The existing Gate validator deliberately accepts a new project scope when
48
- the candidate memory names it. This second check needs to distinguish
49
- that case from a registered canonical name or alias, whose behavior is
50
- already covered by the normal scope rules.
51
- """
52
-
53
- if candidate.get("scope_source") != "model" or not candidate.get("worth"):
54
- return ()
55
- scopes = tuple(
56
- scope
57
- for scope in candidate.get("scopes", ())
58
- if isinstance(scope, str) and scope.partition(":")[0] == "project"
59
- )
60
- if not scopes:
61
- return ()
62
- selected_owners, _ = _model_scope_grounding_evidence(
63
- "", scopes, scope_registry
64
- )
65
- registered = {
66
- key.casefold()
67
- for key in (scope_registry or {})
68
- if isinstance(key, str) and key.partition(":")[0] == "project"
69
- }
70
- authorized = {
71
- value.casefold()
72
- for value in authorized_scopes
73
- if isinstance(value, str) and value.partition(":")[0] == "project"
74
- }
75
- return tuple(
76
- scope
77
- for scope in scopes
78
- if selected_owners.get(scope.casefold(), scope.casefold()) not in registered
79
- and scope.casefold() not in authorized
80
- )
81
-
82
40
 
83
41
  def _candidate_bound_source_text(
84
42
  candidate: Mapping[str, Any],
@@ -127,25 +85,37 @@ def _model_project_scope_is_source_grounded(
127
85
  scope_registry: Mapping[str, Any] | None,
128
86
  authorized_scopes: Iterable[str] = (),
129
87
  ) -> bool:
130
- """Require each new model project scope to occur in its bound source.
131
-
132
- Matching is delegated to the same registry-aware helper used by Gate
133
- validation. The input is restricted to immutable original ``EvidenceUnit``
134
- text from this candidate's current batch, so candidate prose, other units,
135
- related memories, domain-to-project expansion, and semantic inference
136
- cannot lend a project name to this candidate. A literal project name that
137
- appears in the original source remains matchable.
88
+ """Validate every model-selected project Scope against exact bound source.
89
+
90
+ Registered and newly named projects intentionally use the same rule. Core
91
+ verifies that the selected project's canonical name or configured alias is
92
+ present in this candidate's immutable bound source, unless the caller
93
+ explicitly authorized that project Scope. Core does not infer whether some
94
+ other mentioned name is an owner, implementation platform, product,
95
+ notification source, or comparison context; semantic review owns that
96
+ relationship judgment.
138
97
  """
139
98
 
140
- new_scopes = _unregistered_model_project_scopes(
141
- candidate, scope_registry, authorized_scopes
99
+ if candidate.get("scope_source") != "model" or not candidate.get("worth"):
100
+ return True
101
+ scopes = tuple(
102
+ scope
103
+ for scope in candidate.get("scopes", ())
104
+ if isinstance(scope, str) and scope.partition(":")[0] == "project"
142
105
  )
143
- if not new_scopes:
106
+ if not scopes:
144
107
  return True
108
+ authorized = {
109
+ value.casefold()
110
+ for value in authorized_scopes
111
+ if isinstance(value, str) and value.partition(":")[0] == "project"
112
+ }
145
113
  source_text = _candidate_bound_source_text(candidate, batch_units)
146
- if not source_text:
147
- return False
148
- for scope in new_scopes:
114
+ for scope in scopes:
115
+ if scope.casefold() in authorized:
116
+ continue
117
+ if not source_text:
118
+ return False
149
119
  owners, matches = _model_scope_grounding_evidence(
150
120
  source_text, (scope,), scope_registry
151
121
  )
@@ -747,8 +717,9 @@ class MemoryPlanner:
747
717
  prepared_candidates: list[dict[str, Any]] = []
748
718
  for candidate in parsed["candidates"]:
749
719
  item = dict(candidate)
750
- if self.inputs._scope_evidence_conflict(item, turn, validation_scope_registry):
751
- item["_defer_reason"] = "scope_conflict"
720
+ # Exact candidate-bound grounding was already validated above.
721
+ # Do not reinterpret a second mentioned project/product/platform name
722
+ # as contradictory ownership with a registry-only name scan.
752
723
  plan = self.inputs._scope_correction_plan(item, turn, validation_scope_registry)
753
724
  if plan is not None:
754
725
  item.pop("duplicate_memory_id", None)
@@ -937,20 +908,27 @@ class MemoryPlanner:
937
908
  "target_relations", "unknown_target_ids", "candidate_level_target_ids",
938
909
  "scope_correction_plans")]
939
910
  try:
940
- correction_raw = self.model._complete(backend,
941
- "Coverage correction: classify ONLY the supplied unresolved evidence units. "
942
- "Do not re-emit already handled items. Return the same Gate JSON contract.\n"
943
- + gate_prompt([], related_memories=gate_related, scope_background=scope_background,
944
- scope_registry=scope_registry)
945
- + evidence_prompt(missing, batch_index=batch_index, batch_count=batch_count,
946
- todo_witnesses=todo_witnesses)
947
- + "\n"
948
- + COVERAGE_CORRECTION
949
- + "\n"
950
- + COVERAGE_ALREADY_COMPLETED_CORRECTION
951
- + "\nAlready handled candidate IDs: "
952
- + json.dumps([item["candidate_id"] for item in batch_gate["candidates"]]),
953
- system=GATE_SYSTEM, purpose="gate")
911
+ correction_raw = self.model._complete(
912
+ backend,
913
+ coverage_repair_prompt(
914
+ evidence_prompt(
915
+ missing,
916
+ batch_index=batch_index,
917
+ batch_count=batch_count,
918
+ todo_witnesses=todo_witnesses,
919
+ ),
920
+ related_memories=gate_related,
921
+ scope_background=scope_background,
922
+ scope_registry=scope_registry,
923
+ already_handled_candidate_ids=[
924
+ item["candidate_id"] for item in batch_gate["candidates"]
925
+ ],
926
+ ),
927
+ system=GATE_COVERAGE_SYSTEM,
928
+ purpose="gate",
929
+ metric_stage="gate",
930
+ metric_operation="gate_coverage_repair",
931
+ )
954
932
  correction_raw, correction_bindings = split_semantic_envelope(correction_raw)
955
933
  correction_raw, correction_coverage = split_gate_envelope(correction_raw)
956
934
  correction_gate = parse_gate_output(correction_raw,
@@ -1058,10 +1036,10 @@ class MemoryPlanner:
1058
1036
  self.audit._record_disposition(turn_ref, candidate, "NO_CHANGE", reason="same_turn_duplicate")
1059
1037
  continue
1060
1038
  seen_candidates.add(fingerprint)
1061
- # All model candidates pass the same evidence and Scope boundary.
1062
- if self.inputs._scope_evidence_conflict(candidate, turn, validation_scope_registry):
1063
- self.audit._defer_candidate(turn_ref, candidate, "scope_conflict", scopes=candidate.get("scopes", []))
1064
- continue
1039
+ # Exact candidate-bound model project grounding was already
1040
+ # validated by parse_gate. Do not run a registry-name conflict scan
1041
+ # again here; semantic ownership/implementation roles were reviewed
1042
+ # by the model boundary above.
1065
1043
  candidate_id_key = str(candidate.get("candidate_id", "")).casefold()
1066
1044
  correction_plan = scope_correction_plans.get(candidate_id_key)
1067
1045
  defer_reason = candidate.get("_defer_reason")