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.
- {memleaf-0.2.41 → memleaf-0.2.43}/CHANGELOG.md +15 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/MANIFEST.in +2 -3
- {memleaf-0.2.41/src/memleaf.egg-info → memleaf-0.2.43}/PKG-INFO +4 -12
- {memleaf-0.2.41 → memleaf-0.2.43}/README.en.md +3 -11
- {memleaf-0.2.41 → memleaf-0.2.43}/README.md +3 -11
- {memleaf-0.2.41 → memleaf-0.2.43}/docs/general-processing.md +25 -18
- {memleaf-0.2.41 → memleaf-0.2.43}/docs/processing-quality-acceptance.md +10 -2
- memleaf-0.2.43/examples/README.md +19 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/pyproject.toml +1 -1
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/__init__.py +1 -1
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/config.py +5 -1
- memleaf-0.2.43/src/memleaf/extraction_budget.py +280 -0
- memleaf-0.2.43/src/memleaf/extraction_capability.py +114 -0
- memleaf-0.2.43/src/memleaf/extraction_work_state.py +337 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/hermes_provider/plugin.yaml +1 -1
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/llm/base.py +24 -1
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/llm/openai_compatible.py +11 -1
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/llm/router.py +158 -4
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/memory_commit.py +71 -12
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/process_jobs.py +82 -14
- memleaf-0.2.43/src/memleaf/processing.py +485 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/single_pass_memory_planner.py +6 -5
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/single_pass_plan.py +37 -16
- {memleaf-0.2.41 → memleaf-0.2.43/src/memleaf.egg-info}/PKG-INFO +4 -12
- memleaf-0.2.43/src/memleaf.egg-info/SOURCES.txt +109 -0
- memleaf-0.2.41/IMPLEMENTATION_PLAN.md +0 -143
- memleaf-0.2.41/RELEASE_CHECKLIST.md +0 -97
- memleaf-0.2.41/examples/README.md +0 -35
- memleaf-0.2.41/examples/live_core_lifecycle_acceptance.py +0 -156
- memleaf-0.2.41/examples/live_processing_acceptance.py +0 -126
- memleaf-0.2.41/src/memleaf/processing.py +0 -263
- memleaf-0.2.41/src/memleaf.egg-info/SOURCES.txt +0 -227
- memleaf-0.2.41/tests/__init__.py +0 -0
- memleaf-0.2.41/tests/hermes_provider_support.py +0 -189
- memleaf-0.2.41/tests/semantic_fixtures.py +0 -103
- memleaf-0.2.41/tests/stage_b1_support.py +0 -110
- memleaf-0.2.41/tests/stage_b2a_support.py +0 -139
- memleaf-0.2.41/tests/test_admission_noise.py +0 -1169
- memleaf-0.2.41/tests/test_audit_followup_v041.py +0 -496
- memleaf-0.2.41/tests/test_automatic_duplicate_noop_collision.py +0 -126
- memleaf-0.2.41/tests/test_b3_activation.py +0 -131
- memleaf-0.2.41/tests/test_b3_closeout.py +0 -302
- memleaf-0.2.41/tests/test_b3_planning_context.py +0 -74
- memleaf-0.2.41/tests/test_b3_scope_provenance.py +0 -187
- memleaf-0.2.41/tests/test_b3_single_pass_memory_planner.py +0 -313
- memleaf-0.2.41/tests/test_b3_single_pass_plan.py +0 -436
- memleaf-0.2.41/tests/test_batch_review.py +0 -215
- memleaf-0.2.41/tests/test_batch_review_integration.py +0 -146
- memleaf-0.2.41/tests/test_candidate_polarity.py +0 -220
- memleaf-0.2.41/tests/test_codex_install.py +0 -339
- memleaf-0.2.41/tests/test_codex_native_cli.py +0 -90
- memleaf-0.2.41/tests/test_config_migrations_v028.py +0 -60
- memleaf-0.2.41/tests/test_context_budget.py +0 -259
- memleaf-0.2.41/tests/test_conversation_only.py +0 -111
- memleaf-0.2.41/tests/test_credential_safety.py +0 -98
- memleaf-0.2.41/tests/test_cross_host_acceptance.py +0 -143
- memleaf-0.2.41/tests/test_cross_turn_dedupe_regressions.py +0 -194
- memleaf-0.2.41/tests/test_due_date_grounding.py +0 -123
- memleaf-0.2.41/tests/test_due_date_grounding_retry.py +0 -136
- memleaf-0.2.41/tests/test_email_actionable_coverage.py +0 -407
- memleaf-0.2.41/tests/test_evidence_budget.py +0 -192
- memleaf-0.2.41/tests/test_evidence_retention_policy.py +0 -450
- memleaf-0.2.41/tests/test_external_source_dates.py +0 -259
- memleaf-0.2.41/tests/test_extraction_quality_regressions.py +0 -752
- memleaf-0.2.41/tests/test_gate_capacity.py +0 -588
- memleaf-0.2.41/tests/test_gate_output_protocol_v041.py +0 -222
- memleaf-0.2.41/tests/test_gate_output_references_v041.py +0 -49
- memleaf-0.2.41/tests/test_gate_protocol_matrix_v041.py +0 -112
- memleaf-0.2.41/tests/test_gate_schema_repair.py +0 -200
- memleaf-0.2.41/tests/test_gate_scope_latency_v038.py +0 -201
- memleaf-0.2.41/tests/test_general_evidence_admission.py +0 -874
- memleaf-0.2.41/tests/test_general_tool_provenance.py +0 -62
- memleaf-0.2.41/tests/test_global_todo_acceptance.py +0 -291
- memleaf-0.2.41/tests/test_global_todo_query_no_write.py +0 -459
- memleaf-0.2.41/tests/test_global_todo_retrieval.py +0 -112
- memleaf-0.2.41/tests/test_hermes_native_registration.py +0 -144
- memleaf-0.2.41/tests/test_hermes_provider.py +0 -9
- memleaf-0.2.41/tests/test_hermes_provider_01.py +0 -624
- memleaf-0.2.41/tests/test_hermes_provider_02.py +0 -705
- memleaf-0.2.41/tests/test_hermes_provider_03.py +0 -663
- memleaf-0.2.41/tests/test_hermes_provider_04.py +0 -331
- memleaf-0.2.41/tests/test_hermes_runtime_install.py +0 -674
- memleaf-0.2.41/tests/test_hermes_stdio_transport.py +0 -129
- memleaf-0.2.41/tests/test_hermes_transport_evidence.py +0 -92
- memleaf-0.2.41/tests/test_hermes_windows_subprocess.py +0 -43
- memleaf-0.2.41/tests/test_host_events.py +0 -839
- memleaf-0.2.41/tests/test_host_runtime_contract.py +0 -111
- memleaf-0.2.41/tests/test_inspection_state_v028.py +0 -33
- memleaf-0.2.41/tests/test_install.py +0 -431
- memleaf-0.2.41/tests/test_long_run_hygiene.py +0 -179
- memleaf-0.2.41/tests/test_maintenance_v2.py +0 -1412
- memleaf-0.2.41/tests/test_model_discovery.py +0 -296
- memleaf-0.2.41/tests/test_model_invalid_response_metrics_v041.py +0 -87
- memleaf-0.2.41/tests/test_model_owned_fields.py +0 -255
- memleaf-0.2.41/tests/test_new_scope_source_grounding.py +0 -234
- memleaf-0.2.41/tests/test_p3_summary_batch.py +0 -233
- memleaf-0.2.41/tests/test_partial_retry_idempotency.py +0 -330
- memleaf-0.2.41/tests/test_phase2_model_decisions.py +0 -337
- memleaf-0.2.41/tests/test_process_jobs.py +0 -100
- memleaf-0.2.41/tests/test_process_owner_locking.py +0 -96
- memleaf-0.2.41/tests/test_processing_contract_v026.py +0 -329
- memleaf-0.2.41/tests/test_processing_observability_concurrency.py +0 -362
- memleaf-0.2.41/tests/test_prompt_role_slim_v040.py +0 -106
- memleaf-0.2.41/tests/test_provider_neutral_thinking_v039.py +0 -246
- memleaf-0.2.41/tests/test_pypi_install.py +0 -284
- memleaf-0.2.41/tests/test_read_only_deferred_isolation.py +0 -190
- memleaf-0.2.41/tests/test_retrieval_gate.py +0 -183
- memleaf-0.2.41/tests/test_retrieval_v2.py +0 -203
- memleaf-0.2.41/tests/test_review_source_context.py +0 -46
- memleaf-0.2.41/tests/test_revision_digest.py +0 -49
- memleaf-0.2.41/tests/test_session_lineage.py +0 -421
- memleaf-0.2.41/tests/test_shared_memory_refactor.py +0 -257
- memleaf-0.2.41/tests/test_source_neutral_todos_v028.py +0 -49
- memleaf-0.2.41/tests/test_stage_a.py +0 -430
- memleaf-0.2.41/tests/test_stage_b1.py +0 -1151
- memleaf-0.2.41/tests/test_stage_b1_01.py +0 -497
- memleaf-0.2.41/tests/test_stage_b2a.py +0 -9
- memleaf-0.2.41/tests/test_stage_b2a_01.py +0 -530
- memleaf-0.2.41/tests/test_stage_b2a_02.py +0 -674
- memleaf-0.2.41/tests/test_stage_b2a_03.py +0 -641
- memleaf-0.2.41/tests/test_stage_b2a_04.py +0 -629
- memleaf-0.2.41/tests/test_stage_b2a_05.py +0 -317
- memleaf-0.2.41/tests/test_stage_b2b.py +0 -609
- memleaf-0.2.41/tests/test_stage_b3a_commit.py +0 -426
- memleaf-0.2.41/tests/test_stage_b3a_contract.py +0 -283
- memleaf-0.2.41/tests/test_stage_b3b_native_context.py +0 -299
- memleaf-0.2.41/tests/test_stage_b3b_native_index.py +0 -268
- memleaf-0.2.41/tests/test_stage_b3b_scope.py +0 -369
- memleaf-0.2.41/tests/test_stage_b3c_retrieval.py +0 -308
- memleaf-0.2.41/tests/test_stage_b3d_scope_maintenance.py +0 -594
- memleaf-0.2.41/tests/test_stage_c1_mcp.py +0 -808
- memleaf-0.2.41/tests/test_stage_c2_init.py +0 -802
- memleaf-0.2.41/tests/test_stage_c3_packaging.py +0 -165
- memleaf-0.2.41/tests/test_state_layout_v028.py +0 -200
- memleaf-0.2.41/tests/test_summary_date_grounding_integration.py +0 -39
- memleaf-0.2.41/tests/test_target_reconciliation.py +0 -345
- memleaf-0.2.41/tests/test_target_reconciliation_integration.py +0 -50
- memleaf-0.2.41/tests/test_todo_state_recovery.py +0 -312
- memleaf-0.2.41/tests/test_update_review.py +0 -779
- memleaf-0.2.41/tests/test_update_target_recovery.py +0 -1057
- memleaf-0.2.41/tests/test_upgrade_preserves_vault.py +0 -108
- memleaf-0.2.41/tests/test_v023_scope_correction.py +0 -185
- memleaf-0.2.41/tests/test_v2_gate_limits.py +0 -111
- memleaf-0.2.41/tests/test_v2_host_flow.py +0 -240
- memleaf-0.2.41/tests/test_v2_mcp_flow.py +0 -481
- memleaf-0.2.41/tests/test_v2_nomatch_semantics.py +0 -106
- memleaf-0.2.41/tests/test_v2_search_gate_acceptance.py +0 -147
- memleaf-0.2.41/tests/test_whole_unit_bindings.py +0 -361
- memleaf-0.2.41/tests/test_windows_public_mcp_launcher.py +0 -75
- {memleaf-0.2.41 → memleaf-0.2.43}/LICENSE +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/docs/capture-budget-design.md +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/docs/config-migrations.md +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/docs/core-refactor.md +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/docs/evidence-retention.md +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/docs/gate-evidence-boundary.md +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/docs/hermes-mcp-runtime.md +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/docs/v0.2.26-processing-status.md +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/examples/basic_usage.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/examples/mcp_stdio.ndjson +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/install.ps1 +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/install.sh +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/setup.cfg +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/__main__.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/adapters/__init__.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/adapters/antigravity.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/adapters/base.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/adapters/codex.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/adapters/hermes.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/admission.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/batch_review.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/budget.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/capture.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/cli.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/compaction.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/create_coordinator.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/credentials.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/evidence_budget.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/evidence_policy.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/evidence_structure.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/evidence_syntax.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/frontmatter.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/hermes_provider/README.md +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/hermes_provider/__init__.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/hermes_provider/_mcp_client.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/hermes_provider/_provider.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/hermes_provider/_shared.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/hermes_provider/evidence_budget.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/hermes_runtime.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/host_events.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/host_runtime.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/inbox.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/index.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/inspection.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/installer.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/llm/__init__.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/llm/claude_compatible.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/llm/gemini.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/llm/thinking.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/locking.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/mcp_server.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/memory_planner.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/memory_writer.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/model_discovery.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/model_execution.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/models.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/native_index.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/native_registration.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/parallel_model.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/planning_context.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/process_common.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/process_journal.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/process_owner.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/prompts.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/provenance.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/recording_policy.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/redaction.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/retention.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/retrieval.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/retrieval_gate.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/scope_maintenance.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/scope_state.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/service.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/source_policy.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/state_layout.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/summary_batch.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/target_reconciliation.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/turn_audit.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/turn_plan.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/update_coordinator.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/update_review.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/validation.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf/vault.py +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf.egg-info/dependency_links.txt +0 -0
- {memleaf-0.2.41 → memleaf-0.2.43}/src/memleaf.egg-info/entry_points.txt +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
|
437
|
+
## Development and builds
|
|
438
438
|
|
|
439
|
-
Python 3.11+ is required.
|
|
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.
|
|
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
|
|
127
|
-
quote protocol for update/history/maintenance tests. It does
|
|
128
|
-
code and is not proof those judgments are semantically sound.
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
53
|
+
### 2. Cross-platform local regression and MCP/background-runtime verification
|
|
49
54
|
|
|
50
|
-
Run the
|
|
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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Local-first Markdown memory core for AI agents."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.2.
|
|
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
|
+
]
|