memleaf 0.2.86__tar.gz → 0.2.88__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.86 → memleaf-0.2.88}/CHANGELOG.md +58 -0
- {memleaf-0.2.86/src/memleaf.egg-info → memleaf-0.2.88}/PKG-INFO +6 -6
- {memleaf-0.2.86 → memleaf-0.2.88}/README.en.md +4 -4
- {memleaf-0.2.86 → memleaf-0.2.88}/README.md +5 -5
- {memleaf-0.2.86 → memleaf-0.2.88}/examples/incremental_acceptance.json +9 -10
- {memleaf-0.2.86 → memleaf-0.2.88}/pyproject.toml +1 -1
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/__init__.py +1 -1
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/acceptance.py +1 -1
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/adapters/hermes.py +4 -3
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/compaction.py +1 -1
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/extraction_budget.py +3 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/extraction_work_state.py +2 -2
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/hermes_provider/README.md +7 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/hermes_provider/_mcp_client.py +3 -1
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/hermes_provider/_provider.py +12 -5
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/hermes_provider/plugin.yaml +1 -1
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/host_retention.py +46 -2
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/incremental_commit.py +35 -4
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/incremental_execution.py +51 -27
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/incremental_failed_recovery.py +3 -1
- memleaf-0.2.88/src/memleaf/incremental_merge.py +70 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/incremental_partial.py +2 -1
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/incremental_prompts.py +9 -6
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/incremental_protocol.py +24 -5
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/incremental_recovery.py +2 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/incremental_run_state.py +53 -4
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/incremental_semantics.py +57 -15
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/mcp_server.py +73 -22
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/memory_writer.py +15 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/process_common.py +0 -1
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/processing_route.py +35 -2
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/prompts.py +1 -1
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/retrieval_gate.py +25 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/semantic_maintenance.py +1 -1
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/service.py +21 -3
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/single_pass_plan.py +1 -1
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/source_policy.py +7 -0
- {memleaf-0.2.86 → memleaf-0.2.88/src/memleaf.egg-info}/PKG-INFO +6 -6
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf.egg-info/SOURCES.txt +1 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/LICENSE +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/MANIFEST.in +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/acceptance-evidence-guide.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/automatic-processing-route.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/body-compaction.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/candidate-reconciliation.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/capture-budget-design.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/config-migrations.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/core-refactor.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/development-closeout.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/evidence-retention.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/explicit-memory-update.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/extraction-latency.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/failed-run-recovery.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/gate-evidence-boundary.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/general-processing.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/hermes-mcp-runtime.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/hermes-provider-compatibility.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/hermes-source-comparison-context.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/host-acceptance-runbook.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/immutable-release-assets.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/incremental-commit-contract.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/incremental-execution.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/incremental-native-comparison.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/incremental-partial-recovery.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/incremental-planner-preview.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/incremental-runtime-contract.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/incremental-scope-registration.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/incremental-selected-retention.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/installed-artifact-verification.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/memory-lifecycle-tools.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/memory-state-contract.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/migration-preflight.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/processing-quality-acceptance.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/public-query-integrity.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/query-performance.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/remember-incremental-route.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/runtime-state-retention.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/semantic-extraction-protocol.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/source-work-contract.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/docs/v0.2.26-processing-status.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/examples/README.md +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/examples/basic_usage.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/examples/mcp_stdio.ndjson +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/examples/query_benchmark.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/install.ps1 +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/install.sh +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/setup.cfg +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/__main__.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/adapters/__init__.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/adapters/antigravity.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/adapters/base.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/adapters/codex.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/admission.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/batch_review.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/budget.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/capture.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/cli.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/config.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/create_coordinator.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/credentials.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/evidence_budget.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/evidence_policy.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/evidence_structure.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/evidence_syntax.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/explicit_text_source.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/extraction_capability.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/frontmatter.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/hermes_provider/__init__.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/hermes_provider/_shared.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/hermes_provider/evidence_budget.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/hermes_provider/provider_compatibility.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/hermes_runtime.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/host_events.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/host_runtime.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/host_turn_identity.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/inbox.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/incremental_dates.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/incremental_explicit.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/incremental_journal.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/incremental_native.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/incremental_preview.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/incremental_runtime.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/incremental_scopes.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/incremental_selection.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/index.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/inspection.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/installer.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/legacy_update_journal.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/llm/__init__.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/llm/base.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/llm/claude_compatible.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/llm/gemini.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/llm/openai_compatible.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/llm/router.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/llm/thinking.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/locking.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/memory_commit.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/memory_planner.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/memory_retraction.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/memory_update.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/migration.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/model_capabilities.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/model_discovery.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/model_execution.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/models.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/native_index.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/native_registration.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/parallel_model.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/planning_candidates.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/planning_context.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/process_jobs.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/process_journal.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/process_owner.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/processing.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/provenance.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/provider_compatibility.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/query_clock.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/query_progress.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/query_scan.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/receipt_codec.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/recording_policy.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/redaction.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/remember_route.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/retention.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/retrieval.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/runtime_retention.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/scope_maintenance.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/scope_state.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/semantic_protocol.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/single_pass_memory_planner.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/state_layout.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/subprocess_flags.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/summary_batch.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/target_reconciliation.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/turn_audit.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/turn_plan.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/update_coordinator.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/update_review.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/validation.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf/vault.py +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf.egg-info/dependency_links.txt +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf.egg-info/entry_points.txt +0 -0
- {memleaf-0.2.86 → memleaf-0.2.88}/src/memleaf.egg-info/top_level.txt +0 -0
|
@@ -4,6 +4,64 @@ All notable changes to memleaf are documented here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.2.88 — 2026-10-04
|
|
8
|
+
|
|
9
|
+
### Reliable queued retention and bounded action queries
|
|
10
|
+
|
|
11
|
+
- Use the processing timeout for Hermes whole-turn retention completion while
|
|
12
|
+
keeping queue operations short. Settle fully committed retention intents in
|
|
13
|
+
the same atomic ledger write as their receipts, including interrupted host
|
|
14
|
+
callbacks. Reconcile older matching committed intents without model calls;
|
|
15
|
+
partial, changed-source and revoked-authority intents remain protected.
|
|
16
|
+
- Route conversational saves and lifecycle requests through Hermes's native
|
|
17
|
+
whole-turn retention tool. Hide bare `remember` and `update_memory` from the
|
|
18
|
+
Hermes MCP configuration while preserving those public tools for other
|
|
19
|
+
clients. Return a clear queued acknowledgement before background processing.
|
|
20
|
+
- Add `list_todos(view=full)` with bounded action bodies and explicit versioned
|
|
21
|
+
continuation for long bodies. Server-managed clients can use `continue=true`
|
|
22
|
+
with their current retrieval identity instead of copying opaque cursors.
|
|
23
|
+
Preserve directory/cursor clients, host-owned read accounting and strict
|
|
24
|
+
stale-turn, cursor and filter validation.
|
|
25
|
+
- Keep absent message times unknown. Explicitly supplied deadlines work without
|
|
26
|
+
message timestamps; no capture-time fallback or source-time repair is added.
|
|
27
|
+
- Validation: affected local regressions, a zero-model-call replay of the actual
|
|
28
|
+
committed-intent failure, and isolated real DeepSeek save/deadline and global
|
|
29
|
+
action-pagination scenarios passed. No additional product model calls,
|
|
30
|
+
production Vault writes or historical model retries were introduced.
|
|
31
|
+
|
|
32
|
+
## 0.2.87 — 2026-10-04
|
|
33
|
+
|
|
34
|
+
### Shared source-bound maintenance with bounded model cost
|
|
35
|
+
|
|
36
|
+
- Allow up to five actual requests per incremental work item, stopping on
|
|
37
|
+
success. Extraction, format retries, review and recovery share the durable
|
|
38
|
+
allowance; older work retains its smaller creation-time limit.
|
|
39
|
+
- Bind conversational lifecycle updates to the captured host turn. MCP
|
|
40
|
+
`update_memory` no longer accepts model-supplied source times or directly
|
|
41
|
+
commits rewritten facts when host-bound. Preserve the trusted Python API.
|
|
42
|
+
- Retire `waiting_on` from extraction and model/read/list projections; maintain
|
|
43
|
+
dependencies in the current body. Legacy stored data remains readable.
|
|
44
|
+
- Merge confirmed duplicate memories through the existing processing requests.
|
|
45
|
+
Preserve combined facts, provenance and history; reject incompatible targets.
|
|
46
|
+
Commit the survivor before retracting duplicates, with dependency revisions
|
|
47
|
+
checked at the final writer boundary and recoverable frozen operations.
|
|
48
|
+
- Maintain shared facts across related project and task records using the
|
|
49
|
+
existing source-bound review. Independent lifecycles remain separate.
|
|
50
|
+
- Retract withdrawn facts/preferences through validity, retaining their history;
|
|
51
|
+
explicit replacement preferences remain valid. Task cancellation is not a
|
|
52
|
+
substitute for preference withdrawal.
|
|
53
|
+
- Reuse successful, verified full-turn host retention to settle automatic work
|
|
54
|
+
without another model request. Partial selected retention does not claim
|
|
55
|
+
complete automatic coverage. Record actual API token usage without extra
|
|
56
|
+
requests or counting cached replay usage again.
|
|
57
|
+
- Count known provenance overlaps once; mark counts for truncated histories as
|
|
58
|
+
upper bounds when their unseen overlap cannot be reconstructed.
|
|
59
|
+
- Validation: 483 affected regression and installation/acceptance checks passed;
|
|
60
|
+
nine isolated real DeepSeek scenarios and the installed Hermes background
|
|
61
|
+
callback with current source passed. Saved faulty time arguments were rejected
|
|
62
|
+
in an isolated zero-call replay. No production memory migration or historical
|
|
63
|
+
failed-run retry was performed.
|
|
64
|
+
|
|
7
65
|
## 0.2.86 — 2026-10-04
|
|
8
66
|
|
|
9
67
|
### Source-bound Hermes retention and general extraction compatibility
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.88
|
|
4
4
|
Summary: A local-first Markdown memory core for AI agents
|
|
5
5
|
Author: memleaf contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -23,10 +23,10 @@ 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.88。**
|
|
27
27
|
> 自动提炼和复核共用同一条“未来记忆价值”标准:模型综合未来复用、信息增量、再次读取时的直接可用性和忘记成本,只保留对未来理解、判断或行动有实质影响的最小核心;没有明确价值的信息不提炼,不按具体业务场景硬编码排除。
|
|
28
|
-
>
|
|
29
|
-
> 新请求兼容等价的字段表示;未知执行人保持 `null
|
|
28
|
+
> 提炼先比较旧事项中的编号、目标、进展、责任和期限;补全未知信息也应维护原事项,另建独立待办不能替代项目事实更新。确认重复的记忆在现有处理请求中合并,保留历史与来源;项目和独立任务保持各自生命周期,Core 不按标题强行合并。
|
|
29
|
+
> 新请求兼容等价的字段表示;未知执行人保持 `null`,新增或变更负责人和期限需要真实来源;等待条件在正文维护,不再提炼 `waiting_on`。行动及相关字段经过有界语义复核,提炼与复核共用每份工作的最多五次模型请求额度,成功即停止;复核失败不写入。来源时间用于解析相对日期,不补造事实日期。
|
|
30
30
|
> 待办 `due_date` 只表示待办动作本身明确声明的截止日;属于待办主题或预期结果的日期不会被误作截止日。项目归属由模型按语义判断,不要求项目名在证据里逐字出现。复核阶段会保留首轮已选定的更新目标,避免“状态变更”被降级成新增记忆。Markdown 仍是唯一事实源。
|
|
31
31
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
32
32
|
|
|
@@ -285,7 +285,7 @@ python -m memleaf.mcp_server --vault "$HOME/.memleaf"
|
|
|
285
285
|
| `process_status` | 只读查询异步处理 job 状态 |
|
|
286
286
|
| `recover_failed_run` | 预览并显式恢复终态传输失败,保留原请求预算 |
|
|
287
287
|
| `remember` | 用户明确要求时创建或更新记忆 |
|
|
288
|
-
| `update_memory` | 携带当前 revision
|
|
288
|
+
| `update_memory` | 携带当前 revision 维护任务或撤回事实/偏好;宿主绑定的调用排队处理真实消息 |
|
|
289
289
|
| `forget_memory` | 用户明确要求遗忘时,携带 `confirm_delete=true` 按精确 ID 永久删除 |
|
|
290
290
|
| `forget_about` | 携带 `confirm_delete=true` 遗忘明确主题;有歧义时只返回候选 |
|
|
291
291
|
| `rebuild_index` | 重建可重建的本地派生索引 |
|
|
@@ -361,7 +361,7 @@ python -m memleaf.mcp_server --vault /path/to/your/vault \
|
|
|
361
361
|
|
|
362
362
|
捕获、索引、目录检索和读取可离线运行;`process()`、`remember()` 和 `compact()` 需要可用的模型能力。
|
|
363
363
|
|
|
364
|
-
|
|
364
|
+
包含行动或责任、期限变更的增量候选通常会追加一次语义复核;每份增量工作最多五次请求,格式修复与恢复也计入同一持久化额度,成功即停止。
|
|
365
365
|
|
|
366
366
|
`llm.mode` 支持:
|
|
367
367
|
|
|
@@ -4,10 +4,10 @@
|
|
|
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.88.**
|
|
8
8
|
> Automatic extraction and review share one future-value standard: the model weighs likely reuse, information gain, direct usability when read again, and the cost of forgetting. It keeps only the smallest core that can materially help future understanding, decisions, or actions; information without clear value is not extracted, and no business-specific exclusion rule is hard-coded.
|
|
9
|
-
> Extraction compares existing identifiers, objectives, progress, responsibility and deadlines first. Filling an unknown also maintains the original item; a separate task does not replace updating shared project facts. Core does not merge matching titles.
|
|
10
|
-
> New requests accept equivalent field representations. Unknown executors remain `null`; new or changed responsibility
|
|
9
|
+
> Extraction compares existing identifiers, objectives, progress, responsibility and deadlines first. Filling an unknown also maintains the original item; a separate task does not replace updating shared project facts. Confirmed duplicates can be merged in the existing processing requests with their sources and history preserved. Independent project and task lifecycles remain separate; Core does not merge matching titles.
|
|
10
|
+
> New requests accept equivalent field representations. Unknown executors remain `null`; new or changed responsibility and deadlines require actual source evidence. Dependencies belong in the body; `waiting_on` is no longer extracted. Proposed actions and related fields undergo bounded semantic verification within a shared five-request limit per work item, stopping on success; failed verification blocks writes. Source time resolves relative dates without inventing factual dates.
|
|
11
11
|
> A todo's `due_date` means only the explicit deadline of the todo action; dates describing its subject or desired outcome are not deadlines. Project ownership remains the model's semantic judgement and needs no literal name match. The review stage now keeps the update target chosen by the first pass, so a state change is no longer downgraded into a new memory. Markdown remains the sole source of truth.
|
|
12
12
|
> **The current release supports Hermes and Codex.** Antigravity is not detected, installed, or configured.
|
|
13
13
|
|
|
@@ -266,7 +266,7 @@ The server exposes the following tools:
|
|
|
266
266
|
| `process_status` | Read the status of a background processing job |
|
|
267
267
|
| `recover_failed_run` | Preview and explicitly recover terminal transport failures within the original request budget |
|
|
268
268
|
| `remember` | Create or update memory after an explicit request |
|
|
269
|
-
| `update_memory` |
|
|
269
|
+
| `update_memory` | Maintain tasks or retract facts/preferences using their current revision; host-bound calls queue actual source messages |
|
|
270
270
|
| `forget_memory` | Permanently delete an exact ID after explicit user forgetting, with `confirm_delete=true` |
|
|
271
271
|
| `forget_about` | Forget an unambiguous topic with `confirm_delete=true`; return candidates when ambiguous |
|
|
272
272
|
| `rebuild_index` | Rebuild local derived indexes |
|
|
@@ -4,10 +4,10 @@
|
|
|
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.88。**
|
|
8
8
|
> 自动提炼和复核共用同一条“未来记忆价值”标准:模型综合未来复用、信息增量、再次读取时的直接可用性和忘记成本,只保留对未来理解、判断或行动有实质影响的最小核心;没有明确价值的信息不提炼,不按具体业务场景硬编码排除。
|
|
9
|
-
>
|
|
10
|
-
> 新请求兼容等价的字段表示;未知执行人保持 `null
|
|
9
|
+
> 提炼先比较旧事项中的编号、目标、进展、责任和期限;补全未知信息也应维护原事项,另建独立待办不能替代项目事实更新。确认重复的记忆在现有处理请求中合并,保留历史与来源;项目和独立任务保持各自生命周期,Core 不按标题强行合并。
|
|
10
|
+
> 新请求兼容等价的字段表示;未知执行人保持 `null`,新增或变更负责人和期限需要真实来源;等待条件在正文维护,不再提炼 `waiting_on`。行动及相关字段经过有界语义复核,提炼与复核共用每份工作的最多五次模型请求额度,成功即停止;复核失败不写入。来源时间用于解析相对日期,不补造事实日期。
|
|
11
11
|
> 待办 `due_date` 只表示待办动作本身明确声明的截止日;属于待办主题或预期结果的日期不会被误作截止日。项目归属由模型按语义判断,不要求项目名在证据里逐字出现。复核阶段会保留首轮已选定的更新目标,避免“状态变更”被降级成新增记忆。Markdown 仍是唯一事实源。
|
|
12
12
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
13
13
|
|
|
@@ -266,7 +266,7 @@ python -m memleaf.mcp_server --vault "$HOME/.memleaf"
|
|
|
266
266
|
| `process_status` | 只读查询异步处理 job 状态 |
|
|
267
267
|
| `recover_failed_run` | 预览并显式恢复终态传输失败,保留原请求预算 |
|
|
268
268
|
| `remember` | 用户明确要求时创建或更新记忆 |
|
|
269
|
-
| `update_memory` | 携带当前 revision
|
|
269
|
+
| `update_memory` | 携带当前 revision 维护任务或撤回事实/偏好;宿主绑定的调用排队处理真实消息 |
|
|
270
270
|
| `forget_memory` | 用户明确要求遗忘时,携带 `confirm_delete=true` 按精确 ID 永久删除 |
|
|
271
271
|
| `forget_about` | 携带 `confirm_delete=true` 遗忘明确主题;有歧义时只返回候选 |
|
|
272
272
|
| `rebuild_index` | 重建可重建的本地派生索引 |
|
|
@@ -342,7 +342,7 @@ python -m memleaf.mcp_server --vault /path/to/your/vault \
|
|
|
342
342
|
|
|
343
343
|
捕获、索引、目录检索和读取可离线运行;`process()`、`remember()` 和 `compact()` 需要可用的模型能力。
|
|
344
344
|
|
|
345
|
-
|
|
345
|
+
包含行动或责任、期限变更的增量候选通常会追加一次语义复核;每份增量工作最多五次请求,格式修复与恢复也计入同一持久化额度,成功即停止。
|
|
346
346
|
|
|
347
347
|
`llm.mode` 支持:
|
|
348
348
|
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"expected": {
|
|
32
32
|
"execution_status": "completed",
|
|
33
33
|
"coverage_status": "complete",
|
|
34
|
-
"max_calls":
|
|
34
|
+
"max_calls": 2,
|
|
35
35
|
"count": 1,
|
|
36
36
|
"required": [
|
|
37
37
|
{
|
|
@@ -215,14 +215,13 @@
|
|
|
215
215
|
"expected": {
|
|
216
216
|
"execution_status": "completed",
|
|
217
217
|
"coverage_status": "complete",
|
|
218
|
-
"max_calls":
|
|
218
|
+
"max_calls": 2,
|
|
219
219
|
"count": 1,
|
|
220
220
|
"required": [
|
|
221
221
|
{
|
|
222
222
|
"memory_id": "mem-report",
|
|
223
223
|
"status": "active",
|
|
224
224
|
"assignee": "林工",
|
|
225
|
-
"waiting_on": "客户验收",
|
|
226
225
|
"due_date": "2026-09-25"
|
|
227
226
|
}
|
|
228
227
|
]
|
|
@@ -330,7 +329,7 @@
|
|
|
330
329
|
"expected": {
|
|
331
330
|
"execution_status": "completed",
|
|
332
331
|
"coverage_status": "complete",
|
|
333
|
-
"max_calls":
|
|
332
|
+
"max_calls": 2,
|
|
334
333
|
"required": [
|
|
335
334
|
{
|
|
336
335
|
"memory_id": "mem-report",
|
|
@@ -421,7 +420,7 @@
|
|
|
421
420
|
"expected": {
|
|
422
421
|
"execution_status": "completed",
|
|
423
422
|
"coverage_status": "complete",
|
|
424
|
-
"max_calls":
|
|
423
|
+
"max_calls": 2,
|
|
425
424
|
"count": 2,
|
|
426
425
|
"required": [
|
|
427
426
|
{
|
|
@@ -471,7 +470,7 @@
|
|
|
471
470
|
"expected": {
|
|
472
471
|
"execution_status": "completed",
|
|
473
472
|
"coverage_status": "complete",
|
|
474
|
-
"max_calls":
|
|
473
|
+
"max_calls": 2,
|
|
475
474
|
"count": 1,
|
|
476
475
|
"required": [
|
|
477
476
|
{
|
|
@@ -524,7 +523,7 @@
|
|
|
524
523
|
"expected": {
|
|
525
524
|
"execution_status": "completed",
|
|
526
525
|
"coverage_status": "complete",
|
|
527
|
-
"max_calls":
|
|
526
|
+
"max_calls": 2,
|
|
528
527
|
"count": 1,
|
|
529
528
|
"required": [
|
|
530
529
|
{
|
|
@@ -564,7 +563,7 @@
|
|
|
564
563
|
"expected": {
|
|
565
564
|
"execution_status": "completed",
|
|
566
565
|
"coverage_status": "complete",
|
|
567
|
-
"max_calls":
|
|
566
|
+
"max_calls": 2,
|
|
568
567
|
"count": 1,
|
|
569
568
|
"required": [
|
|
570
569
|
{
|
|
@@ -665,7 +664,7 @@
|
|
|
665
664
|
"expected": {
|
|
666
665
|
"execution_status": "completed",
|
|
667
666
|
"coverage_status": "complete",
|
|
668
|
-
"max_calls":
|
|
667
|
+
"max_calls": 2,
|
|
669
668
|
"count": 2,
|
|
670
669
|
"required": [
|
|
671
670
|
{
|
|
@@ -732,7 +731,7 @@
|
|
|
732
731
|
"expected": {
|
|
733
732
|
"execution_status": "completed",
|
|
734
733
|
"coverage_status": "complete",
|
|
735
|
-
"max_calls":
|
|
734
|
+
"max_calls": 2,
|
|
736
735
|
"count": 1,
|
|
737
736
|
"required": [
|
|
738
737
|
{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Local-first Markdown memory core for AI agents."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.2.
|
|
3
|
+
__version__ = "0.2.88"
|
|
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
|
|
@@ -327,7 +327,7 @@ def _checks(expected: dict, records: list[dict], observations: list[dict], calls
|
|
|
327
327
|
else: # forbidden
|
|
328
328
|
good = all(not any(matches(r, forbidden) for r in records) for forbidden in value)
|
|
329
329
|
results.append({"check": key, "passed": good})
|
|
330
|
-
results.append({"check": "per_work_request_cap", "passed": result.get("reserved_requests", 0) <=
|
|
330
|
+
results.append({"check": "per_work_request_cap", "passed": result.get("reserved_requests", 0) <= result["request_limit"]})
|
|
331
331
|
return results
|
|
332
332
|
|
|
333
333
|
|
|
@@ -24,6 +24,7 @@ from .base import (
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
_MCP_IDLE_TIMEOUT_SECONDS = 60
|
|
27
|
+
_HOST_RETENTION_TOOLS = frozenset({"remember", "update_memory"})
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
def expected_mcp_tool_count() -> int:
|
|
@@ -452,15 +453,15 @@ class HermesAdapter:
|
|
|
452
453
|
if (isinstance(managed, dict) and set(managed) == {"patterns", "applied"}
|
|
453
454
|
and managed["applied"] == selected):
|
|
454
455
|
selected = patterns(managed["patterns"])
|
|
455
|
-
known = [tool["name"] for tool in _TOOLS if tool["name"]
|
|
456
|
+
known = [tool["name"] for tool in _TOOLS if tool["name"] not in _HOST_RETENTION_TOOLS
|
|
456
457
|
and any(fnmatch.fnmatchcase(tool["name"], pattern) for pattern in selected)]
|
|
457
458
|
# Retain explicit future names and original wildcard intent so
|
|
458
459
|
# reinstalling a newer declaration set does not freeze it.
|
|
459
|
-
literal = [name for name in selected if name
|
|
460
|
+
literal = [name for name in selected if name not in _HOST_RETENTION_TOOLS and not any(c in name for c in "*?[")]
|
|
460
461
|
updated["include"] = list(dict.fromkeys([*known, *literal]))
|
|
461
462
|
updated["memleaf_bound_retention"] = {"patterns": selected, "applied": updated["include"]}
|
|
462
463
|
else:
|
|
463
|
-
updated["exclude"] = list(dict.fromkeys([*patterns(updated.get("exclude", [])),
|
|
464
|
+
updated["exclude"] = list(dict.fromkeys([*patterns(updated.get("exclude", [])), *sorted(_HOST_RETENTION_TOOLS)]))
|
|
464
465
|
if updated == filters:
|
|
465
466
|
return True
|
|
466
467
|
written = run_argv(self.runner, [detection.executable, "config", "set", key,
|
|
@@ -427,7 +427,7 @@ class Compactor:
|
|
|
427
427
|
value = _content_mapping(candidate.memory)
|
|
428
428
|
value["memory_id"] = candidate.memory.memory_id
|
|
429
429
|
value["validity"] = candidate.memory.validity
|
|
430
|
-
for key in ("assignee", "
|
|
430
|
+
for key in ("assignee", "due_text"):
|
|
431
431
|
if key in candidate.memory.extra:
|
|
432
432
|
value[key] = deepcopy(candidate.memory.extra[key])
|
|
433
433
|
return value
|
|
@@ -14,6 +14,8 @@ from .llm import ModelError
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
MAX_MODEL_REQUESTS = 3
|
|
17
|
+
# Incremental drafting, format repair and review share one durable allowance.
|
|
18
|
+
MAX_INCREMENTAL_REQUESTS = 5
|
|
17
19
|
TARGET_TOTAL_SECONDS = 10.0
|
|
18
20
|
|
|
19
21
|
|
|
@@ -205,6 +207,7 @@ def budget_single_pass_backend(
|
|
|
205
207
|
|
|
206
208
|
__all__ = [
|
|
207
209
|
"MAX_MODEL_REQUESTS",
|
|
210
|
+
"MAX_INCREMENTAL_REQUESTS",
|
|
208
211
|
"TARGET_TOTAL_SECONDS",
|
|
209
212
|
"ExtractionTiming",
|
|
210
213
|
"aggregate_extraction_metrics",
|
|
@@ -17,7 +17,7 @@ from pathlib import Path
|
|
|
17
17
|
from typing import Any, Mapping
|
|
18
18
|
|
|
19
19
|
from .locking import atomic_write_json, read_json
|
|
20
|
-
from .extraction_budget import MAX_MODEL_REQUESTS
|
|
20
|
+
from .extraction_budget import MAX_MODEL_REQUESTS, MAX_INCREMENTAL_REQUESTS
|
|
21
21
|
from .validation import parse_strict_json
|
|
22
22
|
|
|
23
23
|
|
|
@@ -307,7 +307,7 @@ def reserve_model_request(
|
|
|
307
307
|
raise ExtractionWorkStateError("invalid extraction work id")
|
|
308
308
|
if not _valid_identifier(turn_id, maximum=800):
|
|
309
309
|
raise ExtractionWorkStateError("invalid extraction turn id")
|
|
310
|
-
if type(request_limit) is not int or not 1 <= request_limit <= MAX_MODEL_REQUESTS:
|
|
310
|
+
if type(request_limit) is not int or not 1 <= request_limit <= max(MAX_MODEL_REQUESTS, MAX_INCREMENTAL_REQUESTS):
|
|
311
311
|
raise ExtractionWorkStateError("invalid extraction request limit")
|
|
312
312
|
if type(legacy_source_unchanged) is not bool:
|
|
313
313
|
raise ExtractionWorkStateError("invalid legacy source compatibility flag")
|
|
@@ -17,6 +17,13 @@ A queued response is not a saved memory. The installer excludes standalone MCP
|
|
|
17
17
|
supports `remember` for other clients, plus `forget` and maintenance operations.
|
|
18
18
|
This is a Soft Gate, not a guarantee that every answer has performed retrieval.
|
|
19
19
|
|
|
20
|
+
The bound native route also handles conversational corrections, completion,
|
|
21
|
+
cancellation, reopening and withdrawal. A successful full-turn retention receipt
|
|
22
|
+
settles automatic extraction without another model request. Partial selections
|
|
23
|
+
remain separate. MCP lifecycle updates do not accept model-supplied source times;
|
|
24
|
+
trusted Python callers keep their explicit metadata API. Restart Hermes after
|
|
25
|
+
upgrading to load the current Provider.
|
|
26
|
+
|
|
20
27
|
Hermes discovers this directory when it is installed as:
|
|
21
28
|
|
|
22
29
|
```text
|
|
@@ -236,7 +236,9 @@ class _MCPClient:
|
|
|
236
236
|
result = self._request_locked(
|
|
237
237
|
"tools/call",
|
|
238
238
|
{"name": name, "arguments": dict(arguments)},
|
|
239
|
-
timeout=self.process_timeout if name == "process"
|
|
239
|
+
timeout=(self.process_timeout if name == "process"
|
|
240
|
+
or name == "remember_turn" and arguments.get("phase") == "complete"
|
|
241
|
+
else self.timeout),
|
|
240
242
|
)
|
|
241
243
|
except _MCPToolError as error:
|
|
242
244
|
# A local compatibility gate refusal is not a transport failure.
|
|
@@ -1370,6 +1370,11 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
1370
1370
|
return ""
|
|
1371
1371
|
return (
|
|
1372
1372
|
"# Memleaf Memory\n"
|
|
1373
|
+
"For a request to save, correct, complete, cancel, reopen, or withdraw memory, "
|
|
1374
|
+
"call memleaf_remember once with this turn's retrieval_id. It queues the actual "
|
|
1375
|
+
"whole turn; no rewritten content, target revision, or message time is needed. "
|
|
1376
|
+
"Use the returned user_message as the save acknowledgement. A queued request "
|
|
1377
|
+
"starts processing only after your final reply, so its changes are pending.\n"
|
|
1373
1378
|
"Memleaf is the active local-first memory provider. Each visible user "
|
|
1374
1379
|
"turn must call the configured memleaf MCP search tool at least once "
|
|
1375
1380
|
"before answering, including ordinary greetings; a no-match result is "
|
|
@@ -1383,8 +1388,10 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
1383
1388
|
"When pipeline_status is pending/unknown, memory can lag recent conversation: qualify current-state claims. For partial or completed_with_unresolved writes, report only the individually committed changes, and say which remain unresolved; never claim the entire request succeeded. "
|
|
1384
1389
|
"When the user asks for current "
|
|
1385
1390
|
"todos, all unfinished work, urgent work, or work due in a time range, call memleaf MCP "
|
|
1386
|
-
"list_todos instead of relevance search; omit scope for a global query
|
|
1387
|
-
"
|
|
1391
|
+
"list_todos(view=full) instead of relevance search; omit scope for a global query. "
|
|
1392
|
+
"Use the returned continuation arguments until has_more=false, without copying a cursor. "
|
|
1393
|
+
"Each row includes a body; read only truncated rows with offset=body_next_offset "
|
|
1394
|
+
"and expected_version=version, following their remaining body pages. "
|
|
1388
1395
|
"Never exclude a todo because another Hermes session or another Agent created it. Hermes has a soft "
|
|
1389
1396
|
"observer only: do not claim a search happened unless the visible tool "
|
|
1390
1397
|
"messages show it. Visible Hermes "
|
|
@@ -1403,7 +1410,7 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
1403
1410
|
"file writes to simulate success, and do not infer automatic success merely because "
|
|
1404
1411
|
"active or history files exist. Automatic recall is a directory of "
|
|
1405
1412
|
"scope identifiers, hierarchy, and aliases only; it never contains "
|
|
1406
|
-
"memory IDs, titles, or bodies.
|
|
1413
|
+
"memory IDs, titles, or bodies. Do not infer message time from a deadline or submit assistant-written facts through legacy MCP remember. "
|
|
1407
1414
|
"Use deliberate remember/forget tools "
|
|
1408
1415
|
"only when the user explicitly asks for that operation. Automatic "
|
|
1409
1416
|
"capture and processing use only visible user and assistant text; "
|
|
@@ -2060,7 +2067,7 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
2060
2067
|
if not tool_name.startswith(prefix):
|
|
2061
2068
|
return None
|
|
2062
2069
|
name = tool_name[len(prefix):]
|
|
2063
|
-
if name not in {"search", "read", "list_todos", "scope_catalog", "remember"}:
|
|
2070
|
+
if name not in {"search", "read", "list_todos", "scope_catalog", "remember", "update_memory"}:
|
|
2064
2071
|
return None
|
|
2065
2072
|
turn_id = _host_turn_id(turn_context)
|
|
2066
2073
|
session = turn_context.get("session_id") if isinstance(turn_context, Mapping) else None
|
|
@@ -2102,7 +2109,7 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
2102
2109
|
# gated by the initialized host identity and write policy.
|
|
2103
2110
|
if not self._write_enabled:
|
|
2104
2111
|
return []
|
|
2105
|
-
return [{"name": "memleaf_remember", "description": "
|
|
2112
|
+
return [{"name": "memleaf_remember", "description": "Queue the actual complete user turn for a request to save/correct memory, complete/cancel/reopen a task, or withdraw a fact/preference. Call once, with only the current retrieval_id. Reply with the returned user_message to acknowledge the queued request; processing starts after the final reply, so no changes are completed yet.",
|
|
2106
2113
|
"parameters": {"type": "object", "properties": {"retrieval_id": {"type": "string"}},
|
|
2107
2114
|
"required": ["retrieval_id"], "additionalProperties": False}}]
|
|
2108
2115
|
|
|
@@ -11,6 +11,7 @@ from .retrieval_gate import validate_turn, validate_current_turn
|
|
|
11
11
|
KEY = "host_retention_intents"
|
|
12
12
|
MAX_PENDING = 128
|
|
13
13
|
INTENT_PREFIX = "host-retain-"
|
|
14
|
+
PENDING_MESSAGE = "保存请求已排队,后台处理完成后生效。"
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def pending_intents(processed):
|
|
@@ -122,7 +123,8 @@ def remember_turn(service, *, phase, retrieval_id=None, source=None, session_id=
|
|
|
122
123
|
processed[KEY] = pending
|
|
123
124
|
atomic_write_json(service.vault.processed_state_path, processed)
|
|
124
125
|
return {"execution_status": "queued", "saved": False,
|
|
125
|
-
"
|
|
126
|
+
"user_message": PENDING_MESSAGE,
|
|
127
|
+
"guidance": "Acknowledge only that the request is queued. Use user_message for the save acknowledgement; do not report any change as completed. Processing starts after your final reply."}
|
|
126
128
|
if bound is None:
|
|
127
129
|
return {"execution_status": "not_requested", "saved": False, "model_calls": 0}
|
|
128
130
|
# A started intent already owns its source and allowance. Compression
|
|
@@ -138,7 +140,10 @@ def remember_turn(service, *, phase, retrieval_id=None, source=None, session_id=
|
|
|
138
140
|
turn, _ = _window(service, source, session_id, turn_key(turn_id))
|
|
139
141
|
users = [event for event in turn.events if event.role == "user"]
|
|
140
142
|
request = "\n\n".join(event.content for event in users)
|
|
141
|
-
|
|
143
|
+
# One host completion evaluates the entire captured turn. The actual
|
|
144
|
+
# user request still bounds explicit retention; the assistant remains
|
|
145
|
+
# a visible source, never authority for new user obligations.
|
|
146
|
+
refs = [event.event_key for event in turn.events]
|
|
142
147
|
selection, request = bind_selection(intent, refs, request)
|
|
143
148
|
run = load_run(processed, explicit_run_id(source, session_id, selection))
|
|
144
149
|
if run is not None and run["status"] in {"completed", "completed_with_unresolved", "blocked", "failed", "cancelled"}:
|
|
@@ -155,6 +160,32 @@ def remember_turn(service, *, phase, retrieval_id=None, source=None, session_id=
|
|
|
155
160
|
return result
|
|
156
161
|
|
|
157
162
|
|
|
163
|
+
def settle_completed_intent_unlocked(processed, work, turn):
|
|
164
|
+
"""Consume only the matching, committed full-turn authority in the same ledger write."""
|
|
165
|
+
from .incremental_journal import public_result
|
|
166
|
+
from .turn_plan import input_digest
|
|
167
|
+
origin = work.get("host_retention_origin")
|
|
168
|
+
if (origin is None or not work.get("receipt_settled")
|
|
169
|
+
or public_result(work)["execution_status"] != "completed"
|
|
170
|
+
or (work["source"], work["session_id"], work["turn_key"])
|
|
171
|
+
!= (turn.source, turn.session_id, turn.turn_key)
|
|
172
|
+
or work["source_digest"] != input_digest(turn)
|
|
173
|
+
or {e["event_key"] for e in work["evidence"] if e["use"] == "new"} != set(turn.event_keys)):
|
|
174
|
+
return False
|
|
175
|
+
identity = digest([origin[k] for k in ("source", "session_id", "turn_id")])
|
|
176
|
+
pending = pending_intents(processed)
|
|
177
|
+
if pending.get(identity) != origin:
|
|
178
|
+
return False
|
|
179
|
+
try:
|
|
180
|
+
check_run_authority(processed, work)
|
|
181
|
+
except ValueError as error:
|
|
182
|
+
if str(error) != "source_recording_revoked":
|
|
183
|
+
raise
|
|
184
|
+
return False
|
|
185
|
+
pending.pop(identity)
|
|
186
|
+
return True
|
|
187
|
+
|
|
188
|
+
|
|
158
189
|
def complete_for_captured_turn(service, turn, *, model=None, router=None):
|
|
159
190
|
"""Resume durable queued intent after host restart, without borrowing a newer turn."""
|
|
160
191
|
with service.vault.lock():
|
|
@@ -165,3 +196,16 @@ def complete_for_captured_turn(service, turn, *, model=None, router=None):
|
|
|
165
196
|
_, row = bound
|
|
166
197
|
return remember_turn(service, phase="complete", source=turn.source, session_id=turn.session_id,
|
|
167
198
|
turn_id=row["turn_id"], model=model, router=router)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def covers_complete_turn(service, turn, result):
|
|
202
|
+
"""Only a verified immutable full-source commit may replace automatic work."""
|
|
203
|
+
from .incremental_journal import load_work
|
|
204
|
+
from .turn_plan import input_digest
|
|
205
|
+
with service.vault.lock():
|
|
206
|
+
processed = _read_processed(service.vault.processed_state_path)
|
|
207
|
+
work = load_work(processed, result.get('commit_work_id', ''))
|
|
208
|
+
if (work is None or not work.get('host_retention_origin') or not work['receipt_settled']
|
|
209
|
+
or work['source_digest'] != input_digest(turn)):
|
|
210
|
+
return False
|
|
211
|
+
return {e['event_key'] for e in work['evidence'] if e['use'] == 'new'} == set(turn.event_keys)
|
|
@@ -105,7 +105,7 @@ def _freeze_operation(proposal: dict[str, Any], snapshot: Any, now: str, active:
|
|
|
105
105
|
else:
|
|
106
106
|
value.update(memory_id=before.memory_id, created=before.created, updated=now)
|
|
107
107
|
after = Memory.from_mapping(value)
|
|
108
|
-
if before is not None and dedup_digest(before.to_dict()) == dedup_digest(after.to_dict()):
|
|
108
|
+
if before is not None and not op.get("merge_from") and dedup_digest(before.to_dict()) == dedup_digest(after.to_dict()):
|
|
109
109
|
op.update(action="NO_CHANGE", expected_revision=revision_digest(before))
|
|
110
110
|
return op
|
|
111
111
|
if before is None:
|
|
@@ -118,8 +118,14 @@ def _freeze_operation(proposal: dict[str, Any], snapshot: Any, now: str, active:
|
|
|
118
118
|
return op
|
|
119
119
|
from .incremental_dates import source_basis
|
|
120
120
|
provenance = [source_basis(e) for e in state["evidence"] if e["ref"] in op["evidence"]]
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
old_sources, old_meta = (before.sources, before.extra) if before else ([], {})
|
|
122
|
+
if op.get("merge_from"):
|
|
123
|
+
from .source_policy import merge_memory_provenance
|
|
124
|
+
peers = [Memory.from_mapping(t["memory"]) for t in state["targets"].values()
|
|
125
|
+
if t["memory"]["memory_id"] in op["merge_from"]]
|
|
126
|
+
old_sources, old_meta = merge_memory_provenance([before, *peers])
|
|
127
|
+
op["merge_revisions"] = {m.memory_id: revision_digest(m) for m in peers}
|
|
128
|
+
after.sources, source_meta = merge_sources(old_sources, provenance, extra=old_meta)
|
|
123
129
|
after.extra.update(source_meta)
|
|
124
130
|
after.extra["incremental_operation_id"] = op["operation_id"]
|
|
125
131
|
if any(t["memory"]["memory_id"] == op["memory_id"] and t.get("basis_status") == "external_change_detected"
|
|
@@ -162,8 +168,17 @@ def _source_valid(service, processed, work):
|
|
|
162
168
|
|
|
163
169
|
|
|
164
170
|
def _settle_source(service, processed, work, *, source_valid):
|
|
171
|
+
whole_host_turn = False
|
|
172
|
+
if source_valid and work.get("host_retention_origin") is not None:
|
|
173
|
+
# A scoped selection alone cannot prove whole-turn coverage. Only a
|
|
174
|
+
# verified host intent, evaluated against every current source event,
|
|
175
|
+
# may settle the automatic route as well.
|
|
176
|
+
turn, _ = _window(service, work["source"], work["session_id"], work["turn_key"])
|
|
177
|
+
selected = work.get("binding", {}).get("arguments", {}).get("selection", {})
|
|
178
|
+
whole_host_turn = (set(selected.get("source_refs", [])) == set(turn.event_keys)
|
|
179
|
+
== {e["event_key"] for e in work["evidence"] if e["use"] == "new"})
|
|
165
180
|
if (work.get("request_kind", "automatic") == "explicit_remember"
|
|
166
|
-
and not any(e.get("explicit_input") for e in work["evidence"])):
|
|
181
|
+
and not whole_host_turn and not any(e.get("explicit_input") for e in work["evidence"])):
|
|
167
182
|
# This authorization only selected some material. Its own work receipt
|
|
168
183
|
# settles it; it never consumes the automatic turn or changes cleanup.
|
|
169
184
|
return
|
|
@@ -232,6 +247,18 @@ def _resume_unlocked(service, processed, work):
|
|
|
232
247
|
if not guard_matches(current_guard, work["scope_guard"], applied_additions(work)):
|
|
233
248
|
raise ValueError("scope_registry_changed")
|
|
234
249
|
if op["action"] in {"CREATE", "UPDATE"}:
|
|
250
|
+
if op.get("merged_into"):
|
|
251
|
+
survivor = next((item for item in work["operations"]
|
|
252
|
+
if item.get("memory_id") == op["merged_into"]), None)
|
|
253
|
+
current, _ = service._revision_target_unlocked(op["merged_into"])
|
|
254
|
+
if (survivor is None or survivor["state"] not in {"applied", "settled"}
|
|
255
|
+
or current is None or revision_digest(current.memory) != survivor.get("replacement_revision")):
|
|
256
|
+
raise ValueError("merge_survivor_not_committed")
|
|
257
|
+
op["merge_revisions"] = {op["merged_into"]: survivor["replacement_revision"]}
|
|
258
|
+
for identity, expected in op.get("merge_revisions", {}).items():
|
|
259
|
+
current, _ = service._revision_target_unlocked(identity)
|
|
260
|
+
if current is None or revision_digest(current.memory) != expected:
|
|
261
|
+
raise ValueError("merge_target_changed")
|
|
235
262
|
# Pin the configuration already authorized above through
|
|
236
263
|
# the shared writer's history/head boundary as well.
|
|
237
264
|
guarded_op = {**op, "scope_guard": current_guard} if work.get("scope_guard") is not None else op
|
|
@@ -264,6 +291,10 @@ def _resume_unlocked(service, processed, work):
|
|
|
264
291
|
# These two changes share the final atomic ledger replacement.
|
|
265
292
|
work["receipt_settled"] = True
|
|
266
293
|
_settle_source(service, processed, work, source_valid=source_valid)
|
|
294
|
+
if source_valid and work.get("host_retention_origin") is not None:
|
|
295
|
+
from .host_retention import settle_completed_intent_unlocked
|
|
296
|
+
turn, _ = _window(service, work["source"], work["session_id"], work["turn_key"])
|
|
297
|
+
settle_completed_intent_unlocked(processed, work, turn)
|
|
267
298
|
save_work(service, processed, work)
|
|
268
299
|
except OSError as error:
|
|
269
300
|
for op in work["operations"]:
|