memleaf 0.2.41__tar.gz → 0.2.42__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 (243) hide show
  1. {memleaf-0.2.41 → memleaf-0.2.42}/CHANGELOG.md +10 -0
  2. {memleaf-0.2.41/src/memleaf.egg-info → memleaf-0.2.42}/PKG-INFO +2 -2
  3. {memleaf-0.2.41 → memleaf-0.2.42}/README.en.md +1 -1
  4. {memleaf-0.2.41 → memleaf-0.2.42}/README.md +1 -1
  5. {memleaf-0.2.41 → memleaf-0.2.42}/pyproject.toml +1 -1
  6. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/__init__.py +1 -1
  7. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/config.py +5 -1
  8. memleaf-0.2.42/src/memleaf/extraction_budget.py +280 -0
  9. memleaf-0.2.42/src/memleaf/extraction_capability.py +114 -0
  10. memleaf-0.2.42/src/memleaf/extraction_work_state.py +337 -0
  11. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/hermes_provider/plugin.yaml +1 -1
  12. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/llm/base.py +24 -1
  13. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/llm/openai_compatible.py +11 -1
  14. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/llm/router.py +158 -4
  15. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/memory_commit.py +71 -12
  16. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/process_jobs.py +82 -14
  17. memleaf-0.2.42/src/memleaf/processing.py +485 -0
  18. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/single_pass_memory_planner.py +6 -5
  19. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/single_pass_plan.py +37 -16
  20. {memleaf-0.2.41 → memleaf-0.2.42/src/memleaf.egg-info}/PKG-INFO +2 -2
  21. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf.egg-info/SOURCES.txt +14 -1
  22. {memleaf-0.2.41 → memleaf-0.2.42}/tests/semantic_fixtures.py +18 -6
  23. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_b3_single_pass_memory_planner.py +4 -2
  24. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_b3_single_pass_plan.py +30 -5
  25. memleaf-0.2.42/tests/test_backlog_bound_v2.py +81 -0
  26. memleaf-0.2.42/tests/test_explicit_remember_budget_v2.py +91 -0
  27. memleaf-0.2.42/tests/test_extraction_unified_v2.py +244 -0
  28. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_gate_scope_latency_v038.py +5 -2
  29. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_long_run_hygiene.py +6 -6
  30. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_maintenance_v2.py +14 -12
  31. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_process_jobs.py +51 -0
  32. memleaf-0.2.42/tests/test_process_jobs_backlog_v2.py +51 -0
  33. memleaf-0.2.42/tests/test_process_jobs_state_corruption_v2.py +107 -0
  34. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_read_only_deferred_isolation.py +8 -4
  35. memleaf-0.2.42/tests/test_read_only_zero_call_v2.py +116 -0
  36. memleaf-0.2.42/tests/test_single_pass_prompt_slim_v2.py +73 -0
  37. memleaf-0.2.42/tests/test_single_pass_protocol_capability_v2.py +186 -0
  38. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b3a_commit.py +25 -8
  39. memleaf-0.2.42/tests/test_worker_owner_state_v2.py +87 -0
  40. memleaf-0.2.42/tests/test_worker_restart_budget_v2.py +259 -0
  41. memleaf-0.2.41/src/memleaf/processing.py +0 -263
  42. {memleaf-0.2.41 → memleaf-0.2.42}/IMPLEMENTATION_PLAN.md +0 -0
  43. {memleaf-0.2.41 → memleaf-0.2.42}/LICENSE +0 -0
  44. {memleaf-0.2.41 → memleaf-0.2.42}/MANIFEST.in +0 -0
  45. {memleaf-0.2.41 → memleaf-0.2.42}/RELEASE_CHECKLIST.md +0 -0
  46. {memleaf-0.2.41 → memleaf-0.2.42}/docs/capture-budget-design.md +0 -0
  47. {memleaf-0.2.41 → memleaf-0.2.42}/docs/config-migrations.md +0 -0
  48. {memleaf-0.2.41 → memleaf-0.2.42}/docs/core-refactor.md +0 -0
  49. {memleaf-0.2.41 → memleaf-0.2.42}/docs/evidence-retention.md +0 -0
  50. {memleaf-0.2.41 → memleaf-0.2.42}/docs/gate-evidence-boundary.md +0 -0
  51. {memleaf-0.2.41 → memleaf-0.2.42}/docs/general-processing.md +0 -0
  52. {memleaf-0.2.41 → memleaf-0.2.42}/docs/hermes-mcp-runtime.md +0 -0
  53. {memleaf-0.2.41 → memleaf-0.2.42}/docs/processing-quality-acceptance.md +0 -0
  54. {memleaf-0.2.41 → memleaf-0.2.42}/docs/v0.2.26-processing-status.md +0 -0
  55. {memleaf-0.2.41 → memleaf-0.2.42}/examples/README.md +0 -0
  56. {memleaf-0.2.41 → memleaf-0.2.42}/examples/basic_usage.py +0 -0
  57. {memleaf-0.2.41 → memleaf-0.2.42}/examples/live_core_lifecycle_acceptance.py +0 -0
  58. {memleaf-0.2.41 → memleaf-0.2.42}/examples/live_processing_acceptance.py +0 -0
  59. {memleaf-0.2.41 → memleaf-0.2.42}/examples/mcp_stdio.ndjson +0 -0
  60. {memleaf-0.2.41 → memleaf-0.2.42}/install.ps1 +0 -0
  61. {memleaf-0.2.41 → memleaf-0.2.42}/install.sh +0 -0
  62. {memleaf-0.2.41 → memleaf-0.2.42}/setup.cfg +0 -0
  63. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/__main__.py +0 -0
  64. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/adapters/__init__.py +0 -0
  65. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/adapters/antigravity.py +0 -0
  66. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/adapters/base.py +0 -0
  67. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/adapters/codex.py +0 -0
  68. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/adapters/hermes.py +0 -0
  69. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/admission.py +0 -0
  70. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/batch_review.py +0 -0
  71. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/budget.py +0 -0
  72. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/capture.py +0 -0
  73. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/cli.py +0 -0
  74. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/compaction.py +0 -0
  75. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/create_coordinator.py +0 -0
  76. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/credentials.py +0 -0
  77. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/evidence_budget.py +0 -0
  78. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/evidence_policy.py +0 -0
  79. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/evidence_structure.py +0 -0
  80. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/evidence_syntax.py +0 -0
  81. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/frontmatter.py +0 -0
  82. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/hermes_provider/README.md +0 -0
  83. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/hermes_provider/__init__.py +0 -0
  84. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/hermes_provider/_mcp_client.py +0 -0
  85. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/hermes_provider/_provider.py +0 -0
  86. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/hermes_provider/_shared.py +0 -0
  87. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/hermes_provider/evidence_budget.py +0 -0
  88. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/hermes_runtime.py +0 -0
  89. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/host_events.py +0 -0
  90. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/host_runtime.py +0 -0
  91. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/inbox.py +0 -0
  92. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/index.py +0 -0
  93. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/inspection.py +0 -0
  94. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/installer.py +0 -0
  95. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/llm/__init__.py +0 -0
  96. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/llm/claude_compatible.py +0 -0
  97. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/llm/gemini.py +0 -0
  98. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/llm/thinking.py +0 -0
  99. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/locking.py +0 -0
  100. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/mcp_server.py +0 -0
  101. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/memory_planner.py +0 -0
  102. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/memory_writer.py +0 -0
  103. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/model_discovery.py +0 -0
  104. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/model_execution.py +0 -0
  105. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/models.py +0 -0
  106. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/native_index.py +0 -0
  107. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/native_registration.py +0 -0
  108. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/parallel_model.py +0 -0
  109. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/planning_context.py +0 -0
  110. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/process_common.py +0 -0
  111. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/process_journal.py +0 -0
  112. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/process_owner.py +0 -0
  113. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/prompts.py +0 -0
  114. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/provenance.py +0 -0
  115. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/recording_policy.py +0 -0
  116. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/redaction.py +0 -0
  117. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/retention.py +0 -0
  118. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/retrieval.py +0 -0
  119. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/retrieval_gate.py +0 -0
  120. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/scope_maintenance.py +0 -0
  121. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/scope_state.py +0 -0
  122. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/service.py +0 -0
  123. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/source_policy.py +0 -0
  124. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/state_layout.py +0 -0
  125. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/summary_batch.py +0 -0
  126. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/target_reconciliation.py +0 -0
  127. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/turn_audit.py +0 -0
  128. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/turn_plan.py +0 -0
  129. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/update_coordinator.py +0 -0
  130. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/update_review.py +0 -0
  131. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/validation.py +0 -0
  132. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf/vault.py +0 -0
  133. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf.egg-info/dependency_links.txt +0 -0
  134. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf.egg-info/entry_points.txt +0 -0
  135. {memleaf-0.2.41 → memleaf-0.2.42}/src/memleaf.egg-info/top_level.txt +0 -0
  136. {memleaf-0.2.41 → memleaf-0.2.42}/tests/__init__.py +0 -0
  137. {memleaf-0.2.41 → memleaf-0.2.42}/tests/hermes_provider_support.py +0 -0
  138. {memleaf-0.2.41 → memleaf-0.2.42}/tests/stage_b1_support.py +0 -0
  139. {memleaf-0.2.41 → memleaf-0.2.42}/tests/stage_b2a_support.py +0 -0
  140. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_admission_noise.py +0 -0
  141. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_audit_followup_v041.py +0 -0
  142. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_automatic_duplicate_noop_collision.py +0 -0
  143. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_b3_activation.py +0 -0
  144. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_b3_closeout.py +0 -0
  145. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_b3_planning_context.py +0 -0
  146. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_b3_scope_provenance.py +0 -0
  147. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_batch_review.py +0 -0
  148. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_batch_review_integration.py +0 -0
  149. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_candidate_polarity.py +0 -0
  150. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_codex_install.py +0 -0
  151. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_codex_native_cli.py +0 -0
  152. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_config_migrations_v028.py +0 -0
  153. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_context_budget.py +0 -0
  154. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_conversation_only.py +0 -0
  155. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_credential_safety.py +0 -0
  156. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_cross_host_acceptance.py +0 -0
  157. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_cross_turn_dedupe_regressions.py +0 -0
  158. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_due_date_grounding.py +0 -0
  159. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_due_date_grounding_retry.py +0 -0
  160. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_email_actionable_coverage.py +0 -0
  161. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_evidence_budget.py +0 -0
  162. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_evidence_retention_policy.py +0 -0
  163. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_external_source_dates.py +0 -0
  164. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_extraction_quality_regressions.py +0 -0
  165. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_gate_capacity.py +0 -0
  166. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_gate_output_protocol_v041.py +0 -0
  167. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_gate_output_references_v041.py +0 -0
  168. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_gate_protocol_matrix_v041.py +0 -0
  169. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_gate_schema_repair.py +0 -0
  170. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_general_evidence_admission.py +0 -0
  171. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_general_tool_provenance.py +0 -0
  172. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_global_todo_acceptance.py +0 -0
  173. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_global_todo_query_no_write.py +0 -0
  174. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_global_todo_retrieval.py +0 -0
  175. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_hermes_native_registration.py +0 -0
  176. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_hermes_provider.py +0 -0
  177. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_hermes_provider_01.py +0 -0
  178. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_hermes_provider_02.py +0 -0
  179. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_hermes_provider_03.py +0 -0
  180. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_hermes_provider_04.py +0 -0
  181. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_hermes_runtime_install.py +0 -0
  182. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_hermes_stdio_transport.py +0 -0
  183. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_hermes_transport_evidence.py +0 -0
  184. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_hermes_windows_subprocess.py +0 -0
  185. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_host_events.py +0 -0
  186. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_host_runtime_contract.py +0 -0
  187. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_inspection_state_v028.py +0 -0
  188. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_install.py +0 -0
  189. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_model_discovery.py +0 -0
  190. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_model_invalid_response_metrics_v041.py +0 -0
  191. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_model_owned_fields.py +0 -0
  192. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_new_scope_source_grounding.py +0 -0
  193. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_p3_summary_batch.py +0 -0
  194. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_partial_retry_idempotency.py +0 -0
  195. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_phase2_model_decisions.py +0 -0
  196. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_process_owner_locking.py +0 -0
  197. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_processing_contract_v026.py +0 -0
  198. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_processing_observability_concurrency.py +0 -0
  199. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_prompt_role_slim_v040.py +0 -0
  200. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_provider_neutral_thinking_v039.py +0 -0
  201. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_pypi_install.py +0 -0
  202. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_retrieval_gate.py +0 -0
  203. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_retrieval_v2.py +0 -0
  204. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_review_source_context.py +0 -0
  205. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_revision_digest.py +0 -0
  206. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_session_lineage.py +0 -0
  207. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_shared_memory_refactor.py +0 -0
  208. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_source_neutral_todos_v028.py +0 -0
  209. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_a.py +0 -0
  210. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b1.py +0 -0
  211. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b1_01.py +0 -0
  212. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b2a.py +0 -0
  213. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b2a_01.py +0 -0
  214. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b2a_02.py +0 -0
  215. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b2a_03.py +0 -0
  216. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b2a_04.py +0 -0
  217. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b2a_05.py +0 -0
  218. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b2b.py +0 -0
  219. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b3a_contract.py +0 -0
  220. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b3b_native_context.py +0 -0
  221. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b3b_native_index.py +0 -0
  222. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b3b_scope.py +0 -0
  223. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b3c_retrieval.py +0 -0
  224. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_b3d_scope_maintenance.py +0 -0
  225. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_c1_mcp.py +0 -0
  226. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_c2_init.py +0 -0
  227. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_stage_c3_packaging.py +0 -0
  228. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_state_layout_v028.py +0 -0
  229. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_summary_date_grounding_integration.py +0 -0
  230. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_target_reconciliation.py +0 -0
  231. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_target_reconciliation_integration.py +0 -0
  232. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_todo_state_recovery.py +0 -0
  233. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_update_review.py +0 -0
  234. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_update_target_recovery.py +0 -0
  235. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_upgrade_preserves_vault.py +0 -0
  236. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_v023_scope_correction.py +0 -0
  237. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_v2_gate_limits.py +0 -0
  238. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_v2_host_flow.py +0 -0
  239. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_v2_mcp_flow.py +0 -0
  240. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_v2_nomatch_semantics.py +0 -0
  241. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_v2_search_gate_acceptance.py +0 -0
  242. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_whole_unit_bindings.py +0 -0
  243. {memleaf-0.2.41 → memleaf-0.2.42}/tests/test_windows_public_mcp_launcher.py +0 -0
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to memleaf are documented here.
4
4
 
