memleaf 0.2.34__tar.gz → 0.2.35__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.34 → memleaf-0.2.35}/CHANGELOG.md +8 -0
- {memleaf-0.2.34/src/memleaf.egg-info → memleaf-0.2.35}/PKG-INFO +1 -1
- memleaf-0.2.35/docs/processing-quality-acceptance.md +73 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/pyproject.toml +1 -1
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/__init__.py +1 -1
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/config.py +25 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/hermes_provider/plugin.yaml +1 -1
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/llm/base.py +8 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/llm/router.py +10 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/memory_planner.py +179 -92
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/model_execution.py +164 -15
- memleaf-0.2.35/src/memleaf/parallel_model.py +89 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/process_jobs.py +117 -2
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/processing.py +14 -2
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/prompts.py +28 -3
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/turn_plan.py +6 -3
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/update_coordinator.py +318 -173
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/update_review.py +162 -76
- {memleaf-0.2.34 → memleaf-0.2.35/src/memleaf.egg-info}/PKG-INFO +1 -1
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf.egg-info/SOURCES.txt +4 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_automatic_duplicate_noop_collision.py +4 -1
- memleaf-0.2.35/tests/test_processing_observability_concurrency.py +357 -0
- memleaf-0.2.35/tests/test_revision_digest.py +49 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_update_review.py +10 -9
- {memleaf-0.2.34 → memleaf-0.2.35}/IMPLEMENTATION_PLAN.md +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/LICENSE +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/MANIFEST.in +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/README.en.md +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/README.md +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/RELEASE_CHECKLIST.md +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/docs/capture-budget-design.md +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/docs/config-migrations.md +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/docs/core-refactor.md +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/docs/evidence-retention.md +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/docs/gate-evidence-boundary.md +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/docs/general-processing.md +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/docs/hermes-mcp-runtime.md +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/docs/performance.md +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/docs/v0.2.26-processing-status.md +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/examples/README.md +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/examples/basic_usage.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/examples/live_core_lifecycle_acceptance.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/examples/live_processing_acceptance.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/examples/mcp_stdio.ndjson +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/install.ps1 +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/install.sh +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/setup.cfg +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/__main__.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/adapters/__init__.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/adapters/antigravity.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/adapters/base.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/adapters/codex.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/adapters/hermes.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/admission.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/budget.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/capture.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/cli.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/compaction.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/create_coordinator.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/credentials.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/evidence_budget.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/evidence_policy.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/frontmatter.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/hermes_provider/README.md +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/hermes_provider/__init__.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/hermes_provider/evidence_budget.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/hermes_runtime.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/host_events.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/host_runtime.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/inbox.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/index.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/inspection.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/installer.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/llm/__init__.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/llm/claude_compatible.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/llm/gemini.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/llm/openai_compatible.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/locking.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/mcp_server.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/memory_commit.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/memory_writer.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/model_discovery.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/models.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/native_index.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/native_registration.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/planning_context.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/process_common.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/process_journal.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/process_owner.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/provenance.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/recording_policy.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/redaction.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/retention.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/retrieval.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/retrieval_gate.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/scope_maintenance.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/scope_state.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/service.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/source_policy.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/state_layout.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/target_reconciliation.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/turn_audit.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/validation.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf/vault.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf.egg-info/dependency_links.txt +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf.egg-info/entry_points.txt +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/src/memleaf.egg-info/top_level.txt +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/__init__.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/semantic_fixtures.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_admission_noise.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_candidate_polarity.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_codex_install.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_codex_native_cli.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_config_migrations_v028.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_context_budget.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_conversation_only.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_credential_safety.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_cross_host_acceptance.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_cross_turn_dedupe_regressions.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_due_date_grounding.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_due_date_grounding_retry.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_email_actionable_coverage.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_evidence_budget.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_evidence_retention_policy.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_external_source_dates.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_extraction_quality_regressions.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_gate_capacity.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_gate_schema_repair.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_general_evidence_admission.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_general_tool_provenance.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_global_todo_acceptance.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_global_todo_query_no_write.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_global_todo_retrieval.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_hermes_native_registration.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_hermes_provider.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_hermes_runtime_install.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_hermes_stdio_transport.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_hermes_transport_evidence.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_host_events.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_host_runtime_contract.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_inspection_state_v028.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_install.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_long_run_hygiene.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_maintenance_v2.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_model_discovery.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_model_owned_fields.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_new_scope_source_grounding.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_partial_retry_idempotency.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_phase2_model_decisions.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_process_jobs.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_process_owner_locking.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_processing_contract_v026.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_pypi_install.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_read_only_deferred_isolation.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_retrieval_gate.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_retrieval_v2.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_review_source_context.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_session_lineage.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_shared_memory_refactor.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_source_neutral_todos_v028.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_stage_a.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_stage_b1.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_stage_b2a.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_stage_b2b.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_stage_b3a_commit.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_stage_b3a_contract.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_stage_b3b_native_context.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_stage_b3b_native_index.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_stage_b3b_scope.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_stage_b3c_retrieval.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_stage_b3d_scope_maintenance.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_stage_c1_mcp.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_stage_c2_init.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_stage_c3_packaging.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_state_layout_v028.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_summary_date_grounding_integration.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_target_reconciliation.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_target_reconciliation_integration.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_todo_state_recovery.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_update_target_recovery.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_upgrade_preserves_vault.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_v023_scope_correction.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_v2_gate_limits.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_v2_host_flow.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_v2_mcp_flow.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_v2_nomatch_semantics.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_v2_search_gate_acceptance.py +0 -0
- {memleaf-0.2.34 → memleaf-0.2.35}/tests/test_whole_unit_bindings.py +0 -0
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to memleaf are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.2.35 — 2026-09-08
|
|
6
|
+
|
|
7
|
+
- Add safe structural model-call telemetry across successful, deferred, and failed processing: call/retry/failure counts, model-request and stage wall-clock timing, input/output lengths, and maximum in-flight concurrency are retained without prompts, responses, memory bodies, credentials, provider secrets, URLs, or raw exception text.
|
|
8
|
+
- Keep Gate admission serial, then allow bounded candidate-summary and final semantic-review concurrency only on explicitly parallel-safe model transports. `process.model_concurrency` defaults to 3 and is bounded to 1..8; host callbacks and caller-owned HTTP openers remain serial. Same-target UPDATE work stays ordered, target reconciliation happens before parallel work, and model calls never run while a Vault write lock is held.
|
|
9
|
+
- Tighten semantic completeness so admitted memories preserve meaning-defining named subjects/entities, deliverables, concrete actions or states, required business/workstream context, and each number/code with its source-stated role. Final review now treats omission or over-generalization as a quality failure, preserves uncertainty, and must not invent owners, deadlines, statuses, completion meaning, or numeric roles.
|
|
10
|
+
- Separate customer/project ownership from product/platform/system implementation context. Current-turn evidence remains authoritative for new relationships; existing memories are comparison context only and cannot create a new ownership or project-affiliation fact. Independent work remains source-neutrally split, while unresolved aggregate candidates defer instead of being silently discarded.
|
|
11
|
+
- Validation for this release: deterministic regression, same-input synthetic serial-vs-parallel comparison, Linux Python 3.11/3.12/3.13, Windows Python 3.11/3.12/3.13, macOS Python 3.11/3.13, wheel/sdist, installed entry points, and native Codex Windows/macOS gates passed. One Windows Python 3.12 full-regression attempt had an isolated failure and its targeted rerun passed. Authorized real-model replay, production-conversation acceptance, and same-input real-provider latency comparison were not run, so this release does not claim a specific replacement for the previously observed 433-second session.
|
|
12
|
+
|
|
5
13
|
## 0.2.34 — 2026-09-08
|
|
6
14
|
|
|
7
15
|
- Restrict automatic memory extraction to 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. Explicit memory-write prohibitions still fail closed.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Processing performance and semantic-quality acceptance
|
|
2
|
+
|
|
3
|
+
This document defines acceptance for automatic memory extraction without treating one validation layer as proof of another.
|
|
4
|
+
|
|
5
|
+
## Invariants
|
|
6
|
+
|
|
7
|
+
- Markdown under `knowledge/` remains the active-memory source of truth; `history/` remains historical state.
|
|
8
|
+
- Gate stays serial. Model work must not run while a Vault write lock is held.
|
|
9
|
+
- Candidate/review parallelism is bounded and opt-in through an explicitly parallel-safe backend. The default maximum is 3.
|
|
10
|
+
- Same-target updates are coordinated before independent final reviews. Revision checks, idempotency and atomic commit remain authoritative.
|
|
11
|
+
- Performance improvements must not remove Gate, summarization, reconciliation or semantic-review checks that are required for the same input.
|
|
12
|
+
- Prompt/review rules are source-neutral. No mail-, calendar-, customer-, product- or other application-specific extraction rule is used.
|
|
13
|
+
|
|
14
|
+
## Safe model telemetry
|
|
15
|
+
|
|
16
|
+
`process()` returns `model_metrics`, and detached process-job results retain the same bounded projection. Metrics contain only structural numbers:
|
|
17
|
+
|
|
18
|
+
- call count, retry count and failed-call count;
|
|
19
|
+
- cumulative model-request duration and stage wall-clock duration;
|
|
20
|
+
- input/output character and UTF-8 byte counts;
|
|
21
|
+
- maximum in-flight model calls;
|
|
22
|
+
- stage buckets for Gate, summarize, semantic review, coordination and target reconciliation.
|
|
23
|
+
|
|
24
|
+
Prompts, responses, memory bodies, source text, credentials and provider secrets are not retained by these metrics. Existing optional diagnostic logging keeps its separate bounded structural contract.
|
|
25
|
+
|
|
26
|
+
## Semantic acceptance contract
|
|
27
|
+
|
|
28
|
+
A memory must remain understandable without reopening its source. For one admitted future-use topic, the summary/reviewer must preserve source-supported information that defines the item's meaning: the explicit subject or named entity, object/deliverable, concrete action or state, necessary business/workstream context, and any number or code together with its source-stated meaning when that meaning is required for interpretation.
|
|
29
|
+
|
|
30
|
+
Minimality does not permit semantic generalization. A concrete requirement cannot become only “related matters” or an umbrella coordination item. Scope metadata is not a substitute for a named subject in the title/body when that subject distinguishes the item. A customer/project that owns an item and a broader product/platform in which it is implemented are separate relationships; one must not silently replace the other. Existing memories are comparison context and cannot supply a new ownership/project relationship. If attribution, a number's role, ownership, deadline or status is not established, preserve uncertainty or defer rather than guess.
|
|
31
|
+
|
|
32
|
+
Independent deliverables that can be executed, tracked or closed independently remain independent memories. Same-turn aggregate reconciliation may merge only one future-use topic; otherwise the item remains deferred for reprocessing rather than being silently discarded as no change.
|
|
33
|
+
|
|
34
|
+
## Acceptance layers
|
|
35
|
+
|
|
36
|
+
### 1. Deterministic unit/regression tests
|
|
37
|
+
|
|
38
|
+
Required checks include:
|
|
39
|
+
|
|
40
|
+
- telemetry contains only allowlisted structural fields and counts retries correctly;
|
|
41
|
+
- unsafe Host/callable routes remain serial; a parallel-safe API route respects `process.model_concurrency`;
|
|
42
|
+
- final review concurrency preserves request/result ordering and does not mutate audit/Vault state from worker threads;
|
|
43
|
+
- semantic-review contracts require both non-invention and preservation of meaning-defining facts;
|
|
44
|
+
- existing negative-context, evidence-span, scope-operation/native-shadow, aggregate-splitting and Windows process-owner regressions continue to pass.
|
|
45
|
+
|
|
46
|
+
Passing this layer proves only the deterministic contracts under test.
|
|
47
|
+
|
|
48
|
+
### 2. Cross-platform CI and MCP/background-runtime verification
|
|
49
|
+
|
|
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.
|
|
51
|
+
|
|
52
|
+
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
|
+
|
|
54
|
+
### 3. Authorized real-model replay in an isolated Vault
|
|
55
|
+
|
|
56
|
+
Use an explicitly authorized conversation and the same configured model/provider, but point the replay at an isolated temporary Vault. Record only the safe structural metrics plus the resulting test memories. Check at least:
|
|
57
|
+
|
|
58
|
+
- omissions of subject/customer, concrete requirement, business context, and numeric meaning;
|
|
59
|
+
- incorrect project/customer attribution or product-as-owner substitution;
|
|
60
|
+
- incorrect todo state, owner, date or completion inference;
|
|
61
|
+
- independent-item splitting and aggregate deferral/reprocessing;
|
|
62
|
+
- duplicate/update behavior under repeated identical input;
|
|
63
|
+
- sensitive-data boundaries and the configured external-provider authorization.
|
|
64
|
+
|
|
65
|
+
For a before/after performance comparison, use the same captured input and equivalent model configuration. Compare model call count, retries, stage request-duration totals, stage wall-clock duration, maximum concurrency and final-memory quality. Do not claim a latency regression or improvement from different source input.
|
|
66
|
+
|
|
67
|
+
### 4. Production conversation observation
|
|
68
|
+
|
|
69
|
+
After release, compare one or more normal authorized production sessions against the same semantic checklist. Production observations are separate evidence from an isolated replay because model/service latency and real conversation structure can differ.
|
|
70
|
+
|
|
71
|
+
## Delivery reporting
|
|
72
|
+
|
|
73
|
+
Every delivery report must identify each layer as **passed**, **failed**, or **not run**. Never promote “unit tests passed” or “CI passed” into a claim that real-model semantic acceptance passed. If the real conversation content is not authorized for a particular external model/service, do not send it merely to complete acceptance; report that layer as not run.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Local-first Markdown memory core for AI agents."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.2.
|
|
3
|
+
__version__ = "0.2.35"
|
|
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
|
|
@@ -16,6 +16,9 @@ from .scope_state import ScopeError, validate_scope_registry
|
|
|
16
16
|
DEFAULT_REQUEST_TIMEOUT = 120
|
|
17
17
|
MIN_REQUEST_TIMEOUT = 1
|
|
18
18
|
MAX_REQUEST_TIMEOUT = 240
|
|
19
|
+
DEFAULT_MODEL_CONCURRENCY = 3
|
|
20
|
+
MIN_MODEL_CONCURRENCY = 1
|
|
21
|
+
MAX_MODEL_CONCURRENCY = 8
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
def _normalize_request_timeout(value: Any) -> int | float:
|
|
@@ -30,6 +33,14 @@ def _normalize_request_timeout(value: Any) -> int | float:
|
|
|
30
33
|
return int(parsed) if parsed.is_integer() else parsed
|
|
31
34
|
|
|
32
35
|
|
|
36
|
+
def _normalize_model_concurrency(value: Any) -> int:
|
|
37
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
38
|
+
raise ValueError("invalid memleaf process.model_concurrency")
|
|
39
|
+
if not MIN_MODEL_CONCURRENCY <= value <= MAX_MODEL_CONCURRENCY:
|
|
40
|
+
raise ValueError("invalid memleaf process.model_concurrency")
|
|
41
|
+
return value
|
|
42
|
+
|
|
43
|
+
|
|
33
44
|
DEFAULT_CONFIG: dict[str, Any] = {
|
|
34
45
|
"vault": "~/.memleaf",
|
|
35
46
|
"agents": {"codex": True, "hermes": True, "antigravity": False},
|
|
@@ -40,6 +51,7 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
|
|
40
51
|
"memory_compact_candidate_ratio": 0.30,
|
|
41
52
|
"inbox_cleanup_hours": 24,
|
|
42
53
|
"closed_todo_retention_days": 30,
|
|
54
|
+
"model_concurrency": DEFAULT_MODEL_CONCURRENCY,
|
|
43
55
|
},
|
|
44
56
|
"history": {
|
|
45
57
|
"policy": "bounded",
|
|
@@ -157,6 +169,11 @@ def load_config(path: Path | str, *, vault: Path | str | None = None) -> dict[st
|
|
|
157
169
|
closed_todo_days = process.get("closed_todo_retention_days") if isinstance(process, Mapping) else None
|
|
158
170
|
if type(closed_todo_days) is not int or closed_todo_days < 0:
|
|
159
171
|
raise ValueError("invalid memleaf process.closed_todo_retention_days")
|
|
172
|
+
_normalize_model_concurrency(
|
|
173
|
+
process.get("model_concurrency", DEFAULT_MODEL_CONCURRENCY)
|
|
174
|
+
if isinstance(process, Mapping)
|
|
175
|
+
else DEFAULT_MODEL_CONCURRENCY
|
|
176
|
+
)
|
|
160
177
|
history = merged.get("history")
|
|
161
178
|
if not isinstance(history, Mapping):
|
|
162
179
|
raise ValueError("invalid memleaf history settings")
|
|
@@ -199,6 +216,14 @@ def save_config(path: Path | str, config: Mapping[str, Any]) -> None:
|
|
|
199
216
|
validate_native_sources(normalized.get("native_sources", {}), base_dir=Path(path).parent)
|
|
200
217
|
except NativeConfigError as error:
|
|
201
218
|
raise ValueError("invalid memleaf native_sources") from error
|
|
219
|
+
process = normalized.get("process")
|
|
220
|
+
if not isinstance(process, Mapping):
|
|
221
|
+
raise ValueError("invalid memleaf process settings")
|
|
222
|
+
normalized_process = dict(process)
|
|
223
|
+
normalized_process["model_concurrency"] = _normalize_model_concurrency(
|
|
224
|
+
normalized_process.get("model_concurrency", DEFAULT_MODEL_CONCURRENCY)
|
|
225
|
+
)
|
|
226
|
+
normalized["process"] = normalized_process
|
|
202
227
|
llm = normalized.get("llm")
|
|
203
228
|
if not isinstance(llm, Mapping):
|
|
204
229
|
raise ValueError("invalid memleaf llm settings")
|
|
@@ -142,6 +142,7 @@ class ModelUnavailable(ModelError):
|
|
|
142
142
|
class ModelBackend(Protocol):
|
|
143
143
|
provider: str
|
|
144
144
|
model: str
|
|
145
|
+
parallel_safe: bool
|
|
145
146
|
|
|
146
147
|
def complete(
|
|
147
148
|
self,
|
|
@@ -158,6 +159,9 @@ class CallableBackend:
|
|
|
158
159
|
"""Adapt an explicitly injected host/test callback to ``ModelBackend``."""
|
|
159
160
|
|
|
160
161
|
provider = "host"
|
|
162
|
+
# Host callbacks can close over arbitrary mutable state. Never assume a
|
|
163
|
+
# caller-owned callback is thread-safe merely because candidate work is.
|
|
164
|
+
parallel_safe = False
|
|
161
165
|
|
|
162
166
|
def __init__(self, callback: Callable[..., str], *, model: str = "host"):
|
|
163
167
|
if not callable(callback):
|
|
@@ -220,6 +224,7 @@ class HTTPModelBackend:
|
|
|
220
224
|
"""Common standard-library HTTP transport for compatible adapters."""
|
|
221
225
|
|
|
222
226
|
provider = "api"
|
|
227
|
+
parallel_safe = False
|
|
223
228
|
|
|
224
229
|
def __init__(
|
|
225
230
|
self,
|
|
@@ -241,6 +246,9 @@ class HTTPModelBackend:
|
|
|
241
246
|
self.model = model
|
|
242
247
|
self.timeout = normalize_request_timeout(timeout)
|
|
243
248
|
self._opener = opener or urllib.request.urlopen
|
|
249
|
+
# The built-in stateless urllib transport can be used concurrently.
|
|
250
|
+
# An injected opener is caller-owned and therefore defaults to serial.
|
|
251
|
+
self.parallel_safe = opener is None
|
|
244
252
|
|
|
245
253
|
@staticmethod
|
|
246
254
|
def _is_timeout_reason(value: Any) -> bool:
|
|
@@ -52,6 +52,16 @@ class ModelRouter:
|
|
|
52
52
|
mode = kwargs.pop("mode", llm_config.get("mode", "auto"))
|
|
53
53
|
return cls(mode=mode, config=llm_config, **kwargs)
|
|
54
54
|
|
|
55
|
+
@property
|
|
56
|
+
def parallel_safe(self) -> bool:
|
|
57
|
+
"""Expose concurrency only when routing cannot fall through a host callback."""
|
|
58
|
+
|
|
59
|
+
if self.mode == "api":
|
|
60
|
+
return getattr(self.api, "parallel_safe", False) is True
|
|
61
|
+
if self.mode == "auto" and self.host is None:
|
|
62
|
+
return getattr(self.api, "parallel_safe", False) is True
|
|
63
|
+
return False
|
|
64
|
+
|
|
55
65
|
@staticmethod
|
|
56
66
|
def _coerce_host(value: Any) -> Optional[ModelBackend]:
|
|
57
67
|
if value is None:
|
|
@@ -14,6 +14,7 @@ from .create_coordinator import CreateCoordinator
|
|
|
14
14
|
from .update_coordinator import UpdateCoordinator
|
|
15
15
|
from .target_reconciliation import reconcile_candidate_target
|
|
16
16
|
from .evidence_policy import retain_tool_evidence
|
|
17
|
+
from .parallel_model import run_ordered_keyed_jobs
|
|
17
18
|
from .prompts import COVERAGE_ALREADY_COMPLETED_CORRECTION, COVERAGE_CORRECTION, GATE_SYSTEM, SUMMARIZE_SYSTEM, gate_prompt, summarize_prompt
|
|
18
19
|
from .retrieval import normalize_term
|
|
19
20
|
from .scope_state import ScopeError, normalize_scopes
|
|
@@ -1008,6 +1009,18 @@ class MemoryPlanner:
|
|
|
1008
1009
|
seen_candidates: set[tuple[Any, ...]] = set()
|
|
1009
1010
|
seen_duplicate_targets: set[str] = set()
|
|
1010
1011
|
admitted_candidates: dict[str, dict[str, Any]] = {}
|
|
1012
|
+
summary_jobs: list[dict[str, Any]] = []
|
|
1013
|
+
request_slots: list[dict[str, Any]] = []
|
|
1014
|
+
|
|
1015
|
+
def observe_scopes(values: Iterable[Any]) -> None:
|
|
1016
|
+
for observed_scope in values:
|
|
1017
|
+
if (
|
|
1018
|
+
isinstance(observed_scope, str)
|
|
1019
|
+
and observed_scope != "unscoped"
|
|
1020
|
+
and observed_scope not in observed_scopes
|
|
1021
|
+
):
|
|
1022
|
+
observed_scopes.append(observed_scope)
|
|
1023
|
+
|
|
1011
1024
|
for candidate in gate["candidates"]:
|
|
1012
1025
|
candidate = dict(candidate)
|
|
1013
1026
|
duplicate_target = candidate.get("duplicate_memory_id")
|
|
@@ -1266,18 +1279,17 @@ class MemoryPlanner:
|
|
|
1266
1279
|
)
|
|
1267
1280
|
continue
|
|
1268
1281
|
if correction_plan is not None and correction_plan.get("survivor_memory_id"):
|
|
1269
|
-
|
|
1270
|
-
|
|
1282
|
+
request_slots.append({
|
|
1283
|
+
"kind": "request",
|
|
1284
|
+
"request": self.inputs._scope_correction_request(
|
|
1271
1285
|
candidate,
|
|
1272
1286
|
turn,
|
|
1273
1287
|
correction_plan,
|
|
1274
1288
|
conversation_title=title,
|
|
1275
1289
|
native_refs=candidate_native_refs,
|
|
1276
|
-
)
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
if isinstance(observed_scope, str) and observed_scope != "unscoped" and observed_scope not in observed_scopes:
|
|
1280
|
-
observed_scopes.append(observed_scope)
|
|
1290
|
+
),
|
|
1291
|
+
"scopes": list(candidate.get("scopes", [])),
|
|
1292
|
+
})
|
|
1281
1293
|
continue
|
|
1282
1294
|
candidate_native_ids = [item["native_id"] for item in candidate_native_refs]
|
|
1283
1295
|
all_candidate_memory_ids: list[str] = []
|
|
@@ -1335,13 +1347,7 @@ class MemoryPlanner:
|
|
|
1335
1347
|
),
|
|
1336
1348
|
[],
|
|
1337
1349
|
)
|
|
1338
|
-
|
|
1339
|
-
if (
|
|
1340
|
-
isinstance(observed_scope, str)
|
|
1341
|
-
and observed_scope != "unscoped"
|
|
1342
|
-
and observed_scope not in observed_scopes
|
|
1343
|
-
):
|
|
1344
|
-
observed_scopes.append(observed_scope)
|
|
1350
|
+
request_slots.append({"kind": "observe", "scopes": list(duplicate_scopes)})
|
|
1345
1351
|
self.audit._record_disposition(
|
|
1346
1352
|
turn_ref,
|
|
1347
1353
|
candidate,
|
|
@@ -1378,75 +1384,160 @@ class MemoryPlanner:
|
|
|
1378
1384
|
admitted_summary_keys = tuple(dict.fromkeys(event["event_key"] for event in admitted_summary_events))
|
|
1379
1385
|
grounded_summary_dates = _grounded_due_dates(turn, evidence_events=admitted_summary_events)
|
|
1380
1386
|
summary_target = self.inputs._active_memory_by_id(gate_update_target) if gate_update_target else None
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
if isinstance(related_item.get("memory_id"), str):
|
|
1414
|
-
target_memory = self.inputs._active_memory_by_id(related_item["memory_id"])
|
|
1415
|
-
if target_memory is not None:
|
|
1416
|
-
target_revisions[target_memory.memory_id] = revision_digest(target_memory)
|
|
1417
|
-
summary_candidate = dict(candidate)
|
|
1418
|
-
model_candidate_id = summary_candidate.pop("_model_candidate_id", None)
|
|
1419
|
-
if isinstance(model_candidate_id, str) and model_candidate_id:
|
|
1420
|
-
summary_candidate["candidate_id"] = model_candidate_id
|
|
1421
|
-
summary = self.model._complete_json_stage(
|
|
1422
|
-
backend,
|
|
1423
|
-
summarize_prompt(
|
|
1424
|
-
summary_candidate,
|
|
1425
|
-
admitted_summary_events,
|
|
1426
|
-
related_memories=candidate_related,
|
|
1427
|
-
scope_background=candidate_scope_background,
|
|
1428
|
-
scope_registry=scope_registry,
|
|
1429
|
-
),
|
|
1430
|
-
system=SUMMARIZE_SYSTEM,
|
|
1431
|
-
purpose="summarize",
|
|
1432
|
-
parser=parse_summary,
|
|
1433
|
-
diagnostic_context={
|
|
1434
|
-
"source": turn.source,
|
|
1435
|
-
"session_id": turn.session_id,
|
|
1436
|
-
"turn_index": turn.turn_index,
|
|
1437
|
-
},
|
|
1387
|
+
|
|
1388
|
+
def parse_summary(
|
|
1389
|
+
raw: str,
|
|
1390
|
+
*,
|
|
1391
|
+
candidate_value: Mapping[str, Any] = candidate,
|
|
1392
|
+
admitted_keys_value: tuple[str, ...] = admitted_summary_keys,
|
|
1393
|
+
native_ids_value: list[str] = candidate_native_ids,
|
|
1394
|
+
update_ids_value: list[str] = same_type_update_memory_ids,
|
|
1395
|
+
grounded_dates_value: set[str] = grounded_summary_dates,
|
|
1396
|
+
admitted_events_value: list[dict[str, Any]] = admitted_summary_events,
|
|
1397
|
+
summary_target_value: Any = summary_target,
|
|
1398
|
+
gate_update_target_value: Any = gate_update_target,
|
|
1399
|
+
gate_target_type_value: Any = gate_target_type,
|
|
1400
|
+
) -> dict[str, Any]:
|
|
1401
|
+
parsed = parse_summarize_output(
|
|
1402
|
+
_normalize_summary_dates(raw, turn, candidate_value),
|
|
1403
|
+
current_event_keys=admitted_keys_value,
|
|
1404
|
+
related_native_ids=native_ids_value,
|
|
1405
|
+
related_memory_ids=update_ids_value,
|
|
1406
|
+
scope_registry=validation_scope_registry,
|
|
1407
|
+
expected_scopes=candidate_value["scopes"],
|
|
1408
|
+
expected_scope_source=candidate_value["scope_source"],
|
|
1409
|
+
allowed_due_dates=grounded_dates_value,
|
|
1410
|
+
allow_no_change=True,
|
|
1411
|
+
# The summarize stage may not reinterpret a gate
|
|
1412
|
+
# candidate, including CREATE candidates. Updates
|
|
1413
|
+
# additionally retain the active target's immutable
|
|
1414
|
+
# type below.
|
|
1415
|
+
expected_type=candidate_value.get("type"),
|
|
1416
|
+
allow_update_target=gate_update_target_value is not None,
|
|
1417
|
+
expected_update_memory_id=gate_update_target_value,
|
|
1418
|
+
expected_target_type=gate_target_type_value,
|
|
1438
1419
|
)
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1420
|
+
if _summary_date_grounding_violations(
|
|
1421
|
+
parsed,
|
|
1422
|
+
grounded_dates=grounded_dates_value,
|
|
1423
|
+
source_texts=[
|
|
1424
|
+
event.get("content", "")
|
|
1425
|
+
for event in admitted_events_value
|
|
1426
|
+
if event.get("role") in {"user", "assistant"}
|
|
1427
|
+
],
|
|
1428
|
+
preserved_texts=(
|
|
1429
|
+
summary_target_value.title,
|
|
1430
|
+
summary_target_value.body,
|
|
1431
|
+
summary_target_value.due_date,
|
|
1432
|
+
) if summary_target_value else (),
|
|
1433
|
+
):
|
|
1434
|
+
raise ModelOutputError(
|
|
1435
|
+
"summary contains a date absent from its admitted evidence",
|
|
1436
|
+
validation_detail="relative_time",
|
|
1437
|
+
)
|
|
1438
|
+
return parsed
|
|
1439
|
+
|
|
1440
|
+
target_revisions = {}
|
|
1441
|
+
for related_item in candidate_related:
|
|
1442
|
+
if isinstance(related_item.get("memory_id"), str):
|
|
1443
|
+
target_memory = self.inputs._active_memory_by_id(related_item["memory_id"])
|
|
1444
|
+
if target_memory is not None:
|
|
1445
|
+
target_revisions[target_memory.memory_id] = revision_digest(target_memory)
|
|
1446
|
+
summary_candidate = dict(candidate)
|
|
1447
|
+
model_candidate_id = summary_candidate.pop("_model_candidate_id", None)
|
|
1448
|
+
if isinstance(model_candidate_id, str) and model_candidate_id:
|
|
1449
|
+
summary_candidate["candidate_id"] = model_candidate_id
|
|
1450
|
+
summary_prompt_value = summarize_prompt(
|
|
1451
|
+
summary_candidate,
|
|
1452
|
+
admitted_summary_events,
|
|
1453
|
+
related_memories=candidate_related,
|
|
1454
|
+
scope_background=candidate_scope_background,
|
|
1455
|
+
scope_registry=scope_registry,
|
|
1456
|
+
)
|
|
1457
|
+
diagnostic_context = {
|
|
1458
|
+
"source": turn.source,
|
|
1459
|
+
"session_id": turn.session_id,
|
|
1460
|
+
"turn_index": turn.turn_index,
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
def run_summary(
|
|
1464
|
+
*,
|
|
1465
|
+
prompt_value: str = summary_prompt_value,
|
|
1466
|
+
parser_value: Any = parse_summary,
|
|
1467
|
+
diagnostic_value: Mapping[str, Any] = diagnostic_context,
|
|
1468
|
+
) -> dict[str, Any]:
|
|
1469
|
+
try:
|
|
1470
|
+
return {
|
|
1471
|
+
"status": "ok",
|
|
1472
|
+
"summary": self.model._complete_json_stage(
|
|
1473
|
+
backend,
|
|
1474
|
+
prompt_value,
|
|
1475
|
+
system=SUMMARIZE_SYSTEM,
|
|
1476
|
+
purpose="summarize",
|
|
1477
|
+
parser=parser_value,
|
|
1478
|
+
diagnostic_context=diagnostic_value,
|
|
1479
|
+
),
|
|
1480
|
+
}
|
|
1481
|
+
except ModelOutputError as error:
|
|
1482
|
+
if getattr(error, "validation_detail", None) not in {
|
|
1483
|
+
"relative_time",
|
|
1484
|
+
"due_date_not_grounded",
|
|
1485
|
+
}:
|
|
1486
|
+
raise
|
|
1487
|
+
return {"status": "relative_time"}
|
|
1488
|
+
|
|
1489
|
+
target_key = (
|
|
1490
|
+
f"update:{gate_update_target.casefold()}"
|
|
1491
|
+
if isinstance(gate_update_target, str) and gate_update_target
|
|
1492
|
+
else f"create:{str(candidate['candidate_id']).casefold()}"
|
|
1493
|
+
)
|
|
1494
|
+
job_index = len(summary_jobs)
|
|
1495
|
+
summary_jobs.append({
|
|
1496
|
+
"key": target_key,
|
|
1497
|
+
"call": run_summary,
|
|
1498
|
+
"candidate": dict(candidate),
|
|
1499
|
+
"candidate_related": candidate_related,
|
|
1500
|
+
"candidate_native_refs": candidate_native_refs,
|
|
1501
|
+
"correction_plan": correction_plan,
|
|
1502
|
+
"gate_update_target": gate_update_target,
|
|
1503
|
+
"target_revisions": target_revisions,
|
|
1504
|
+
})
|
|
1505
|
+
request_slots.append({"kind": "summary", "job_index": job_index})
|
|
1506
|
+
|
|
1507
|
+
summary_outcomes = run_ordered_keyed_jobs(
|
|
1508
|
+
self.model,
|
|
1509
|
+
backend,
|
|
1510
|
+
[(job["key"], job["call"]) for job in summary_jobs],
|
|
1511
|
+
)
|
|
1512
|
+
|
|
1513
|
+
for slot in request_slots:
|
|
1514
|
+
kind = slot.get("kind")
|
|
1515
|
+
if kind == "observe":
|
|
1516
|
+
observe_scopes(slot.get("scopes", []))
|
|
1517
|
+
continue
|
|
1518
|
+
if kind == "request":
|
|
1519
|
+
request = slot.get("request")
|
|
1520
|
+
if isinstance(request, dict):
|
|
1521
|
+
requests.append(request)
|
|
1522
|
+
observe_scopes(slot.get("scopes", []))
|
|
1523
|
+
continue
|
|
1524
|
+
if kind != "summary":
|
|
1525
|
+
continue
|
|
1526
|
+
job_index = slot.get("job_index")
|
|
1527
|
+
if not isinstance(job_index, int) or isinstance(job_index, bool):
|
|
1528
|
+
raise ProcessingError("invalid prepared summary job")
|
|
1529
|
+
job = summary_jobs[job_index]
|
|
1530
|
+
outcome = summary_outcomes[job_index]
|
|
1531
|
+
candidate = job["candidate"]
|
|
1532
|
+
candidate_related = job["candidate_related"]
|
|
1533
|
+
candidate_native_refs = job["candidate_native_refs"]
|
|
1534
|
+
correction_plan = job["correction_plan"]
|
|
1535
|
+
gate_update_target = job["gate_update_target"]
|
|
1536
|
+
target_revisions = job["target_revisions"]
|
|
1537
|
+
if outcome.get("status") == "relative_time":
|
|
1445
1538
|
# The candidate's source turn remains in inbox for an
|
|
1446
|
-
# explicit retry.
|
|
1447
|
-
# still commit safely in the same transaction.
|
|
1448
|
-
# relative_time audit reason covers both unresolved relative
|
|
1449
|
-
# text and an ungrounded explicit due_date.
|
|
1539
|
+
# explicit retry. Other candidates from this same turn may
|
|
1540
|
+
# still commit safely in the same transaction.
|
|
1450
1541
|
self.audit._defer_candidate(
|
|
1451
1542
|
turn_ref,
|
|
1452
1543
|
candidate,
|
|
@@ -1454,6 +1545,10 @@ class MemoryPlanner:
|
|
|
1454
1545
|
scopes=candidate["scopes"],
|
|
1455
1546
|
)
|
|
1456
1547
|
continue
|
|
1548
|
+
summary = outcome.get("summary")
|
|
1549
|
+
if not isinstance(summary, Mapping):
|
|
1550
|
+
raise ProcessingError("invalid prepared summary result")
|
|
1551
|
+
summary = dict(summary)
|
|
1457
1552
|
if summary.get("decision") == NO_CHANGE_DECISION:
|
|
1458
1553
|
self.audit._record_disposition(
|
|
1459
1554
|
turn_ref,
|
|
@@ -1470,7 +1565,6 @@ class MemoryPlanner:
|
|
|
1470
1565
|
if gate_update_target is not None:
|
|
1471
1566
|
summary_update_target = summary.get("update_memory_id")
|
|
1472
1567
|
if summary_update_target is None:
|
|
1473
|
-
summary = dict(summary)
|
|
1474
1568
|
summary["update_memory_id"] = gate_update_target
|
|
1475
1569
|
elif (
|
|
1476
1570
|
not isinstance(summary_update_target, str)
|
|
@@ -1481,12 +1575,10 @@ class MemoryPlanner:
|
|
|
1481
1575
|
validation_detail="invalid_update_target",
|
|
1482
1576
|
)
|
|
1483
1577
|
else:
|
|
1484
|
-
summary = dict(summary)
|
|
1485
1578
|
summary["update_memory_id"] = gate_update_target
|
|
1486
|
-
|
|
1579
|
+
self.service.read(gate_update_target, include_history=False)
|
|
1487
1580
|
# The summary is the complete model-proposed current value.
|
|
1488
1581
|
# Do not concatenate old/new bodies using business keywords.
|
|
1489
|
-
pass
|
|
1490
1582
|
if summary["scopes"] == ["unscoped"] or summary.get("scope_source") == "insufficient_context":
|
|
1491
1583
|
self.audit._defer_candidate(
|
|
1492
1584
|
turn_ref,
|
|
@@ -1547,13 +1639,8 @@ class MemoryPlanner:
|
|
|
1547
1639
|
else pending_request["memory_id"]
|
|
1548
1640
|
),
|
|
1549
1641
|
)
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
isinstance(observed_scope, str)
|
|
1553
|
-
and observed_scope != "unscoped"
|
|
1554
|
-
and observed_scope not in observed_scopes
|
|
1555
|
-
):
|
|
1556
|
-
observed_scopes.append(observed_scope)
|
|
1642
|
+
observe_scopes(summary["scopes"])
|
|
1643
|
+
|
|
1557
1644
|
requests = CreateCoordinator(self.model, self.audit).resolve(
|
|
1558
1645
|
requests, candidates=admitted_candidates, evidence_units=planning_evidence_units, events=events,
|
|
1559
1646
|
backend=backend, scope_registry=scope_registry, validation_scope_registry=validation_scope_registry)
|