memleaf 0.2.41__tar.gz → 0.2.43__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 (235) hide show
  1. {memleaf-0.2.41 → memleaf-0.2.43}/CHANGELOG.md +15 -0
  2. {memleaf-0.2.41 → memleaf-0.2.43}/MANIFEST.in +2 -3
  3. {memleaf-0.2.41/src/memleaf.egg-info → memleaf-0.2.43}/PKG-INFO +4 -12
  4. {memleaf-0.2.41 → memleaf-0.2.43}/README.en.md +3 -11
  5. {memleaf-0.2.41 → memleaf-0.2.43}/README.md +3 -11
  6. {memleaf-0.2.41 → memleaf-0.2.43}/docs/general-processing.md +25 -18
  7. {memleaf-0.2.41 → memleaf-0.2.43}/docs/processing-quality-acceptance.md +10 -2
  8. memleaf-0.2.43/examples/README.md +19 -0
  9. {memleaf-0.2.41 → memleaf-0.2.43}/pyproject.toml +1 -1
  10. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/__init__.py +1 -1
  11. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/config.py +5 -1
  12. memleaf-0.2.43/src/memleaf/extraction_budget.py +280 -0
  13. memleaf-0.2.43/src/memleaf/extraction_capability.py +114 -0
  14. memleaf-0.2.43/src/memleaf/extraction_work_state.py +337 -0
  15. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/hermes_provider/plugin.yaml +1 -1
  16. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/llm/base.py +24 -1
  17. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/llm/openai_compatible.py +11 -1
  18. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/llm/router.py +158 -4
  19. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/memory_commit.py +71 -12
  20. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/process_jobs.py +82 -14
  21. memleaf-0.2.43/src/memleaf/processing.py +485 -0
  22. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/single_pass_memory_planner.py +6 -5
  23. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/single_pass_plan.py +37 -16
  24. {memleaf-0.2.41 → memleaf-0.2.43/src/memleaf.egg-info}/PKG-INFO +4 -12
  25. memleaf-0.2.43/src/memleaf.egg-info/SOURCES.txt +109 -0
  26. memleaf-0.2.41/IMPLEMENTATION_PLAN.md +0 -143
  27. memleaf-0.2.41/RELEASE_CHECKLIST.md +0 -97
  28. memleaf-0.2.41/examples/README.md +0 -35
  29. memleaf-0.2.41/examples/live_core_lifecycle_acceptance.py +0 -156
  30. memleaf-0.2.41/examples/live_processing_acceptance.py +0 -126
  31. memleaf-0.2.41/src/memleaf/processing.py +0 -263
  32. memleaf-0.2.41/src/memleaf.egg-info/SOURCES.txt +0 -227
  33. memleaf-0.2.41/tests/__init__.py +0 -0
  34. memleaf-0.2.41/tests/hermes_provider_support.py +0 -189
  35. memleaf-0.2.41/tests/semantic_fixtures.py +0 -103
  36. memleaf-0.2.41/tests/stage_b1_support.py +0 -110
  37. memleaf-0.2.41/tests/stage_b2a_support.py +0 -139
  38. memleaf-0.2.41/tests/test_admission_noise.py +0 -1169
  39. memleaf-0.2.41/tests/test_audit_followup_v041.py +0 -496
  40. memleaf-0.2.41/tests/test_automatic_duplicate_noop_collision.py +0 -126
  41. memleaf-0.2.41/tests/test_b3_activation.py +0 -131
  42. memleaf-0.2.41/tests/test_b3_closeout.py +0 -302
  43. memleaf-0.2.41/tests/test_b3_planning_context.py +0 -74
  44. memleaf-0.2.41/tests/test_b3_scope_provenance.py +0 -187
  45. memleaf-0.2.41/tests/test_b3_single_pass_memory_planner.py +0 -313
  46. memleaf-0.2.41/tests/test_b3_single_pass_plan.py +0 -436
  47. memleaf-0.2.41/tests/test_batch_review.py +0 -215
  48. memleaf-0.2.41/tests/test_batch_review_integration.py +0 -146
  49. memleaf-0.2.41/tests/test_candidate_polarity.py +0 -220
  50. memleaf-0.2.41/tests/test_codex_install.py +0 -339
  51. memleaf-0.2.41/tests/test_codex_native_cli.py +0 -90
  52. memleaf-0.2.41/tests/test_config_migrations_v028.py +0 -60
  53. memleaf-0.2.41/tests/test_context_budget.py +0 -259
  54. memleaf-0.2.41/tests/test_conversation_only.py +0 -111
  55. memleaf-0.2.41/tests/test_credential_safety.py +0 -98
  56. memleaf-0.2.41/tests/test_cross_host_acceptance.py +0 -143
  57. memleaf-0.2.41/tests/test_cross_turn_dedupe_regressions.py +0 -194
  58. memleaf-0.2.41/tests/test_due_date_grounding.py +0 -123
  59. memleaf-0.2.41/tests/test_due_date_grounding_retry.py +0 -136
  60. memleaf-0.2.41/tests/test_email_actionable_coverage.py +0 -407
  61. memleaf-0.2.41/tests/test_evidence_budget.py +0 -192
  62. memleaf-0.2.41/tests/test_evidence_retention_policy.py +0 -450
  63. memleaf-0.2.41/tests/test_external_source_dates.py +0 -259
  64. memleaf-0.2.41/tests/test_extraction_quality_regressions.py +0 -752
  65. memleaf-0.2.41/tests/test_gate_capacity.py +0 -588
  66. memleaf-0.2.41/tests/test_gate_output_protocol_v041.py +0 -222
  67. memleaf-0.2.41/tests/test_gate_output_references_v041.py +0 -49
  68. memleaf-0.2.41/tests/test_gate_protocol_matrix_v041.py +0 -112
  69. memleaf-0.2.41/tests/test_gate_schema_repair.py +0 -200
  70. memleaf-0.2.41/tests/test_gate_scope_latency_v038.py +0 -201
  71. memleaf-0.2.41/tests/test_general_evidence_admission.py +0 -874
  72. memleaf-0.2.41/tests/test_general_tool_provenance.py +0 -62
  73. memleaf-0.2.41/tests/test_global_todo_acceptance.py +0 -291
  74. memleaf-0.2.41/tests/test_global_todo_query_no_write.py +0 -459
  75. memleaf-0.2.41/tests/test_global_todo_retrieval.py +0 -112
  76. memleaf-0.2.41/tests/test_hermes_native_registration.py +0 -144
  77. memleaf-0.2.41/tests/test_hermes_provider.py +0 -9
  78. memleaf-0.2.41/tests/test_hermes_provider_01.py +0 -624
  79. memleaf-0.2.41/tests/test_hermes_provider_02.py +0 -705
  80. memleaf-0.2.41/tests/test_hermes_provider_03.py +0 -663
  81. memleaf-0.2.41/tests/test_hermes_provider_04.py +0 -331
  82. memleaf-0.2.41/tests/test_hermes_runtime_install.py +0 -674
  83. memleaf-0.2.41/tests/test_hermes_stdio_transport.py +0 -129
  84. memleaf-0.2.41/tests/test_hermes_transport_evidence.py +0 -92
  85. memleaf-0.2.41/tests/test_hermes_windows_subprocess.py +0 -43
  86. memleaf-0.2.41/tests/test_host_events.py +0 -839
  87. memleaf-0.2.41/tests/test_host_runtime_contract.py +0 -111
  88. memleaf-0.2.41/tests/test_inspection_state_v028.py +0 -33
  89. memleaf-0.2.41/tests/test_install.py +0 -431
  90. memleaf-0.2.41/tests/test_long_run_hygiene.py +0 -179
  91. memleaf-0.2.41/tests/test_maintenance_v2.py +0 -1412
  92. memleaf-0.2.41/tests/test_model_discovery.py +0 -296
  93. memleaf-0.2.41/tests/test_model_invalid_response_metrics_v041.py +0 -87
  94. memleaf-0.2.41/tests/test_model_owned_fields.py +0 -255
  95. memleaf-0.2.41/tests/test_new_scope_source_grounding.py +0 -234
  96. memleaf-0.2.41/tests/test_p3_summary_batch.py +0 -233
  97. memleaf-0.2.41/tests/test_partial_retry_idempotency.py +0 -330
  98. memleaf-0.2.41/tests/test_phase2_model_decisions.py +0 -337
  99. memleaf-0.2.41/tests/test_process_jobs.py +0 -100
  100. memleaf-0.2.41/tests/test_process_owner_locking.py +0 -96
  101. memleaf-0.2.41/tests/test_processing_contract_v026.py +0 -329
  102. memleaf-0.2.41/tests/test_processing_observability_concurrency.py +0 -362
  103. memleaf-0.2.41/tests/test_prompt_role_slim_v040.py +0 -106
  104. memleaf-0.2.41/tests/test_provider_neutral_thinking_v039.py +0 -246
  105. memleaf-0.2.41/tests/test_pypi_install.py +0 -284
  106. memleaf-0.2.41/tests/test_read_only_deferred_isolation.py +0 -190
  107. memleaf-0.2.41/tests/test_retrieval_gate.py +0 -183
  108. memleaf-0.2.41/tests/test_retrieval_v2.py +0 -203
  109. memleaf-0.2.41/tests/test_review_source_context.py +0 -46
  110. memleaf-0.2.41/tests/test_revision_digest.py +0 -49
  111. memleaf-0.2.41/tests/test_session_lineage.py +0 -421
  112. memleaf-0.2.41/tests/test_shared_memory_refactor.py +0 -257
  113. memleaf-0.2.41/tests/test_source_neutral_todos_v028.py +0 -49
  114. memleaf-0.2.41/tests/test_stage_a.py +0 -430
  115. memleaf-0.2.41/tests/test_stage_b1.py +0 -1151
  116. memleaf-0.2.41/tests/test_stage_b1_01.py +0 -497
  117. memleaf-0.2.41/tests/test_stage_b2a.py +0 -9
  118. memleaf-0.2.41/tests/test_stage_b2a_01.py +0 -530
  119. memleaf-0.2.41/tests/test_stage_b2a_02.py +0 -674
  120. memleaf-0.2.41/tests/test_stage_b2a_03.py +0 -641
  121. memleaf-0.2.41/tests/test_stage_b2a_04.py +0 -629
  122. memleaf-0.2.41/tests/test_stage_b2a_05.py +0 -317
  123. memleaf-0.2.41/tests/test_stage_b2b.py +0 -609
  124. memleaf-0.2.41/tests/test_stage_b3a_commit.py +0 -426
  125. memleaf-0.2.41/tests/test_stage_b3a_contract.py +0 -283
  126. memleaf-0.2.41/tests/test_stage_b3b_native_context.py +0 -299
  127. memleaf-0.2.41/tests/test_stage_b3b_native_index.py +0 -268
  128. memleaf-0.2.41/tests/test_stage_b3b_scope.py +0 -369
  129. memleaf-0.2.41/tests/test_stage_b3c_retrieval.py +0 -308
  130. memleaf-0.2.41/tests/test_stage_b3d_scope_maintenance.py +0 -594
  131. memleaf-0.2.41/tests/test_stage_c1_mcp.py +0 -808
  132. memleaf-0.2.41/tests/test_stage_c2_init.py +0 -802
  133. memleaf-0.2.41/tests/test_stage_c3_packaging.py +0 -165
  134. memleaf-0.2.41/tests/test_state_layout_v028.py +0 -200
  135. memleaf-0.2.41/tests/test_summary_date_grounding_integration.py +0 -39
  136. memleaf-0.2.41/tests/test_target_reconciliation.py +0 -345
  137. memleaf-0.2.41/tests/test_target_reconciliation_integration.py +0 -50
  138. memleaf-0.2.41/tests/test_todo_state_recovery.py +0 -312
  139. memleaf-0.2.41/tests/test_update_review.py +0 -779
  140. memleaf-0.2.41/tests/test_update_target_recovery.py +0 -1057
  141. memleaf-0.2.41/tests/test_upgrade_preserves_vault.py +0 -108
  142. memleaf-0.2.41/tests/test_v023_scope_correction.py +0 -185
  143. memleaf-0.2.41/tests/test_v2_gate_limits.py +0 -111
  144. memleaf-0.2.41/tests/test_v2_host_flow.py +0 -240
  145. memleaf-0.2.41/tests/test_v2_mcp_flow.py +0 -481
  146. memleaf-0.2.41/tests/test_v2_nomatch_semantics.py +0 -106
  147. memleaf-0.2.41/tests/test_v2_search_gate_acceptance.py +0 -147
  148. memleaf-0.2.41/tests/test_whole_unit_bindings.py +0 -361
  149. memleaf-0.2.41/tests/test_windows_public_mcp_launcher.py +0 -75
  150. {memleaf-0.2.41 → memleaf-0.2.43}/LICENSE +0 -0
  151. {memleaf-0.2.41 → memleaf-0.2.43}/docs/capture-budget-design.md +0 -0
  152. {memleaf-0.2.41 → memleaf-0.2.43}/docs/config-migrations.md +0 -0
  153. {memleaf-0.2.41 → memleaf-0.2.43}/docs/core-refactor.md +0 -0
  154. {memleaf-0.2.41 → memleaf-0.2.43}/docs/evidence-retention.md +0 -0
  155. {memleaf-0.2.41 → memleaf-0.2.43}/docs/gate-evidence-boundary.md +0 -0
  156. {memleaf-0.2.41 → memleaf-0.2.43}/docs/hermes-mcp-runtime.md +0 -0
  157. {memleaf-0.2.41 → memleaf-0.2.43}/docs/v0.2.26-processing-status.md +0 -0
  158. {memleaf-0.2.41 → memleaf-0.2.43}/examples/basic_usage.py +0 -0
  159. {memleaf-0.2.41 → memleaf-0.2.43}/examples/mcp_stdio.ndjson +0 -0
  160. {memleaf-0.2.41 → memleaf-0.2.43}/install.ps1 +0 -0
  161. {memleaf-0.2.41 → memleaf-0.2.43}/install.sh +0 -0
  162. {memleaf-0.2.41 → memleaf-0.2.43}/setup.cfg +0 -0
  163. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/__main__.py +0 -0
  164. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/adapters/__init__.py +0 -0
  165. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/adapters/antigravity.py +0 -0
  166. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/adapters/base.py +0 -0
  167. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/adapters/codex.py +0 -0
  168. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/adapters/hermes.py +0 -0
  169. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/admission.py +0 -0
  170. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/batch_review.py +0 -0
  171. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/budget.py +0 -0
  172. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/capture.py +0 -0
  173. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/cli.py +0 -0
  174. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/compaction.py +0 -0
  175. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/create_coordinator.py +0 -0
  176. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/credentials.py +0 -0
  177. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/evidence_budget.py +0 -0
  178. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/evidence_policy.py +0 -0
  179. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/evidence_structure.py +0 -0
  180. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/evidence_syntax.py +0 -0
  181. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/frontmatter.py +0 -0
  182. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/hermes_provider/README.md +0 -0
  183. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/hermes_provider/__init__.py +0 -0
  184. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/hermes_provider/_mcp_client.py +0 -0
  185. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/hermes_provider/_provider.py +0 -0
  186. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/hermes_provider/_shared.py +0 -0
  187. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/hermes_provider/evidence_budget.py +0 -0
  188. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/hermes_runtime.py +0 -0
  189. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/host_events.py +0 -0
  190. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/host_runtime.py +0 -0
  191. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/inbox.py +0 -0
  192. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/index.py +0 -0
  193. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/inspection.py +0 -0
  194. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/installer.py +0 -0
  195. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/llm/__init__.py +0 -0
  196. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/llm/claude_compatible.py +0 -0
  197. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/llm/gemini.py +0 -0
  198. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/llm/thinking.py +0 -0
  199. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/locking.py +0 -0
  200. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/mcp_server.py +0 -0
  201. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/memory_planner.py +0 -0
  202. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/memory_writer.py +0 -0
  203. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/model_discovery.py +0 -0
  204. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/model_execution.py +0 -0
  205. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/models.py +0 -0
  206. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/native_index.py +0 -0
  207. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/native_registration.py +0 -0
  208. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/parallel_model.py +0 -0
  209. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/planning_context.py +0 -0
  210. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/process_common.py +0 -0
  211. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/process_journal.py +0 -0
  212. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/process_owner.py +0 -0
  213. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/prompts.py +0 -0
  214. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/provenance.py +0 -0
  215. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/recording_policy.py +0 -0
  216. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/redaction.py +0 -0
  217. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/retention.py +0 -0
  218. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/retrieval.py +0 -0
  219. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/retrieval_gate.py +0 -0
  220. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/scope_maintenance.py +0 -0
  221. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/scope_state.py +0 -0
  222. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/service.py +0 -0
  223. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/source_policy.py +0 -0
  224. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/state_layout.py +0 -0
  225. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/summary_batch.py +0 -0
  226. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/target_reconciliation.py +0 -0
  227. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/turn_audit.py +0 -0
  228. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/turn_plan.py +0 -0
  229. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/update_coordinator.py +0 -0
  230. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/update_review.py +0 -0
  231. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/validation.py +0 -0
  232. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/vault.py +0 -0
  233. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf.egg-info/dependency_links.txt +0 -0
  234. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf.egg-info/entry_points.txt +0 -0
  235. {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf.egg-info/top_level.txt +0 -0
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to memleaf are documented here.
4
4
 
5
+ ## 0.2.43 — 2026-09-12
6
+
7
+ - Keep regression tests, live-acceptance helpers, and implementation/release plans in local checkouts rather than the Git tree or source distribution.
8
+ - Narrow hosted CI to package build and installed-command smoke checks while retaining the existing release metadata checks and GitHub/PyPI publication workflow.
9
+
10
+ ## 0.2.42 — 2026-09-12
11
+
12
+ - Rework ordinary automatic extraction around the unified `b3-single-pass-v1` planner. The current visible user/assistant turn is the only new-fact evidence; existing local/native memories remain comparison context. One structured model stage decides CREATE / UPDATE / NO_CHANGE / DEFERRED, with deterministic Core validation and at most one bounded format/structure repair.
13
+ - Make the latency-critical `single_pass` stage non-thinking by default and use bounded structured output for supported OpenAI-compatible routes. Fixed safe API routes enforce a six-second primary request cap, an eight-second preparation-plus-model window, a ten-second total turn deadline, and rejection of late results before commit; no unmeasured P50/P95 claim is made.
14
+ - Persist background extraction request/time budgets by process-job and turn identity so worker restarts cannot regain consumed attempts or wall time. Damaged budget/job state, invalid ownership/order, and wall-clock rollback fail closed instead of reopening a fresh extraction budget.
15
+ - Commit automatic processing one complete turn at a time and re-read durable Markdown/journal state before planning the next turn. Frozen-turn recovery preserves already committed work without replaying model calls/history, while one process invocation drains at most four claimed turns and exposes the remaining backlog explicitly.
16
+ - Move compaction/maintenance out of the `process()` / `remember()` extraction critical path, keep explicit no-write instructions as deterministic zero-model-call settlements, and retain structural-only model telemetry without prompt/response bodies or credentials.
17
+ - Separate B3 protocol compatibility from strict transport deadline safety. Host/Python callbacks can use the same B3 semantic protocol without falsely claiming hard cancellation; mixed auto routes remain fail-closed, `single_pass` forbids hidden host-to-API fallback, and explicit `remember()` keeps its existing bounded validator retry semantics.
18
+ - Preserve the product architecture: Markdown under `knowledge/` remains the active source of truth, `history/` remains historical state, permanent memory stays globally shared across agents using the same Vault, and no database, Redis, vector service, resident daemon, or local-model dependency is introduced. The release candidate passed Linux Python 3.11/3.12/3.13, Windows Python 3.11/3.12/3.13, macOS Python 3.11/3.13, native Codex Windows/macOS, wheel/sdist, installed-entry-point, and full unittest CI; real-model quality/latency acceptance remains a separate user-run step.
19
+
5
20
  ## 0.2.41 — 2026-09-11
6
21
 
7
22
  - Promote the B3 single-pass automatic memory planner for explicitly `single_pass_safe` API backends. Core prepares bounded local retrieval/context once, one semantic planner stage decides CREATE / UPDATE / NO_CHANGE / DEFERRED, deterministic validation remains authoritative, and model-output repair is bounded to at most one retry (`max_attempts=2`). Host/custom/host-backed providers keep the proven P3 fallback, while explicit `remember` retains its existing single-summary path.
@@ -4,9 +4,8 @@ include install.sh
4
4
  include install.ps1
5
5
  include README.md
6
6
  include README.en.md
7
- include RELEASE_CHECKLIST.md
8
- include IMPLEMENTATION_PLAN.md
9
- recursive-include tests *.py
10
7
  recursive-include examples *.md *.ndjson *.py
8
+ recursive-exclude examples *_acceptance.py
9
+ prune tests
11
10
 
12
11
  include docs/*.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memleaf
3
- Version: 0.2.41
3
+ Version: 0.2.43
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.41。**
26
+ > **版本:0.2.43。**
27
27
  > 记忆只从用户与 Agent 的可见对话提炼。Agent 已在回复中整理的事实、项目进展和明确待办可作为来源;邮件、附件、网页和终端等工具原文不进入记忆提炼。模型负责保留原话中的不确定性,现有记忆仅用于比较、去重和更新。Markdown 仍是唯一事实源。
28
28
  > **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
29
29
 
@@ -451,17 +451,9 @@ history:
451
451
  - 选择 API/云端模型后,模型处理输入会离开本机并发送给该提供方;
452
452
  - `context()` 或无宿主绑定的客户端只能获得单页边界,不能宣称具备跨轮硬预算。
453
453
 
454
- ## 开发与验证
454
+ ## 开发与构建
455
455
 
456
- 要求 Python 3.11+。本地运行测试:
457
-
458
- ```bash
459
- PYTHONPATH=src python3.11 -m unittest discover -s tests -p 'test_*.py' -v
460
- python3.11 -m compileall -q src tests examples
461
- git diff --check
462
- ```
463
-
464
- GitHub Actions 覆盖 Linux 与 Windows 的 Python 3.11、3.12、3.13 测试,并验证 wheel/source distribution 构建、源码包测试和 Windows PowerShell 安装器语法。构建发行包需要 `build`:
456
+ 要求 Python 3.11+。GitHub Actions 在 Python 3.12 上构建 wheel 和 source distribution,并对安装后的包、命令入口和版本信息做冒烟检查。构建发行包需要 `build`:
465
457
 
466
458
  ```bash
467
459
  python -m pip install build
@@ -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.41.**
7
+ > **Version: 0.2.43.**
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
 
@@ -434,17 +434,9 @@ Directories are normally created with mode `0700`, and files are stored as plain
434
434
  - If an API or cloud model is selected, model-processing input leaves the machine and is sent to that provider.
435
435
  - `context()` and clients without a host-bound retrieval turn only have per-page limits; they cannot claim cross-turn hard budgets.
436
436
 
437
- ## Development and verification
437
+ ## Development and builds
438
438
 
439
- Python 3.11+ is required. Run locally:
440
-
441
- ```bash
442
- PYTHONPATH=src python3.11 -m unittest discover -s tests -p 'test_*.py' -v
443
- python3.11 -m compileall -q src tests examples
444
- git diff --check
445
- ```
446
-
447
- GitHub Actions covers Python 3.11, 3.12, and 3.13 on Linux and Windows, plus wheel/source-distribution builds, source-archive tests, and PowerShell installer syntax validation. Build packages with:
439
+ Python 3.11+ is required. GitHub Actions builds the wheel and source distribution on Python 3.12, then smoke-checks the installed package, CLI entry points, and version commands. Build packages with:
448
440
 
449
441
  ```bash
450
442
  python -m pip install build
@@ -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.41。**
7
+ > **版本:0.2.43。**
8
8
  > 记忆只从用户与 Agent 的可见对话提炼。Agent 已在回复中整理的事实、项目进展和明确待办可作为来源;邮件、附件、网页和终端等工具原文不进入记忆提炼。模型负责保留原话中的不确定性,现有记忆仅用于比较、去重和更新。Markdown 仍是唯一事实源。
9
9
  > **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
10
10
 
@@ -432,17 +432,9 @@ history:
432
432
  - 选择 API/云端模型后,模型处理输入会离开本机并发送给该提供方;
433
433
  - `context()` 或无宿主绑定的客户端只能获得单页边界,不能宣称具备跨轮硬预算。
434
434
 
435
- ## 开发与验证
435
+ ## 开发与构建
436
436
 
437
- 要求 Python 3.11+。本地运行测试:
438
-
439
- ```bash
440
- PYTHONPATH=src python3.11 -m unittest discover -s tests -p 'test_*.py' -v
441
- python3.11 -m compileall -q src tests examples
442
- git diff --check
443
- ```
444
-
445
- GitHub Actions 覆盖 Linux 与 Windows 的 Python 3.11、3.12、3.13 测试,并验证 wheel/source distribution 构建、源码包测试和 Windows PowerShell 安装器语法。构建发行包需要 `build`:
437
+ 要求 Python 3.11+。GitHub Actions 在 Python 3.12 上构建 wheel 和 source distribution,并对安装后的包、命令入口和版本信息做冒烟检查。构建发行包需要 `build`:
446
438
 
447
439
  ```bash
448
440
  python -m pip install build
@@ -122,11 +122,15 @@ the preview. There is deliberately no apply-preview command.
122
122
 
123
123
  ## Verification boundaries
124
124
 
125
+ The regression suite and acceptance helpers referenced here are maintained
126
+ locally and are not included in this repository or its source distribution.
127
+ Remote CI validates package construction and installed command entry points only.
128
+
125
129
  Deterministic backends test schema and filesystem behavior, not hosted-model
126
- accuracy. tests/semantic_fixtures.py adapts prescribed old Gate judgments to the
127
- quote protocol for update/history/maintenance tests. It does not run in product
128
- code and is not proof those judgments are semantically sound. New adversarial
129
- protocol tests choose their quotations explicitly. No model score or absolute
130
+ accuracy. The local-only `tests/semantic_fixtures.py` adapts prescribed old Gate
131
+ judgments to the quote protocol for update/history/maintenance tests. It does
132
+ not run in product code and is not proof those judgments are semantically sound.
133
+ New adversarial protocol tests choose their quotations explicitly. No model score or absolute
130
134
  "all languages/all scenarios" accuracy claim is made.
131
135
 
132
136
  Before release, separately inspect live Model Route behavior on held-out real
@@ -142,20 +146,23 @@ that acceptance.
142
146
  Windows processing-owner liveness is queried through a process handle with
143
147
  SYNCHRONIZE access, never by sending a signal. Shared-Vault writes use a
144
148
  native byte-range lock between processes, not only a Python thread mutex.
145
- Native child-process tests run on Linux, macOS and Windows.
146
-
147
- The full Python suite runs on all three OS families. Only the `install.sh`
148
- shell-harness class is POSIX-only; Windows retains installation, upgrade,
149
- PowerShell syntax, host lifecycle and native Codex acceptance. Test launchers
150
- use native `.cmd` wrappers on Windows. Byte-preservation assertions compare
151
- actual before/after bytes, including CRLF. POSIX mode-bit checks are conditional:
152
- Windows file privacy follows the Vault directory's inherited ACLs, and Unix
153
- `0600` must not be interpreted as proof of a Windows owner-only DACL.
154
-
155
- Real-model acceptance requires an explicit model route in repository Actions:
156
- secret `MEMLEAF_LIVE_MODEL_TOKEN` and variables `MEMLEAF_LIVE_BASE_URL` and
157
- `MEMLEAF_LIVE_MODEL`. Missing configuration is a blocked acceptance result,
158
- not a passing semantic test. Never publish based only on deterministic mocks.
149
+ The local-only Python suite is intended to run on all three OS families; remote
150
+ CI does not exercise it. Native child-process tests cover Linux, macOS and
151
+ Windows when run locally.
152
+
153
+ Only the `install.sh` shell-harness class is POSIX-only; local Windows checks
154
+ cover installation, upgrade, PowerShell syntax, host lifecycle and native Codex
155
+ acceptance. Test launchers use native `.cmd` wrappers on Windows.
156
+ Byte-preservation assertions compare actual before/after bytes, including CRLF.
157
+ POSIX mode-bit checks are conditional: Windows file privacy follows the Vault
158
+ directory's inherited ACLs, and Unix `0600` must not be interpreted as proof of a
159
+ Windows owner-only DACL.
160
+
161
+ Real-model acceptance requires an explicitly configured model route using
162
+ `MEMLEAF_LIVE_MODEL_TOKEN`, `MEMLEAF_LIVE_BASE_URL`, and `MEMLEAF_LIVE_MODEL`.
163
+ Live acceptance tooling is maintained locally and is not run by remote CI.
164
+ Missing configuration is a blocked acceptance result, not a passing semantic
165
+ test. Never publish based only on deterministic mocks.
159
166
 
160
167
  ## Capture policy
161
168
 
@@ -2,6 +2,11 @@
2
2
 
3
3
  This document defines acceptance for automatic memory extraction without treating one validation layer as proof of another.
4
4
 
5
+ The deterministic suite and acceptance helpers described below are maintained
6
+ locally and are not committed or shipped. Remote CI currently validates package
7
+ construction and installed command entry points only; the layers below are not
8
+ automatically exercised by that workflow.
9
+
5
10
  ## Invariants
6
11
 
7
12
  - Markdown under `knowledge/` remains the active-memory source of truth; `history/` remains historical state.
@@ -45,9 +50,12 @@ Required checks include:
45
50
 
46
51
  Passing this layer proves only the deterministic contracts under test.
47
52
 
48
- ### 2. Cross-platform CI and MCP/background-runtime verification
53
+ ### 2. Cross-platform local regression and MCP/background-runtime verification
49
54
 
50
- Run the repository's supported OS matrix and the process-job/MCP tests. Verify that detached worker status exposes the bounded metrics, reruns aggregate metrics safely, and no model call is made while the Vault write lock is held.
55
+ Run the maintained local-only test suite on supported OSes and the
56
+ process-job/MCP tests. Verify that detached worker status exposes the bounded
57
+ metrics, reruns aggregate metrics safely, and no model call is made while the
58
+ Vault write lock is held.
51
59
 
52
60
  Passing this layer proves the tested runtime paths and platforms. It does not prove that a real model will always extract every business fact.
53
61
 
@@ -0,0 +1,19 @@
1
+ # Examples
2
+
3
+ `basic_usage.py` runs entirely offline. Without `--vault` it creates a
4
+ temporary vault; pass a directory explicitly when you want to inspect the
5
+ generated Markdown after the process exits.
6
+
7
+ The example shows a lightweight `context()` directory followed by an explicit
8
+ `read_page()` call for the selected entry. Directory results contain no body;
9
+ long bodies can be read in subsequent pages using `next_offset` and `version`.
10
+
11
+ The `mcp_stdio.ndjson` file contains one legacy initialization request and one
12
+ modern discovery request. Pipe it to the stdio adapter with an explicit vault:
13
+
14
+ ```sh
15
+ python -m memleaf.mcp_server --vault <your-vault> < examples/mcp_stdio.ndjson
16
+ ```
17
+
18
+ The NDJSON file is request-only and intentionally contains no tool call,
19
+ network endpoint, local path, or secret.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memleaf"
7
- version = "0.2.41"
7
+ version = "0.2.43"
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.41"
3
+ __version__ = "0.2.43"
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,9 +19,13 @@ 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")
22
+ THINKING_PURPOSES = ("gate", "summarize", "compact", "single_pass")
23
23
  THINKING_MODES = frozenset({"default", "disabled", "low", "high", "max"})
24
24
  DEFAULT_THINKING = {purpose: "low" for purpose in THINKING_PURPOSES}
25
+ # Unified extraction is latency-sensitive and keeps deterministic validation in
26
+ # Core. Make the single-pass path non-thinking by default without changing
27
+ # the established policy for maintenance/legacy stages.
28
+ DEFAULT_THINKING["single_pass"] = "disabled"
25
29
 
26
30
 
27
31
  def _normalize_request_timeout(value: Any) -> int | float:
@@ -0,0 +1,280 @@
1
+ """Latency and outbound-request budget for unified memory extraction.
2
+
3
+ The budget is deliberately attached to one logical single-pass turn instead
4
+ of to an HTTP adapter. That keeps the product invariants (at most two actual
5
+ model requests and a bounded end-to-end turn) independent from provider-
6
+ specific prompt/transport details.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import math
11
+ import time
12
+ from collections.abc import Callable
13
+ from typing import Any, Mapping
14
+
15
+ from .llm import ModelError
16
+
17
+
18
+ MAX_MODEL_REQUESTS = 2
19
+ TARGET_TOTAL_SECONDS = 10.0
20
+ MODEL_TIME_BUDGET_SECONDS = 8.0
21
+ PRIMARY_REQUEST_MAX_SECONDS = 6.0
22
+
23
+
24
+ class SinglePassBudgetBackend:
25
+ """Wrap one safe backend with the extraction request/time budget.
26
+
27
+ ``single_pass_safe`` routes are already constrained so one ``complete()``
28
+ maps to one provider request (no host->API fallback). The wrapper
29
+ therefore makes the two-call limit a true outbound-request limit for the
30
+ production B3 route, not merely a parser-attempt count.
31
+
32
+ ``deadline`` is absolute in the supplied monotonic clock. Supplying it is
33
+ what lets preparation time consume the same turn budget instead of
34
+ starting a fresh eight-second clock only when the HTTP call begins.
35
+
36
+ ``reserve_request`` optionally persists the request ordinal before the
37
+ outbound call. Background workers use it so a process restart cannot
38
+ reopen attempts already consumed by the same durable work item.
39
+ """
40
+
41
+ single_pass_safe = True
42
+
43
+ def __init__(
44
+ self,
45
+ backend: Any,
46
+ *,
47
+ clock: Any = time.monotonic,
48
+ deadline: float | None = None,
49
+ reserve_request: Callable[[], int | None] | None = None,
50
+ ):
51
+ if not hasattr(backend, "complete"):
52
+ raise TypeError("single-pass backend must expose complete()")
53
+ if reserve_request is not None and not callable(reserve_request):
54
+ raise TypeError("reserve_request must be callable")
55
+ self._backend = backend
56
+ self._clock = clock
57
+ self._started = float(clock())
58
+ self._deadline = (
59
+ self._started + MODEL_TIME_BUDGET_SECONDS
60
+ if deadline is None
61
+ else float(deadline)
62
+ )
63
+ self._requests = 0
64
+ self._reserve_request = reserve_request
65
+
66
+ @property
67
+ def provider(self) -> str:
68
+ return str(getattr(self._backend, "provider", "unknown"))
69
+
70
+ @property
71
+ def model(self) -> str:
72
+ return str(getattr(self._backend, "model", "unknown"))
73
+
74
+ @property
75
+ def parallel_safe(self) -> bool:
76
+ return getattr(self._backend, "parallel_safe", False) is True
77
+
78
+ @property
79
+ def structured_batch_safe(self) -> bool:
80
+ return getattr(self._backend, "structured_batch_safe", False) is True
81
+
82
+ @property
83
+ def request_count(self) -> int:
84
+ return self._requests
85
+
86
+ def _remaining(self) -> float:
87
+ return self._deadline - float(self._clock())
88
+
89
+ def _request_ordinal(self, *, purpose: str) -> int:
90
+ if self._requests >= MAX_MODEL_REQUESTS:
91
+ raise ModelError(
92
+ "single-pass extraction budget exhausted",
93
+ code="model_timeout",
94
+ stage=purpose or "single_pass",
95
+ )
96
+ if self._reserve_request is None:
97
+ return self._requests + 1
98
+ try:
99
+ ordinal = self._reserve_request()
100
+ except Exception as error:
101
+ raise ModelError(
102
+ "single-pass extraction request budget cannot be reserved",
103
+ code="model_timeout",
104
+ stage=purpose or "single_pass",
105
+ ) from error
106
+ if type(ordinal) is not int or not 1 <= ordinal <= MAX_MODEL_REQUESTS:
107
+ raise ModelError(
108
+ "single-pass extraction budget exhausted",
109
+ code="model_timeout",
110
+ stage=purpose or "single_pass",
111
+ )
112
+ return ordinal
113
+
114
+ def complete(
115
+ self,
116
+ prompt: str,
117
+ *,
118
+ system: str = "",
119
+ purpose: str = "",
120
+ temperature: float = 0.0,
121
+ ) -> str:
122
+ remaining = self._remaining()
123
+ if remaining <= 0:
124
+ raise ModelError(
125
+ "single-pass extraction budget exhausted",
126
+ code="model_timeout",
127
+ stage=purpose or "single_pass",
128
+ )
129
+ ordinal = self._request_ordinal(purpose=purpose)
130
+ cap = min(
131
+ PRIMARY_REQUEST_MAX_SECONDS if ordinal == 1 else remaining,
132
+ remaining,
133
+ )
134
+ if cap <= 0:
135
+ raise ModelError(
136
+ "single-pass extraction budget exhausted",
137
+ code="model_timeout",
138
+ stage=purpose or "single_pass",
139
+ )
140
+ # Count locally only after the durable reservation succeeded. A kill
141
+ # after this point still leaves the persistent ordinal consumed.
142
+ self._requests += 1
143
+
144
+ set_timeout = getattr(self._backend, "set_call_timeout", None)
145
+ clear_timeout = getattr(self._backend, "clear_call_timeout", None)
146
+ if callable(set_timeout):
147
+ set_timeout(cap)
148
+ try:
149
+ value = self._backend.complete(
150
+ prompt,
151
+ system=system,
152
+ purpose=purpose,
153
+ temperature=temperature,
154
+ )
155
+ finally:
156
+ if callable(clear_timeout):
157
+ try:
158
+ clear_timeout()
159
+ except Exception:
160
+ pass
161
+
162
+ # A callback/custom transport may ignore the timeout hook. Such a late
163
+ # result must never become writable merely because it eventually returned.
164
+ if self._remaining() < 0:
165
+ raise ModelError(
166
+ "single-pass extraction result arrived after deadline",
167
+ code="model_timeout",
168
+ stage=purpose or "single_pass",
169
+ )
170
+ return value
171
+
172
+ def consume_call_metrics(self) -> dict[str, Any]:
173
+ consume = getattr(self._backend, "consume_call_metrics", None)
174
+ if not callable(consume):
175
+ return {}
176
+ try:
177
+ value = consume()
178
+ except Exception:
179
+ return {}
180
+ return dict(value) if isinstance(value, Mapping) else {}
181
+
182
+
183
+ class ExtractionWorkBudget:
184
+ """One monotonic budget beginning before preparation for a visible turn.
185
+
186
+ The first eight seconds are available to preparation plus model work. The
187
+ remaining two seconds are reserved for deterministic validation/commit.
188
+ If the ten-second total deadline has already elapsed, the turn is not
189
+ allowed to enter the mutation boundary.
190
+
191
+ ``elapsed_seconds`` restores wall time already consumed by the same durable
192
+ background work item before a worker restart. The persisted ledger uses a
193
+ wall clock only to derive that cross-process elapsed interval; after this
194
+ object is constructed, every new deadline check uses the supplied monotonic
195
+ clock in the current process.
196
+ """
197
+
198
+ def __init__(
199
+ self,
200
+ *,
201
+ clock: Any = time.monotonic,
202
+ elapsed_seconds: float = 0.0,
203
+ ):
204
+ if (
205
+ isinstance(elapsed_seconds, bool)
206
+ or not isinstance(elapsed_seconds, (int, float))
207
+ or not math.isfinite(float(elapsed_seconds))
208
+ or float(elapsed_seconds) < 0
209
+ ):
210
+ raise ValueError("elapsed_seconds must be a finite non-negative number")
211
+ self._clock = clock
212
+ current = float(clock())
213
+ elapsed = float(elapsed_seconds)
214
+ self._started = current - elapsed
215
+ self._model_deadline = current + (MODEL_TIME_BUDGET_SECONDS - elapsed)
216
+ self._total_deadline = current + (TARGET_TOTAL_SECONDS - elapsed)
217
+
218
+ @property
219
+ def started(self) -> float:
220
+ return self._started
221
+
222
+ @property
223
+ def model_deadline(self) -> float:
224
+ return self._model_deadline
225
+
226
+ @property
227
+ def total_deadline(self) -> float:
228
+ return self._total_deadline
229
+
230
+ def remaining_total(self) -> float:
231
+ return self._total_deadline - float(self._clock())
232
+
233
+ def wrap_backend(
234
+ self,
235
+ backend: Any,
236
+ *,
237
+ reserve_request: Callable[[], int | None] | None = None,
238
+ ) -> SinglePassBudgetBackend:
239
+ return budget_single_pass_backend(
240
+ backend,
241
+ clock=self._clock,
242
+ deadline=self._model_deadline,
243
+ reserve_request=reserve_request,
244
+ )
245
+
246
+ def ensure_before_commit(self) -> None:
247
+ if self.remaining_total() <= 0:
248
+ raise ModelError(
249
+ "single-pass extraction exceeded total deadline before commit",
250
+ code="model_timeout",
251
+ stage="single_pass",
252
+ )
253
+
254
+
255
+ def budget_single_pass_backend(
256
+ backend: Any,
257
+ *,
258
+ clock: Any = time.monotonic,
259
+ deadline: float | None = None,
260
+ reserve_request: Callable[[], int | None] | None = None,
261
+ ) -> SinglePassBudgetBackend:
262
+ if isinstance(backend, SinglePassBudgetBackend):
263
+ return backend
264
+ return SinglePassBudgetBackend(
265
+ backend,
266
+ clock=clock,
267
+ deadline=deadline,
268
+ reserve_request=reserve_request,
269
+ )
270
+
271
+
272
+ __all__ = [
273
+ "MAX_MODEL_REQUESTS",
274
+ "MODEL_TIME_BUDGET_SECONDS",
275
+ "PRIMARY_REQUEST_MAX_SECONDS",
276
+ "TARGET_TOTAL_SECONDS",
277
+ "ExtractionWorkBudget",
278
+ "SinglePassBudgetBackend",
279
+ "budget_single_pass_backend",
280
+ ]