5
+ ## 0.2.42 — 2026-09-12
6
+
7
+ - 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.
8
+ - 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.
9
+ - 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.
10
+ - 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.
11
+ - 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.
12
+ - 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.
13
+ - 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.
14
+
5
15
  ## 0.2.41 — 2026-09-11
6
16
 
7
17
  - 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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memleaf
3
- Version: 0.2.41
3
+ Version: 0.2.42
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.42。**
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.41.**
7
+ > **Version: 0.2.42.**
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.41。**
7
+ > **版本:0.2.42。**
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.41"
7
+ version = "0.2.42"
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.42"
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
+ ]
@@ -0,0 +1,114 @@
1
+ """Capability checks for unified automatic extraction.
2
+
3
+ Protocol compatibility and hard latency safety are intentionally separate.
4
+ A backend may understand the B3 single-pass contract without being able to
5
+ honor transport-level cancellation/deadlines. Automatic extraction can use
6
+ one semantic protocol in both cases, while Processor applies the strict
7
+ 8/10-second budget only to ``single_pass_safe`` transports.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from .llm import CallableBackend, ModelRouter
14
+
15
+
16
+ def _callable_protocol_override(backend: CallableBackend) -> bool | None:
17
+ """Return a caller-declared protocol override for callback adapters.
18
+
19
+ Raw callbacks default to the unified B3 contract. A callback may set
20
+ ``single_pass_protocol = False`` when it intentionally implements the
21
+ legacy staged test/compatibility protocol. Product code never needs this
22
+ for ordinary host callbacks; the escape hatch prevents capability
23
+ inference from rewriting explicitly versioned compatibility fixtures.
24
+ """
25
+
26
+ callback = getattr(backend, "callback", None)
27
+ value = getattr(callback, "single_pass_protocol", None)
28
+ return value if isinstance(value, bool) else None
29
+
30
+
31
+ def _direct_protocol_capable(backend: Any) -> bool:
32
+ """Return whether one concrete backend can speak the B3 output contract."""
33
+
34
+ if backend is None:
35
+ return False
36
+ if getattr(backend, "single_pass_safe", False) is True:
37
+ return True
38
+ declared = getattr(backend, "single_pass_protocol", None)
39
+ if isinstance(declared, bool):
40
+ return declared
41
+ if isinstance(backend, CallableBackend):
42
+ override = _callable_protocol_override(backend)
43
+ if override is not None:
44
+ return override
45
+ # Raw Python callbacks are adapted by ModelExecutor into
46
+ # CallableBackend. They can consume memleaf's B3 prompt, but are not
47
+ # strict-deadline safe because caller-owned code may ignore
48
+ # timeout/cancellation entirely.
49
+ return True
50
+ return False
51
+
52
+
53
+ def supports_single_pass_protocol(backend: Any) -> bool:
54
+ """Return B3 protocol capability without claiming strict transport SLA."""
55
+
56
+ if _direct_protocol_capable(backend):
57
+ return True
58
+ if not isinstance(backend, ModelRouter):
59
+ return False
60
+
61
+ if backend.mode == "api":
62
+ return _direct_protocol_capable(backend.api)
63
+ if backend.mode == "host":
64
+ return _direct_protocol_capable(backend.host)
65
+
66
+ # Auto mode can expose both routes over its lifetime. Even though the
67
+ # single-pass call itself is pinned against hidden host->API fallback, B3
68
+ # capability remains fail-closed unless every configured reachable route
69
+ # speaks the same protocol. This prevents a later routing-policy change or
70
+ # route selection from silently changing the output contract.
71
+ if backend.host is not None:
72
+ if not _direct_protocol_capable(backend.host):
73
+ return False
74
+ return backend.api is None or _direct_protocol_capable(backend.api)
75
+ return _direct_protocol_capable(backend.api)
76
+
77
+
78
+ def _direct_requires_inline_system(backend: Any) -> bool:
79
+ """Return whether a concrete callback may receive only the prompt value.
80
+
81
+ CallableBackend intentionally supports legacy ``callback(prompt)``
82
+ signatures. In that mode its ``system`` argument cannot reach the caller,
83
+ so B3 must inline the system contract into the prompt rather than silently
84
+ dropping the planner rules. This says nothing about latency safety.
85
+ """
86
+
87
+ return isinstance(backend, CallableBackend)
88
+
89
+
90
+ def requires_inline_single_pass_system(backend: Any) -> bool:
91
+ """Keep B3 instructions visible across prompt-only callback routes."""
92
+
93
+ if _direct_requires_inline_system(backend):
94
+ return True
95
+ if not isinstance(backend, ModelRouter):
96
+ return False
97
+ if backend.mode == "api":
98
+ return _direct_requires_inline_system(backend.api)
99
+ if backend.mode == "host":
100
+ return _direct_requires_inline_system(backend.host)
101
+
102
+ # Keep the prompt self-contained whenever either configured auto route is
103
+ # a prompt-only callback. Capability gating above ensures both routes speak
104
+ # B3 before automatic extraction chooses this protocol.
105
+ return (
106
+ _direct_requires_inline_system(backend.host)
107
+ or _direct_requires_inline_system(backend.api)
108
+ )
109
+
110
+
111
+ __all__ = [
112
+ "requires_inline_single_pass_system",
113
+ "supports_single_pass_protocol",
114
+ ]