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