memleaf 0.2.87__tar.gz → 0.2.89__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.87 → memleaf-0.2.89}/CHANGELOG.md +50 -0
- {memleaf-0.2.87/src/memleaf.egg-info → memleaf-0.2.89}/PKG-INFO +4 -4
- {memleaf-0.2.87 → memleaf-0.2.89}/README.en.md +3 -3
- {memleaf-0.2.87 → memleaf-0.2.89}/README.md +3 -3
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/hermes-source-comparison-context.md +7 -3
- {memleaf-0.2.87 → memleaf-0.2.89}/pyproject.toml +1 -1
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/__init__.py +1 -1
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/adapters/hermes.py +4 -3
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/hermes_provider/README.md +2 -2
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/hermes_provider/_mcp_client.py +3 -1
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/hermes_provider/_provider.py +19 -11
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/hermes_provider/plugin.yaml +1 -1
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/host_retention.py +29 -1
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/incremental_commit.py +5 -1
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/incremental_preview.py +13 -12
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/incremental_prompts.py +4 -3
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/incremental_protocol.py +2 -2
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/incremental_semantics.py +27 -6
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/mcp_server.py +84 -12
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/processing_route.py +16 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/retrieval_gate.py +74 -1
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/service.py +20 -1
- {memleaf-0.2.87 → memleaf-0.2.89/src/memleaf.egg-info}/PKG-INFO +4 -4
- {memleaf-0.2.87 → memleaf-0.2.89}/LICENSE +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/MANIFEST.in +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/acceptance-evidence-guide.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/automatic-processing-route.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/body-compaction.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/candidate-reconciliation.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/capture-budget-design.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/config-migrations.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/core-refactor.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/development-closeout.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/evidence-retention.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/explicit-memory-update.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/extraction-latency.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/failed-run-recovery.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/gate-evidence-boundary.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/general-processing.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/hermes-mcp-runtime.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/hermes-provider-compatibility.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/host-acceptance-runbook.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/immutable-release-assets.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/incremental-commit-contract.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/incremental-execution.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/incremental-native-comparison.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/incremental-partial-recovery.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/incremental-planner-preview.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/incremental-runtime-contract.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/incremental-scope-registration.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/incremental-selected-retention.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/installed-artifact-verification.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/memory-lifecycle-tools.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/memory-state-contract.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/migration-preflight.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/processing-quality-acceptance.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/public-query-integrity.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/query-performance.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/remember-incremental-route.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/runtime-state-retention.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/semantic-extraction-protocol.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/source-work-contract.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/docs/v0.2.26-processing-status.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/examples/README.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/examples/basic_usage.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/examples/incremental_acceptance.json +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/examples/mcp_stdio.ndjson +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/examples/query_benchmark.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/install.ps1 +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/install.sh +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/setup.cfg +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/__main__.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/acceptance.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/adapters/__init__.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/adapters/antigravity.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/adapters/base.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/adapters/codex.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/admission.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/batch_review.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/budget.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/capture.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/cli.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/compaction.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/config.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/create_coordinator.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/credentials.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/evidence_budget.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/evidence_policy.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/evidence_structure.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/evidence_syntax.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/explicit_text_source.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/extraction_budget.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/extraction_capability.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/extraction_work_state.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/frontmatter.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/hermes_provider/__init__.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/hermes_provider/_shared.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/hermes_provider/evidence_budget.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/hermes_provider/provider_compatibility.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/hermes_runtime.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/host_events.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/host_runtime.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/host_turn_identity.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/inbox.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/incremental_dates.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/incremental_execution.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/incremental_explicit.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/incremental_failed_recovery.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/incremental_journal.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/incremental_merge.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/incremental_native.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/incremental_partial.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/incremental_recovery.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/incremental_run_state.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/incremental_runtime.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/incremental_scopes.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/incremental_selection.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/index.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/inspection.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/installer.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/legacy_update_journal.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/llm/__init__.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/llm/base.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/llm/claude_compatible.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/llm/gemini.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/llm/openai_compatible.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/llm/router.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/llm/thinking.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/locking.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/memory_commit.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/memory_planner.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/memory_retraction.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/memory_update.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/memory_writer.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/migration.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/model_capabilities.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/model_discovery.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/model_execution.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/models.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/native_index.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/native_registration.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/parallel_model.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/planning_candidates.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/planning_context.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/process_common.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/process_jobs.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/process_journal.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/process_owner.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/processing.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/prompts.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/provenance.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/provider_compatibility.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/query_clock.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/query_progress.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/query_scan.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/receipt_codec.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/recording_policy.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/redaction.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/remember_route.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/retention.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/retrieval.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/runtime_retention.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/scope_maintenance.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/scope_state.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/semantic_maintenance.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/semantic_protocol.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/single_pass_memory_planner.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/single_pass_plan.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/source_policy.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/state_layout.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/subprocess_flags.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/summary_batch.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/target_reconciliation.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/turn_audit.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/turn_plan.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/update_coordinator.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/update_review.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/validation.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf/vault.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf.egg-info/SOURCES.txt +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf.egg-info/dependency_links.txt +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf.egg-info/entry_points.txt +0 -0
- {memleaf-0.2.87 → memleaf-0.2.89}/src/memleaf.egg-info/top_level.txt +0 -0
|
@@ -4,6 +4,56 @@ All notable changes to memleaf are documented here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.2.89 — 2026-10-04
|
|
8
|
+
|
|
9
|
+
### Complete comparison context and bound tool continuation
|
|
10
|
+
|
|
11
|
+
- Remove the 20-memory comparison ceiling and capture truncation. Keep all
|
|
12
|
+
required read, receipt and revision targets, limiting only optional recall
|
|
13
|
+
and the existing total request size. Preserve strict source, scope and
|
|
14
|
+
missing-target validation; incomplete historical capsules remain protected.
|
|
15
|
+
- Use one discoverable Hermes MCP `save_turn` route for source-bound retention
|
|
16
|
+
and lifecycle requests. Stop exposing a competing native save schema while
|
|
17
|
+
preserving its legacy handler and internal completion interface. Queueing
|
|
18
|
+
remains idempotent and does not claim that changes have already been saved.
|
|
19
|
+
- Bind long-body continuation to server-owned memory IDs, versions, offsets
|
|
20
|
+
and history flags. Clients follow `body_continuation` without copying record
|
|
21
|
+
tuples; version changes require an explicit restart of the same target.
|
|
22
|
+
Preserve direct versioned reads and strict current-turn checks.
|
|
23
|
+
- Keep delivery dates and progress out of task titles, retaining years that
|
|
24
|
+
identify the subject. Verify body clauses against actual user-confirmed
|
|
25
|
+
facts and repair unsupported prose in the existing review pass without
|
|
26
|
+
changing evidence, ownership or deadlines. No model stage is added.
|
|
27
|
+
- Validation: 411 affected local checks, an isolated replay of the real
|
|
28
|
+
21-action conversation, and three real DeepSeek extraction/review samples
|
|
29
|
+
passed. Formal Vault data and historical jobs were not changed; updated
|
|
30
|
+
Hermes desktop behavior still requires installation and a host restart.
|
|
31
|
+
|
|
32
|
+
## 0.2.88 — 2026-10-04
|
|
33
|
+
|
|
34
|
+
### Reliable queued retention and bounded action queries
|
|
35
|
+
|
|
36
|
+
- Use the processing timeout for Hermes whole-turn retention completion while
|
|
37
|
+
keeping queue operations short. Settle fully committed retention intents in
|
|
38
|
+
the same atomic ledger write as their receipts, including interrupted host
|
|
39
|
+
callbacks. Reconcile older matching committed intents without model calls;
|
|
40
|
+
partial, changed-source and revoked-authority intents remain protected.
|
|
41
|
+
- Route conversational saves and lifecycle requests through Hermes's native
|
|
42
|
+
whole-turn retention tool. Hide bare `remember` and `update_memory` from the
|
|
43
|
+
Hermes MCP configuration while preserving those public tools for other
|
|
44
|
+
clients. Return a clear queued acknowledgement before background processing.
|
|
45
|
+
- Add `list_todos(view=full)` with bounded action bodies and explicit versioned
|
|
46
|
+
continuation for long bodies. Server-managed clients can use `continue=true`
|
|
47
|
+
with their current retrieval identity instead of copying opaque cursors.
|
|
48
|
+
Preserve directory/cursor clients, host-owned read accounting and strict
|
|
49
|
+
stale-turn, cursor and filter validation.
|
|
50
|
+
- Keep absent message times unknown. Explicitly supplied deadlines work without
|
|
51
|
+
message timestamps; no capture-time fallback or source-time repair is added.
|
|
52
|
+
- Validation: affected local regressions, a zero-model-call replay of the actual
|
|
53
|
+
committed-intent failure, and isolated real DeepSeek save/deadline and global
|
|
54
|
+
action-pagination scenarios passed. No additional product model calls,
|
|
55
|
+
production Vault writes or historical model retries were introduced.
|
|
56
|
+
|
|
7
57
|
## 0.2.87 — 2026-10-04
|
|
8
58
|
|
|
9
59
|
### Shared source-bound maintenance with bounded model cost
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.89
|
|
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.89。**
|
|
27
27
|
> 自动提炼和复核共用同一条“未来记忆价值”标准:模型综合未来复用、信息增量、再次读取时的直接可用性和忘记成本,只保留对未来理解、判断或行动有实质影响的最小核心;没有明确价值的信息不提炼,不按具体业务场景硬编码排除。
|
|
28
28
|
> 提炼先比较旧事项中的编号、目标、进展、责任和期限;补全未知信息也应维护原事项,另建独立待办不能替代项目事实更新。确认重复的记忆在现有处理请求中合并,保留历史与来源;项目和独立任务保持各自生命周期,Core 不按标题强行合并。
|
|
29
29
|
> 新请求兼容等价的字段表示;未知执行人保持 `null`,新增或变更负责人和期限需要真实来源;等待条件在正文维护,不再提炼 `waiting_on`。行动及相关字段经过有界语义复核,提炼与复核共用每份工作的最多五次模型请求额度,成功即停止;复核失败不写入。来源时间用于解析相对日期,不补造事实日期。
|
|
@@ -166,13 +166,13 @@ python -m pip install -U memleaf && python -m memleaf install
|
|
|
166
166
|
3. 发现并保存可用的聊天模型路由;Hermes CLI 返回的脱敏凭证不会被当成真实 API key,而会继续尝试环境变量和 Hermes `.env`;已有有效 memleaf 路由会保留;
|
|
167
167
|
4. 激活 `memory.provider=memleaf`;
|
|
168
168
|
5. 通过 Hermes 官方 CLI 配置 memleaf MCP;
|
|
169
|
-
6. 配置 MCP lazy/idle 生命周期,以及绑定真实用户消息的 Hermes
|
|
169
|
+
6. 配置 MCP lazy/idle 生命周期,以及绑定真实用户消息的 Hermes MCP 保存入口;
|
|
170
170
|
7. 验证 MCP Server 能发现当前版本声明的全部工具;
|
|
171
171
|
8. 写入本地 Agent 状态索引。
|
|
172
172
|
|
|
173
173
|
完成后重启 Hermes。
|
|
174
174
|
|
|
175
|
-
Hermes
|
|
175
|
+
Hermes 显式保存使用 MCP `save_turn`:先绑定本轮并排队,轮次完成后再以真实用户消息提炼,排队不表示已保存。它与检索工具共用 Hermes 的延迟工具入口,Provider 不再另外暴露原生保存工具。安装器会在 Hermes 中屏蔽独立 MCP `remember`,保留其他工具选择;Core/Python API 和其他客户端的 `remember` 继续可用。仅升级 pip 包不足以更新 Provider 和这项工具配置。
|
|
176
176
|
|
|
177
177
|
如果没有检测到 Hermes、无法取得完整模型路由、Provider 激活失败或 MCP 工具发现验证失败,安装会明确返回失败,不会把未完成的接入报告为成功。
|
|
178
178
|
|
|
@@ -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.89.**
|
|
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
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
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.
|
|
@@ -147,13 +147,13 @@ Both installation paths automatically:
|
|
|
147
147
|
3. Discover and save a callable chat-model route. Redacted credentials returned by the Hermes CLI are never treated as real API keys; discovery falls through to environment variables and Hermes `.env`, while preserving an existing valid memleaf route.
|
|
148
148
|
4. Activate `memory.provider=memleaf`.
|
|
149
149
|
5. Configure the memleaf MCP entry through Hermes' official CLI.
|
|
150
|
-
6. Configure MCP lazy/idle lifecycle settings and the source-bound
|
|
150
|
+
6. Configure MCP lazy/idle lifecycle settings and the source-bound Hermes MCP save route.
|
|
151
151
|
7. Verify that the MCP server exposes all tools declared by the installed version.
|
|
152
152
|
8. Record the local Agent integration status.
|
|
153
153
|
|
|
154
154
|
Restart Hermes after installation.
|
|
155
155
|
|
|
156
|
-
Hermes uses
|
|
156
|
+
Hermes uses MCP `save_turn` to queue a request bound to the current turn, then retain the actual user messages after the turn completes. Queued does not mean saved. It shares the deferred MCP tool space with retrieval; the Provider no longer exposes a separate native save schema. The installer excludes standalone MCP `remember` in Hermes while preserving other tool choices; the Core/Python API and other clients keep `remember`. Upgrading only the pip package does not refresh the Provider or this tool configuration.
|
|
157
157
|
|
|
158
158
|
If Hermes cannot be detected, no complete model route can be configured, Provider activation fails, or MCP tool-discovery verification fails, the installer returns an explicit failure rather than reporting an incomplete integration as successful.
|
|
159
159
|
|
|
@@ -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.89。**
|
|
8
8
|
> 自动提炼和复核共用同一条“未来记忆价值”标准:模型综合未来复用、信息增量、再次读取时的直接可用性和忘记成本,只保留对未来理解、判断或行动有实质影响的最小核心;没有明确价值的信息不提炼,不按具体业务场景硬编码排除。
|
|
9
9
|
> 提炼先比较旧事项中的编号、目标、进展、责任和期限;补全未知信息也应维护原事项,另建独立待办不能替代项目事实更新。确认重复的记忆在现有处理请求中合并,保留历史与来源;项目和独立任务保持各自生命周期,Core 不按标题强行合并。
|
|
10
10
|
> 新请求兼容等价的字段表示;未知执行人保持 `null`,新增或变更负责人和期限需要真实来源;等待条件在正文维护,不再提炼 `waiting_on`。行动及相关字段经过有界语义复核,提炼与复核共用每份工作的最多五次模型请求额度,成功即停止;复核失败不写入。来源时间用于解析相对日期,不补造事实日期。
|
|
@@ -147,13 +147,13 @@ python -m pip install -U memleaf && python -m memleaf install
|
|
|
147
147
|
3. 发现并保存可用的聊天模型路由;Hermes CLI 返回的脱敏凭证不会被当成真实 API key,而会继续尝试环境变量和 Hermes `.env`;已有有效 memleaf 路由会保留;
|
|
148
148
|
4. 激活 `memory.provider=memleaf`;
|
|
149
149
|
5. 通过 Hermes 官方 CLI 配置 memleaf MCP;
|
|
150
|
-
6. 配置 MCP lazy/idle 生命周期,以及绑定真实用户消息的 Hermes
|
|
150
|
+
6. 配置 MCP lazy/idle 生命周期,以及绑定真实用户消息的 Hermes MCP 保存入口;
|
|
151
151
|
7. 验证 MCP Server 能发现当前版本声明的全部工具;
|
|
152
152
|
8. 写入本地 Agent 状态索引。
|
|
153
153
|
|
|
154
154
|
完成后重启 Hermes。
|
|
155
155
|
|
|
156
|
-
Hermes
|
|
156
|
+
Hermes 显式保存使用 MCP `save_turn`:先绑定本轮并排队,轮次完成后再以真实用户消息提炼,排队不表示已保存。它与检索工具共用 Hermes 的延迟工具入口,Provider 不再另外暴露原生保存工具。安装器会在 Hermes 中屏蔽独立 MCP `remember`,保留其他工具选择;Core/Python API 和其他客户端的 `remember` 继续可用。仅升级 pip 包不足以更新 Provider 和这项工具配置。
|
|
157
157
|
|
|
158
158
|
如果没有检测到 Hermes、无法取得完整模型路由、Provider 激活失败或 MCP 工具发现验证失败,安装会明确返回失败,不会把未完成的接入报告为成功。
|
|
159
159
|
|
|
@@ -40,9 +40,13 @@ assistant event, including the immutable capture receipt and payload digest.
|
|
|
40
40
|
It accepts no caller-supplied list of IDs through this interface. The capsule is
|
|
41
41
|
comparison metadata, not factual evidence, and its token is not projected into
|
|
42
42
|
the model request. Automatic preparation gives these required targets priority
|
|
43
|
-
alongside explicit-write receipts
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
alongside explicit-write receipts. There is no 20-ID truncation or target-count
|
|
44
|
+
ceiling: required targets expand the preferred candidate count; optional recall
|
|
45
|
+
only fills remaining slots. A missing target or an oversized request still blocks
|
|
46
|
+
preparation instead of silently dropping required context. The existing request
|
|
47
|
+
byte budget remains; no model call is added to capture. Legacy capsules already
|
|
48
|
+
marked overflow remain incomplete and cannot be silently reconstructed from a
|
|
49
|
+
later live gate.
|
|
46
50
|
|
|
47
51
|
A delayed callback can use its own unexpired gate even after a later turn starts.
|
|
48
52
|
Once the capsule is captured, gate expiry cannot erase it or break a duplicate
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Local-first Markdown memory core for AI agents."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.2.
|
|
3
|
+
__version__ = "0.2.89"
|
|
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
|
|
@@ -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,
|
|
@@ -10,14 +10,14 @@ Automatic capture and processing use only the visible user and assistant text.
|
|
|
10
10
|
Tool calls and results, email or attachment bodies, and other hidden payloads are
|
|
11
11
|
not automatic memory input.
|
|
12
12
|
Hermes uses MCP `search` and `read` with the current `retrieval_id` for recall.
|
|
13
|
-
The
|
|
13
|
+
The deferred MCP `save_turn` tool queues explicit retention bound to the current
|
|
14
14
|
turn; the completed capture supplies the original user messages and source times.
|
|
15
15
|
A queued response is not a saved memory. The installer excludes standalone MCP
|
|
16
16
|
`remember` in Hermes while preserving other configured tool choices. MCP still
|
|
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
|
|
20
|
+
The bound MCP route also handles conversational corrections, completion,
|
|
21
21
|
cancellation, reopening and withdrawal. A successful full-turn retention receipt
|
|
22
22
|
settles automatic extraction without another model request. Partial selections
|
|
23
23
|
remain separate. MCP lifecycle updates do not accept model-supplied source times;
|
|
@@ -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,12 @@ 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
|
+
"discover and call the deferred MCP tool mcp__memleaf__save_turn once with this turn's retrieval_id, "
|
|
1375
|
+
"using the exact name returned by tool_search through tool_call. It queues the actual "
|
|
1376
|
+
"whole turn; no rewritten content, target revision, or message time is needed. "
|
|
1377
|
+
"Use the returned user_message as the save acknowledgement. A queued request "
|
|
1378
|
+
"starts processing only after your final reply, so its changes are pending.\n"
|
|
1373
1379
|
"Memleaf is the active local-first memory provider. Each visible user "
|
|
1374
1380
|
"turn must call the configured memleaf MCP search tool at least once "
|
|
1375
1381
|
"before answering, including ordinary greetings; a no-match result is "
|
|
@@ -1383,8 +1389,10 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
1383
1389
|
"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
1390
|
"When the user asks for current "
|
|
1385
1391
|
"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
|
-
"
|
|
1392
|
+
"list_todos(view=full) instead of relevance search; omit scope for a global query. "
|
|
1393
|
+
"Use the returned continuation arguments until has_more=false, without copying a cursor. "
|
|
1394
|
+
"Each row includes a body; after list pagination, follow the returned body_continuation "
|
|
1395
|
+
"with MCP read until it is absent. The server owns the memory ID, version and offset. "
|
|
1388
1396
|
"Never exclude a todo because another Hermes session or another Agent created it. Hermes has a soft "
|
|
1389
1397
|
"observer only: do not claim a search happened unless the visible tool "
|
|
1390
1398
|
"messages show it. Visible Hermes "
|
|
@@ -1403,7 +1411,7 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
1403
1411
|
"file writes to simulate success, and do not infer automatic success merely because "
|
|
1404
1412
|
"active or history files exist. Automatic recall is a directory of "
|
|
1405
1413
|
"scope identifiers, hierarchy, and aliases only; it never contains "
|
|
1406
|
-
"memory IDs, titles, or bodies.
|
|
1414
|
+
"memory IDs, titles, or bodies. Do not infer message time from a deadline or submit assistant-written facts through legacy MCP remember. "
|
|
1407
1415
|
"Use deliberate remember/forget tools "
|
|
1408
1416
|
"only when the user explicitly asks for that operation. Automatic "
|
|
1409
1417
|
"capture and processing use only visible user and assistant text; "
|
|
@@ -2060,8 +2068,10 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
2060
2068
|
if not tool_name.startswith(prefix):
|
|
2061
2069
|
return None
|
|
2062
2070
|
name = tool_name[len(prefix):]
|
|
2063
|
-
if name not in {"search", "read", "list_todos", "scope_catalog", "remember", "update_memory"}:
|
|
2071
|
+
if name not in {"search", "read", "list_todos", "scope_catalog", "remember", "update_memory", "save_turn"}:
|
|
2064
2072
|
return None
|
|
2073
|
+
if name == "save_turn" and (not self._write_enabled or not self._gate_enabled):
|
|
2074
|
+
raise ValueError("memory_host_retention_disabled")
|
|
2065
2075
|
turn_id = _host_turn_id(turn_context)
|
|
2066
2076
|
session = turn_context.get("session_id") if isinstance(turn_context, Mapping) else None
|
|
2067
2077
|
ordinal = turn_context.get("turn_number") if isinstance(turn_context, Mapping) else None
|
|
@@ -2098,13 +2108,11 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
2098
2108
|
return bound
|
|
2099
2109
|
|
|
2100
2110
|
def get_tool_schemas(self) -> List[Dict[str, Any]]:
|
|
2101
|
-
#
|
|
2102
|
-
#
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
return [
|
|
2106
|
-
"parameters": {"type": "object", "properties": {"retrieval_id": {"type": "string"}},
|
|
2107
|
-
"required": ["retrieval_id"], "additionalProperties": False}}]
|
|
2111
|
+
# One discoverable MCP save_turn route avoids the host's split between
|
|
2112
|
+
# native provider tools and deferred tool_call dispatch. Keep the old
|
|
2113
|
+
# handler below for already registered native clients, without exposing
|
|
2114
|
+
# two competing ways to save the same turn.
|
|
2115
|
+
return []
|
|
2108
2116
|
|
|
2109
2117
|
def handle_tool_call(self, tool_name: str, args: Dict[str, Any], **kwargs: Any) -> str:
|
|
2110
2118
|
if (tool_name != "memleaf_remember" or not self._write_enabled or not self._gate_enabled
|
|
@@ -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
|
|
@@ -158,6 +160,32 @@ def remember_turn(service, *, phase, retrieval_id=None, source=None, session_id=
|
|
|
158
160
|
return result
|
|
159
161
|
|
|
160
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
|
+
|
|
161
189
|
def complete_for_captured_turn(service, turn, *, model=None, router=None):
|
|
162
190
|
"""Resume durable queued intent after host restart, without borrowing a newer turn."""
|
|
163
191
|
with service.vault.lock():
|
|
@@ -56,7 +56,7 @@ def _arguments(source, session_id, turn_id, scope, priority_memory_ids, candidat
|
|
|
56
56
|
source, session_id = safe_component(source, "source"), safe_component(session_id, "session id")
|
|
57
57
|
if not isinstance(turn_id, str) or not turn_id or len(turn_id) > 800:
|
|
58
58
|
raise ValueError("invalid_turn_id")
|
|
59
|
-
if type(candidate_limit) is not int or
|
|
59
|
+
if type(candidate_limit) is not int or candidate_limit < 1:
|
|
60
60
|
raise ValueError("invalid_candidate_limit")
|
|
61
61
|
if type(allow_new_scopes) is not bool:
|
|
62
62
|
raise ValueError("invalid_context_flags")
|
|
@@ -291,6 +291,10 @@ def _resume_unlocked(service, processed, work):
|
|
|
291
291
|
# These two changes share the final atomic ledger replacement.
|
|
292
292
|
work["receipt_settled"] = True
|
|
293
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)
|
|
294
298
|
save_work(service, processed, work)
|
|
295
299
|
except OSError as error:
|
|
296
300
|
for op in work["operations"]:
|
|
@@ -30,13 +30,14 @@ def _prepare_incremental_unlocked(service: Any, *, source: str, session_id: str,
|
|
|
30
30
|
|
|
31
31
|
At this stage evidence units are complete visible messages, not per-sentence
|
|
32
32
|
heuristic fragments. Caller-provided target IDs are required context and are
|
|
33
|
-
never dropped to fit
|
|
33
|
+
never dropped to fit a count limit. candidate_limit bounds optional recall;
|
|
34
|
+
required comparison targets expand that count within the request byte budget.
|
|
34
35
|
"""
|
|
35
36
|
source = safe_component(source, "source")
|
|
36
37
|
session_id = safe_component(session_id, "session id")
|
|
37
38
|
if not isinstance(turn_id, str) or not turn_id:
|
|
38
39
|
raise ValueError("invalid_turn_id")
|
|
39
|
-
if type(candidate_limit) is not int or
|
|
40
|
+
if type(candidate_limit) is not int or candidate_limit < 1:
|
|
40
41
|
raise ValueError("invalid_candidate_limit")
|
|
41
42
|
from .incremental_scopes import registry_view, resolve_scope
|
|
42
43
|
registry, scope_guard, scope_aliases = registry_view(service.vault.config())
|
|
@@ -47,8 +48,6 @@ def _prepare_incremental_unlocked(service: Any, *, source: str, session_id: str,
|
|
|
47
48
|
if isinstance(priority_memory_ids, (str, bytes)):
|
|
48
49
|
raise ValueError("invalid_priority_ids")
|
|
49
50
|
priority = list(dict.fromkeys(priority_memory_ids))
|
|
50
|
-
if len(priority) > candidate_limit:
|
|
51
|
-
raise ValueError("blocked_context")
|
|
52
51
|
for identity in priority:
|
|
53
52
|
safe_component(identity, "memory id")
|
|
54
53
|
# Do not use the mutation boundary: it would resume compaction writes.
|
|
@@ -149,15 +148,13 @@ def _prepare_incremental_unlocked(service: Any, *, source: str, session_id: str,
|
|
|
149
148
|
if context.get("overflow") is not False:
|
|
150
149
|
raise ValueError("blocked_context")
|
|
151
150
|
identities = context.get("memory_ids")
|
|
152
|
-
if not isinstance(identities, list)
|
|
151
|
+
if not isinstance(identities, list):
|
|
153
152
|
raise ValueError("invalid_comparison_context")
|
|
154
153
|
for identity in identities:
|
|
155
154
|
safe_component(identity, "memory id")
|
|
156
155
|
read_ids.extend(identities)
|
|
157
156
|
priority = list(dict.fromkeys(explicit_ids + read_ids + priority))
|
|
158
157
|
priority += sorted(x for x in prior_ids if x.casefold() in records and x not in priority)
|
|
159
|
-
if len(priority) > candidate_limit:
|
|
160
|
-
raise ValueError("blocked_context")
|
|
161
158
|
chosen = []
|
|
162
159
|
for identity in priority:
|
|
163
160
|
if identity.casefold() not in records:
|
|
@@ -165,15 +162,19 @@ def _prepare_incremental_unlocked(service: Any, *, source: str, session_id: str,
|
|
|
165
162
|
chosen.append(records[identity.casefold()])
|
|
166
163
|
from .planning_candidates import ComparisonRanker
|
|
167
164
|
ranker = ComparisonRanker(records)
|
|
165
|
+
# Required reads/receipts are supplied in full. Recall adds only enough
|
|
166
|
+
# optional targets to reach the caller's preferred count, without ranking
|
|
167
|
+
# unrelated memories when required context already exceeds it.
|
|
168
|
+
optional_limit = min(max(0, candidate_limit - len(chosen)), len(records))
|
|
169
|
+
seen = {memory.memory_id.casefold() for memory in chosen}
|
|
168
170
|
query_rows = []
|
|
169
|
-
for event in evidence:
|
|
171
|
+
for event in evidence if optional_limit else ():
|
|
170
172
|
for is_native in (False, True):
|
|
171
|
-
keys = [key for key in records if (key in native_keys) == is_native]
|
|
172
|
-
query_rows.append([records[key] for key in ranker.rank(event["text"], keys,
|
|
173
|
+
keys = [key for key in records if key not in seen and (key in native_keys) == is_native]
|
|
174
|
+
query_rows.append([records[key] for key in ranker.rank(event["text"], keys, optional_limit)])
|
|
173
175
|
# Reuse the pure retrieval functions, not service._search_unlocked(),
|
|
174
176
|
# whose index accessor may rebuild files or recover compaction.
|
|
175
|
-
|
|
176
|
-
for rank in range(candidate_limit):
|
|
177
|
+
for rank in range(optional_limit):
|
|
177
178
|
for candidates in query_rows:
|
|
178
179
|
if len(chosen) >= candidate_limit:
|
|
179
180
|
break
|
|
@@ -6,9 +6,10 @@ INCREMENTAL_SYSTEM = """从完整可见对话提炼长期记忆,返回严格 J
|
|
|
6
6
|
- 用户明确撤回旧事实或偏好:UPDATE 原 target,patch.validity=retracted;撤回说明转入历史,不作为仍有效的偏好保留。用户明确给出替代事实/新偏好:UPDATE 原 target 为新的有效内容。撤回后“需要时另说”不代表已建立替代偏好。
|
|
7
7
|
- user 的已确认事实、决定、持续目标、责任、期限、进展、偏好可保留。临时操作请求及普通应答不自动产生长期待办。
|
|
8
8
|
- assistant 仅能解释已确认事实、报告有依据的已执行结果。assistant 的建议、计划、推测、额外安排不产生用户/第三人义务、承诺、偏好或期限;用户确认一件事不等于确认回复的其他建议。不要把“建议以后做”改写成“需要做”。
|
|
9
|
+
- 逐分句核验正文:assistant 增加的原因、影响、条件或后续步骤,user 未确认则省略;其余已确认事实保留。
|
|
9
10
|
- use=new 是本轮变化依据;context、旧目标、explicit_writes 仅供比较,不能独立产生新事实。逐项核对证据是否支持拟写事实、归属、执行人和日期。
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
比较 new 与目标 observed 的先后;已被较新 context/目标覆盖的变化不重放,无变化 NO_CHANGE,不借 context 清空 deadline。按身份/编号、主体、目标和业务上下文比较全部候选,包含已完成项和跨会话来源。补编号、纠正状态、补有依据的结构字段均 UPDATE 原目标;完整覆盖才 NO_CHANGE。独立新事项 CREATE;同一变化同步维护全部相关候选,旧职责不可遗漏。项目与独立任务各保留生命周期;确为同一事项的重复记录才 MERGE,不凭标题相似合并。保留其余有效旧事实,正文表达当前状态,不追加日志。
|
|
12
13
|
|
|
13
14
|
动作:
|
|
14
15
|
CREATE:action、非空 evidence、memory;memory 必填 type/scope/title/body。
|
|
@@ -18,13 +19,13 @@ NO_CHANGE:action、非空 evidence、target;正文及结构化字段均已
|
|
|
18
19
|
DEFERRED:action、非空 evidence、reason、need;reason=missing_identity|missing_context|conflict,need 说明无法确定的内容。
|
|
19
20
|
NO_MEMORY:仅 action;自动模式整轮无值得维护/新增的内容时独占 items。
|
|
20
21
|
每项 evidence 是引用字符串数组,例如 ["e1","e2"],不能复制整个证据对象。至少一个 use=new 引用,必须实际支持本项,不要求覆盖每条消息。
|
|
21
|
-
|
|
22
|
+
结构示例:
|
|
22
23
|
{"action":"CREATE","evidence":["e1"],"memory":{"type":"fact","scope":"unscoped","title":"…","body":"…"}}
|
|
23
24
|
{"action":"UPDATE","evidence":["e1"],"target":"m1","patch":{"body":"…"}}
|
|
24
|
-
{"action":"NO_CHANGE","evidence":["e1"],"target":"m1"}
|
|
25
25
|
{"items":[{"action":"NO_MEMORY"}]}
|
|
26
26
|
|
|
27
27
|
字段:
|
|
28
|
+
title 标识事项,不放交期/发生日/进度,改存正文与结构字段;旧标题交期一并清理。名称固有年度等身份日期保留。
|
|
28
29
|
type=fact|todo|preference|project|event|identity|other。有独立完成条件的持续行动设 actionable:true(todo 隐含);事实类别与行动属性可分开表达。
|
|
29
30
|
status=active|completed|cancelled;完成/取消/交接同时纠正冲突的 title/body;独立未完行动不能被兄弟项完成吞掉。重新开启 UPDATE 使用 reopen:true 和 patch.status=active,需要较新明确依据。
|
|
30
31
|
assignee 是实际执行人,只有明确由用户本人执行才 user,其他执行人用明确名称,未知为 null。用户记录/转发/协调不证明本人执行。等待依赖、卡点仅在正文维护,按最新事实替换过时内容,不生成等待字段。
|
|
@@ -24,7 +24,7 @@ from .turn_plan import revision_digest
|
|
|
24
24
|
from .validation import ModelOutputError, parse_strict_json
|
|
25
25
|
|
|
26
26
|
PROTOCOL_VERSION = "incremental-items-v1"
|
|
27
|
-
SEMANTIC_PROTOCOL = "incremental-turn-
|
|
27
|
+
SEMANTIC_PROTOCOL = "incremental-turn-v17"
|
|
28
28
|
EXTRACTION_CONTRACT = "field-reviewed-v1"
|
|
29
29
|
MAX_BYTES = 128 * 1024
|
|
30
30
|
MAX_ITEMS = 64
|
|
@@ -341,7 +341,7 @@ class PlanningSnapshot:
|
|
|
341
341
|
for status in basis_statuses.values())):
|
|
342
342
|
raise ValueError("invalid_basis_status")
|
|
343
343
|
targets = targets or {}
|
|
344
|
-
if
|
|
344
|
+
if writable and set(writable) - set(targets):
|
|
345
345
|
raise ValueError("invalid_targets")
|
|
346
346
|
for ref, memory in targets.items():
|
|
347
347
|
if not re.fullmatch(r"m[1-9][0-9]*", ref) or not isinstance(memory, Memory):
|
|
@@ -15,10 +15,11 @@ REVIEW_SYSTEM = """核验拟写入的持续行动。original_input 是唯一事
|
|
|
15
15
|
每个 draft.items 都必须有且只有一个 decision,item 是从 0 开始的位置。keep 判断该事项本身是否由原文确立且持续值得保留。临时操作及助手额外建议不能产生用户长期待办,原话只证明另一个操作时 keep:false。独立事实不因其他候选有误而拒绝。
|
|
16
16
|
keep:true 时,field_support 必须逐个列出候选 memory/patch 中的业务字段,每个布尔值判断该字段的拟议值是否受来源支持;不列 responsibility_basis、effective、at、reopen 等引用/控制字段。没有 replace 的 NO_CHANGE/DEFERRED/NO_MEMORY 的 field_support 为 {}。keep:false 时 field_support 为 {}。unverified_fields 是系统发现未能验证引用的可选责任字段,必须判为 false;不能补造引用。这不影响其他有依据的事实或任务。归属/客户尚未确定不自动表示行动被阻塞。
|
|
17
17
|
不能仅检查引文存在:必须判断原话是否确立该具体行动、责任、状态和期限。assistant 的建议/计划不能赋予 user 新义务。转发、协调、记录不等于本人执行;支持旧任务不等于接受新安排。
|
|
18
|
+
body 必须逐个事实分句核验;assistant 自行增加的原因、影响判断、条件或后续步骤不受 user 原话支持,不能因整段主题相符就判 true。剔除无依据分句,保留其余已确认事实,可用 replace 修正 CREATE 的 title/body;没有修正时 body:false。title 只标识事项,交期/发生日/进度放正文与结构字段;名称固有的年度等身份信息保留。UPDATE/NO_CHANGE 的旧标题仍含交期时,在现有 replace/updates 中一起维护。
|
|
18
19
|
尤其检查拟改 deadline:依赖条件、交接执行人、进度、尚未完成都不证明原期限已取消;只有明确取消/替换这个期限才支持变更。没有明确期限变化时 deadline:false,原期限沿用。卡点和等待条件仅在正文中维护,并清除已经解决的旧卡点。
|
|
19
20
|
新引用只能来自 new;context 和旧目标只供比较,不能重新推动旧变化。候选若仅重新陈述旧目标已覆盖的内容,不应作为新 UPDATE。同一事项补充/纠正维护原目标,独立事项允许 CREATE,不能凭标题相似合并。
|
|
20
21
|
逐字段判断后输出 decisions;对共同事实变更,检查所有候选记忆中是否仍有旧职责或旧状态,不能认可只更新一条而遗留冲突。确认 MERGE 各方确为同一事项且完整保留有效内容;独立生命周期的项目/任务不可合并。
|
|
21
|
-
修正遗漏时,decision 可增加 replace(同 target 的完整 UPDATE
|
|
22
|
+
修正遗漏时,decision 可增加 replace(同 target 的完整 UPDATE,或原 CREATE 仅改 title/body 的完整副本),field_support 判断替换后的字段;CREATE 修正不得改变其 evidence、scope、type、责任、期限或引用。不得替换 DEFERRED,也不得新增义务或猜测未知。还可输出 updates:[{"row":UPDATE,"field_support":{...}}] 更新原 draft 未涉及的可写候选目标,证据仍只取原输入 new。没有修正就省略 replace/updates。每条修正必须保留仍有效的旧事实与原期限;撤回用 validity=retracted。不要输出解释。
|
|
22
23
|
"""
|
|
23
24
|
|
|
24
25
|
_METADATA = {"responsibility_basis", "effective", "at", "reopen"}
|
|
@@ -165,11 +166,30 @@ def apply_review(request, response):
|
|
|
165
166
|
raise ValueError
|
|
166
167
|
row = rows[i]
|
|
167
168
|
if "replace" in decision:
|
|
168
|
-
if
|
|
169
|
-
or row.get("action") not in {"UPDATE", "NO_CHANGE"}):
|
|
169
|
+
if i in data.get("invalid_representation_items", []) or not decision["keep"]:
|
|
170
170
|
raise ValueError
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
if row.get("action") == "CREATE":
|
|
172
|
+
replacement = deepcopy(decision["replace"])
|
|
173
|
+
if (not isinstance(replacement, dict) or replacement.get("action") != "CREATE"
|
|
174
|
+
or not isinstance(replacement.get("memory"), dict)
|
|
175
|
+
or not isinstance(row.get("memory"), dict)
|
|
176
|
+
or set(support) != fields(replacement)
|
|
177
|
+
or any(v is not True for v in support.values())):
|
|
178
|
+
raise ValueError
|
|
179
|
+
# Review can repair prose, not rewrite the draft's source,
|
|
180
|
+
# authority, identity, lifecycle or field annotations.
|
|
181
|
+
def without_prose(value):
|
|
182
|
+
value = deepcopy(value)
|
|
183
|
+
for key in ("title", "body"):
|
|
184
|
+
value["memory"].pop(key, None)
|
|
185
|
+
return value
|
|
186
|
+
if without_prose(row) != without_prose(replacement):
|
|
187
|
+
raise ValueError
|
|
188
|
+
elif row.get("action") in {"UPDATE", "NO_CHANGE"}:
|
|
189
|
+
replacement = _maintenance_update(decision["replace"], support, data["original_input"])
|
|
190
|
+
if replacement["target"] != row["target"]:
|
|
191
|
+
raise ValueError
|
|
192
|
+
else:
|
|
173
193
|
raise ValueError
|
|
174
194
|
row = rows[i] = replacement
|
|
175
195
|
data.get("unverified_fields", [[] for _ in rows])[i] = []
|
|
@@ -224,7 +244,8 @@ def apply_review(request, response):
|
|
|
224
244
|
rows[i] = None
|
|
225
245
|
rows = [r for r in rows if r is not None]
|
|
226
246
|
additions = review.get("updates", [])
|
|
227
|
-
|
|
247
|
+
from .incremental_protocol import MAX_ITEMS
|
|
248
|
+
if not isinstance(additions, list) or len(rows) + len(additions) > MAX_ITEMS:
|
|
228
249
|
raise ValueError
|
|
229
250
|
used = {row.get("target") for row in rows if isinstance(row, dict)}
|
|
230
251
|
for entry in additions:
|