memleaf 0.2.87__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.87 → memleaf-0.2.88}/CHANGELOG.md +25 -0
- {memleaf-0.2.87/src/memleaf.egg-info → memleaf-0.2.88}/PKG-INFO +2 -2
- {memleaf-0.2.87 → memleaf-0.2.88}/README.en.md +1 -1
- {memleaf-0.2.87 → memleaf-0.2.88}/README.md +1 -1
- {memleaf-0.2.87 → memleaf-0.2.88}/pyproject.toml +1 -1
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/__init__.py +1 -1
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/adapters/hermes.py +4 -3
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/hermes_provider/_mcp_client.py +3 -1
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/hermes_provider/_provider.py +11 -4
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/hermes_provider/plugin.yaml +1 -1
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/host_retention.py +29 -1
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/incremental_commit.py +4 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/mcp_server.py +34 -4
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/processing_route.py +16 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/retrieval_gate.py +25 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/service.py +20 -1
- {memleaf-0.2.87 → memleaf-0.2.88/src/memleaf.egg-info}/PKG-INFO +2 -2
- {memleaf-0.2.87 → memleaf-0.2.88}/LICENSE +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/MANIFEST.in +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/acceptance-evidence-guide.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/automatic-processing-route.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/body-compaction.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/candidate-reconciliation.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/capture-budget-design.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/config-migrations.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/core-refactor.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/development-closeout.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/evidence-retention.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/explicit-memory-update.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/extraction-latency.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/failed-run-recovery.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/gate-evidence-boundary.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/general-processing.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/hermes-mcp-runtime.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/hermes-provider-compatibility.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/hermes-source-comparison-context.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/host-acceptance-runbook.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/immutable-release-assets.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/incremental-commit-contract.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/incremental-execution.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/incremental-native-comparison.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/incremental-partial-recovery.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/incremental-planner-preview.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/incremental-runtime-contract.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/incremental-scope-registration.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/incremental-selected-retention.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/installed-artifact-verification.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/memory-lifecycle-tools.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/memory-state-contract.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/migration-preflight.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/processing-quality-acceptance.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/public-query-integrity.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/query-performance.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/remember-incremental-route.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/runtime-state-retention.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/semantic-extraction-protocol.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/source-work-contract.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/docs/v0.2.26-processing-status.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/examples/README.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/examples/basic_usage.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/examples/incremental_acceptance.json +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/examples/mcp_stdio.ndjson +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/examples/query_benchmark.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/install.ps1 +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/install.sh +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/setup.cfg +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/__main__.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/acceptance.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/adapters/__init__.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/adapters/antigravity.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/adapters/base.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/adapters/codex.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/admission.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/batch_review.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/budget.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/capture.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/cli.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/compaction.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/config.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/create_coordinator.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/credentials.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/evidence_budget.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/evidence_policy.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/evidence_structure.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/evidence_syntax.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/explicit_text_source.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/extraction_budget.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/extraction_capability.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/extraction_work_state.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/frontmatter.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/hermes_provider/README.md +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/hermes_provider/__init__.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/hermes_provider/_shared.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/hermes_provider/evidence_budget.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/hermes_provider/provider_compatibility.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/hermes_runtime.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/host_events.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/host_runtime.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/host_turn_identity.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/inbox.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/incremental_dates.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/incremental_execution.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/incremental_explicit.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/incremental_failed_recovery.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/incremental_journal.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/incremental_merge.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/incremental_native.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/incremental_partial.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/incremental_preview.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/incremental_prompts.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/incremental_protocol.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/incremental_recovery.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/incremental_run_state.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/incremental_runtime.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/incremental_scopes.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/incremental_selection.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/incremental_semantics.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/index.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/inspection.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/installer.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/legacy_update_journal.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/llm/__init__.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/llm/base.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/llm/claude_compatible.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/llm/gemini.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/llm/openai_compatible.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/llm/router.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/llm/thinking.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/locking.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/memory_commit.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/memory_planner.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/memory_retraction.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/memory_update.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/memory_writer.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/migration.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/model_capabilities.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/model_discovery.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/model_execution.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/models.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/native_index.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/native_registration.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/parallel_model.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/planning_candidates.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/planning_context.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/process_common.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/process_jobs.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/process_journal.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/process_owner.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/processing.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/prompts.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/provenance.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/provider_compatibility.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/query_clock.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/query_progress.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/query_scan.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/receipt_codec.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/recording_policy.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/redaction.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/remember_route.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/retention.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/retrieval.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/runtime_retention.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/scope_maintenance.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/scope_state.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/semantic_maintenance.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/semantic_protocol.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/single_pass_memory_planner.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/single_pass_plan.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/source_policy.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/state_layout.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/subprocess_flags.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/summary_batch.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/target_reconciliation.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/turn_audit.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/turn_plan.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/update_coordinator.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/update_review.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/validation.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf/vault.py +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf.egg-info/SOURCES.txt +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf.egg-info/dependency_links.txt +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf.egg-info/entry_points.txt +0 -0
- {memleaf-0.2.87 → memleaf-0.2.88}/src/memleaf.egg-info/top_level.txt +0 -0
|
@@ -4,6 +4,31 @@ 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
|
+
|
|
7
32
|
## 0.2.87 — 2026-10-04
|
|
8
33
|
|
|
9
34
|
### 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.88
|
|
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.88。**
|
|
27
27
|
> 自动提炼和复核共用同一条“未来记忆价值”标准:模型综合未来复用、信息增量、再次读取时的直接可用性和忘记成本,只保留对未来理解、判断或行动有实质影响的最小核心;没有明确价值的信息不提炼,不按具体业务场景硬编码排除。
|
|
28
28
|
> 提炼先比较旧事项中的编号、目标、进展、责任和期限;补全未知信息也应维护原事项,另建独立待办不能替代项目事实更新。确认重复的记忆在现有处理请求中合并,保留历史与来源;项目和独立任务保持各自生命周期,Core 不按标题强行合并。
|
|
29
29
|
> 新请求兼容等价的字段表示;未知执行人保持 `null`,新增或变更负责人和期限需要真实来源;等待条件在正文维护,不再提炼 `waiting_on`。行动及相关字段经过有界语义复核,提炼与复核共用每份工作的最多五次模型请求额度,成功即停止;复核失败不写入。来源时间用于解析相对日期,不补造事实日期。
|
|
@@ -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.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
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.
|
|
@@ -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.88。**
|
|
8
8
|
> 自动提炼和复核共用同一条“未来记忆价值”标准:模型综合未来复用、信息增量、再次读取时的直接可用性和忘记成本,只保留对未来理解、判断或行动有实质影响的最小核心;没有明确价值的信息不提炼,不按具体业务场景硬编码排除。
|
|
9
9
|
> 提炼先比较旧事项中的编号、目标、进展、责任和期限;补全未知信息也应维护原事项,另建独立待办不能替代项目事实更新。确认重复的记忆在现有处理请求中合并,保留历史与来源;项目和独立任务保持各自生命周期,Core 不按标题强行合并。
|
|
10
10
|
> 新请求兼容等价的字段表示;未知执行人保持 `null`,新增或变更负责人和期限需要真实来源;等待条件在正文维护,不再提炼 `waiting_on`。行动及相关字段经过有界语义复核,提炼与复核共用每份工作的最多五次模型请求额度,成功即停止;复核失败不写入。来源时间用于解析相对日期,不补造事实日期。
|
|
@@ -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
|
|
@@ -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,
|
|
@@ -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; "
|
|
@@ -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
|
|
@@ -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():
|
|
@@ -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"]:
|
|
@@ -37,6 +37,7 @@ from .retrieval_gate import (
|
|
|
37
37
|
observe_search,
|
|
38
38
|
observe_todo_list,
|
|
39
39
|
todo_filter_key,
|
|
40
|
+
todo_continuation,
|
|
40
41
|
validate_current_turn,
|
|
41
42
|
validate_turn,
|
|
42
43
|
)
|
|
@@ -78,7 +79,9 @@ INSTRUCTIONS = (
|
|
|
78
79
|
"at the execution boundary (supplied by a bound host or explicitly by a bare client), "
|
|
79
80
|
"and a current FOUND search or list_todos result; NO_MATCH, ERROR, and DEGRADED turns cannot read. "
|
|
80
81
|
"For global current-action questions use list_todos rather than relevance search, omit scope to cover "
|
|
81
|
-
"all scopes
|
|
82
|
+
"all scopes. Use view=full on server-managed turns, follow returned continuation arguments "
|
|
83
|
+
"until has_more=false, and read only truncated bodies using body_next_offset/version. "
|
|
84
|
+
"Directory clients pass next_cursor as cursor, then read each relevant action body. "
|
|
82
85
|
"For 'my work' use list_todos responsibility=mine; use delegated or all for others' work. "
|
|
83
86
|
"Unassigned ownership is uncertain. A dependency alone does not create a user follow-up task; "
|
|
84
87
|
"an active item is not automatically the user's personal work. "
|
|
@@ -264,8 +267,10 @@ _TOOLS: tuple[dict[str, Any], ...] = (
|
|
|
264
267
|
"description": (
|
|
265
268
|
"Enumerate explicitly tracked actions (including non-todo memories with status) by status/date across all scopes. "
|
|
266
269
|
"This is not relevance search. A bare MCP client may omit retrieval_id only on the first "
|
|
267
|
-
"page; memleaf returns one that must be reused for pagination and read.
|
|
268
|
-
"
|
|
270
|
+
"page; memleaf returns one that must be reused for pagination and read. Server-managed clients continue with "
|
|
271
|
+
"continue=true with only retrieval_id until has_more=false; the server carries the exact cursor and filters. "
|
|
272
|
+
"Use view=full to receive bounded action bodies in the list; read only rows with body_has_more=true "
|
|
273
|
+
"using offset=body_next_offset and expected_version=version. Directory clients may copy next_cursor into cursor, then read the matching action bodies with the same "
|
|
269
274
|
"retrieval_id. Displayed list numbers are turn-local; resolve a later numbered update "
|
|
270
275
|
"against the previously shown title and memory_id, and clarify if that mapping is ambiguous. "
|
|
271
276
|
"responsibility=mine selects only explicit assignee=user; delegated selects other named owners, "
|
|
@@ -286,6 +291,8 @@ _TOOLS: tuple[dict[str, Any], ...] = (
|
|
|
286
291
|
"include_overdue": {"type": "boolean"},
|
|
287
292
|
"include_unscheduled": {"type": "boolean"},
|
|
288
293
|
"cursor": {"type": "string"},
|
|
294
|
+
"continue": {"type": "boolean", "description": "Fetch the next page of this turn's last list_todos query; pass only retrieval_id with continue=true."},
|
|
295
|
+
"view": {"type": "string", "enum": ["directory", "full"], "description": "Full bundles bounded bodies for server-managed Hermes/bare MCP turns. Host-observed turns retain directory pagination and read accounting."},
|
|
289
296
|
"limit": {"type": "integer", "minimum": 1},
|
|
290
297
|
"retrieval_id": {"type": "string"},
|
|
291
298
|
},
|
|
@@ -791,6 +798,9 @@ def _tool_result(value: Any, *, is_error: bool = False) -> dict[str, Any]:
|
|
|
791
798
|
if not isinstance(structured, dict):
|
|
792
799
|
structured = {"result": structured}
|
|
793
800
|
status = structured.get("execution_status")
|
|
801
|
+
if status == "queued":
|
|
802
|
+
structured["outcome"] = {"complete": False,
|
|
803
|
+
"guidance": "The request is queued; acknowledge user_message. No saved or completed change is confirmed by this result."}
|
|
794
804
|
if status in {"completed", "completed_with_unresolved", "partial", "blocked", "failed", "retryable", "recovery_required", "cancelled"}:
|
|
795
805
|
commit = structured.get("commit") or {}
|
|
796
806
|
unresolved = status != "completed" or commit.get("coverage_status") == "partial"
|
|
@@ -1017,6 +1027,7 @@ def _observe_mcp_todos(
|
|
|
1017
1027
|
if state.get("source") in {"hermes", "mcp"}
|
|
1018
1028
|
else "hermes"
|
|
1019
1029
|
),
|
|
1030
|
+
query_arguments={k: v for k, v in arguments.items() if k != "cursor"},
|
|
1020
1031
|
)
|
|
1021
1032
|
|
|
1022
1033
|
|
|
@@ -1104,12 +1115,22 @@ def _invoke_tool(
|
|
|
1104
1115
|
value = dict(value)
|
|
1105
1116
|
value["retrieval_id"] = retrieval_id
|
|
1106
1117
|
elif name == "list_todos":
|
|
1118
|
+
continuation = args.pop("continue", False)
|
|
1107
1119
|
retrieval_id = args.pop("retrieval_id", None)
|
|
1108
1120
|
if retrieval_id is None:
|
|
1109
|
-
if args.get("cursor") is not None:
|
|
1121
|
+
if continuation or args.get("cursor") is not None:
|
|
1110
1122
|
raise RetrievalGateError("retrieval_id_required")
|
|
1111
1123
|
retrieval_id = _open_mcp_retrieval_turn(service, request_id)
|
|
1112
1124
|
managed_state = _managed_search_state(service, retrieval_id)
|
|
1125
|
+
if continuation:
|
|
1126
|
+
if args or managed_state is None:
|
|
1127
|
+
raise RetrievalGateError("retrieval_todo_pagination_mismatch")
|
|
1128
|
+
args = todo_continuation(service.vault, retrieval_id, current_source=managed_state["source"])
|
|
1129
|
+
# Host-owned observers retain their existing search/read contract.
|
|
1130
|
+
# Full bodies are bundled only where the server owns accounting.
|
|
1131
|
+
directory_fallback = managed_state is None and args.get("view") == "full"
|
|
1132
|
+
if directory_fallback:
|
|
1133
|
+
args["view"] = "directory"
|
|
1113
1134
|
observed_args = dict(args)
|
|
1114
1135
|
try:
|
|
1115
1136
|
value = service.list_todos(**args)
|
|
@@ -1125,6 +1146,15 @@ def _invoke_tool(
|
|
|
1125
1146
|
return _tool_error(error)
|
|
1126
1147
|
value = dict(value)
|
|
1127
1148
|
value["retrieval_id"] = retrieval_id
|
|
1149
|
+
if directory_fallback:
|
|
1150
|
+
value["view"] = "directory"
|
|
1151
|
+
value["answer_guidance"] = "This host observes directory results. Pass next_cursor as cursor until has_more=false, then read matching bodies with the same retrieval_id."
|
|
1152
|
+
if args.get("view") == "full":
|
|
1153
|
+
for row in value["results"]:
|
|
1154
|
+
guarded_read(service.vault, retrieval_id, row["memory_id"],
|
|
1155
|
+
lambda allowed, row=row: {"body": row["body"]}, current_source=managed_state["source"])
|
|
1156
|
+
if value.get("has_more") and managed_state is not None:
|
|
1157
|
+
value["continuation"] = {"continue": True, "retrieval_id": retrieval_id}
|
|
1128
1158
|
elif name == "read":
|
|
1129
1159
|
# Keep the protocol boundary hard-capped even if a caller sends a
|
|
1130
1160
|
# larger value. Core read_page performs the same authoritative
|
|
@@ -68,6 +68,22 @@ def _snapshot(service, source, session_id):
|
|
|
68
68
|
runs = {k: load_run(processed, k) for k in processed.get(RUN_KEY, {})}
|
|
69
69
|
works = {k: load_work(processed, k) for k in processed.get(COMMIT_KEY, {})}
|
|
70
70
|
grouped = journal._turns_by_session()
|
|
71
|
+
# A process can stop after its committed receipt but before the old host
|
|
72
|
+
# completion returned. Clear only a verified full-source intent, without
|
|
73
|
+
# selecting the completed turn for another model request.
|
|
74
|
+
from .host_retention import settle_completed_intent_unlocked
|
|
75
|
+
retention_works = {}
|
|
76
|
+
for work in works.values():
|
|
77
|
+
if work.get("host_retention_origin") is not None:
|
|
78
|
+
retention_works.setdefault((work["source"], work["session_id"], work["turn_key"]), []).append(work)
|
|
79
|
+
settled = False
|
|
80
|
+
for turns in grouped.values():
|
|
81
|
+
for turn in turns:
|
|
82
|
+
if turn.complete and turn.processable and _matches(source, session_id, vars(turn)):
|
|
83
|
+
for work in retention_works.get((turn.source, turn.session_id, turn.turn_key), ()):
|
|
84
|
+
settled = settle_completed_intent_unlocked(processed, work, turn) or settled
|
|
85
|
+
if settled:
|
|
86
|
+
journal._write_processed_unlocked(processed)
|
|
71
87
|
actions, skipped = [], []
|
|
72
88
|
available_identities = set()
|
|
73
89
|
for _, available in sorted(grouped.items()):
|
|
@@ -495,6 +495,7 @@ def observe_todo_list(
|
|
|
495
495
|
has_more: bool,
|
|
496
496
|
next_cursor: str | None,
|
|
497
497
|
current_source: str | None = None,
|
|
498
|
+
query_arguments: Mapping[str, Any] | None = None,
|
|
498
499
|
) -> None:
|
|
499
500
|
"""Record one real list_todos page and enforce a single current-turn cursor chain."""
|
|
500
501
|
|
|
@@ -539,6 +540,9 @@ def observe_todo_list(
|
|
|
539
540
|
entry["todo_list_pages"] = int(entry.get("todo_list_pages", 0) or 0) + 1
|
|
540
541
|
entry["todo_list_pending"] = bool(has_more)
|
|
541
542
|
entry["todo_list_expected_cursor_hash"] = next_hash if has_more else ""
|
|
543
|
+
if query_arguments is not None:
|
|
544
|
+
entry["todo_list_next_cursor"] = next_cursor if has_more else None
|
|
545
|
+
entry["todo_list_query_arguments"] = dict(query_arguments) if has_more else None
|
|
542
546
|
entry["status"] = {"found": "FOUND", "no_match": "NO_MATCH", "error": "ERROR"}[status]
|
|
543
547
|
seen = [item for item in seen if isinstance(item, str)][-255:]
|
|
544
548
|
seen.append(call_hash)
|
|
@@ -549,6 +553,26 @@ def observe_todo_list(
|
|
|
549
553
|
_write_ledger(path, ledger)
|
|
550
554
|
|
|
551
555
|
|
|
556
|
+
def todo_continuation(vault, retrieval_id, *, current_source):
|
|
557
|
+
"""Return only this live turn's last server-issued page, never a model's cursor rewrite."""
|
|
558
|
+
retrieval_id = _retrieval_id(retrieval_id)
|
|
559
|
+
root = _coerce_vault(vault)
|
|
560
|
+
with _with_lock(root):
|
|
561
|
+
ledger = _read_ledger(_ledger_path(root))
|
|
562
|
+
entry = _entry_for(ledger, retrieval_id)
|
|
563
|
+
if not _is_current_entry(ledger["entries"], retrieval_id, entry, current_source):
|
|
564
|
+
raise RetrievalGateError("retrieval_turn_mismatch")
|
|
565
|
+
cursor = entry.get("todo_list_next_cursor")
|
|
566
|
+
arguments = entry.get("todo_list_query_arguments")
|
|
567
|
+
if (entry.get("todo_list_pending") is not True or not isinstance(cursor, str)
|
|
568
|
+
or not isinstance(arguments, dict)
|
|
569
|
+
or _optional_hash(cursor) != entry.get("todo_list_expected_cursor_hash")
|
|
570
|
+
or hashlib.sha256(todo_filter_key(arguments).encode()).hexdigest()
|
|
571
|
+
!= entry.get("todo_list_filter_hash")):
|
|
572
|
+
raise RetrievalGateError("retrieval_todo_pagination_mismatch")
|
|
573
|
+
return {**arguments, "cursor": cursor}
|
|
574
|
+
|
|
575
|
+
|
|
552
576
|
def request_gate_retry(vault: Vault | Path | str, retrieval_id: str) -> int:
|
|
553
577
|
"""Record one Stop continuation request and return its 1-based count."""
|
|
554
578
|
|
|
@@ -719,6 +743,7 @@ __all__ = [
|
|
|
719
743
|
"observe_search",
|
|
720
744
|
"observe_todo_list",
|
|
721
745
|
"todo_filter_key",
|
|
746
|
+
"todo_continuation",
|
|
722
747
|
"request_gate_retry",
|
|
723
748
|
"validate_current_turn",
|
|
724
749
|
"validate_turn",
|
|
@@ -1368,7 +1368,18 @@ class Memleaf:
|
|
|
1368
1368
|
}
|
|
1369
1369
|
if payload_chars(proposed) > MAX_SEARCH_CANDIDATE_CHARS:
|
|
1370
1370
|
if not selected:
|
|
1371
|
-
|
|
1371
|
+
# Full views keep the same response cap. A long body is
|
|
1372
|
+
# explicitly paged instead of hiding or skipping the row.
|
|
1373
|
+
row = dict(candidates[index])
|
|
1374
|
+
while isinstance(row.get("body"), str) and len(row["body"]) > 1:
|
|
1375
|
+
row["body"] = row["body"][:len(row["body"]) // 2]
|
|
1376
|
+
row.update(body_has_more=True, body_next_offset=len(row["body"]))
|
|
1377
|
+
if payload_chars({**proposed, "results": [row]}) <= MAX_SEARCH_CANDIDATE_CHARS:
|
|
1378
|
+
candidates[index] = row
|
|
1379
|
+
break
|
|
1380
|
+
else:
|
|
1381
|
+
raise RetrievalError("search_result_too_large", "todo result exceeds the response budget")
|
|
1382
|
+
continue
|
|
1372
1383
|
break
|
|
1373
1384
|
selected.append(candidates[index])
|
|
1374
1385
|
index = next_index
|
|
@@ -1395,6 +1406,7 @@ class Memleaf:
|
|
|
1395
1406
|
limit: int | None = None,
|
|
1396
1407
|
as_of: str | None = None,
|
|
1397
1408
|
timezone: str | None = None,
|
|
1409
|
+
view: str = "directory",
|
|
1398
1410
|
) -> dict[str, Any]:
|
|
1399
1411
|
"""Enumerate explicitly tracked actions, including legacy todo memories."""
|
|
1400
1412
|
|
|
@@ -1402,6 +1414,8 @@ class Memleaf:
|
|
|
1402
1414
|
raise ValueError("invalid todo status")
|
|
1403
1415
|
if responsibility not in {"all", "mine", "delegated", "unassigned"}:
|
|
1404
1416
|
raise ValueError("invalid todo responsibility")
|
|
1417
|
+
if view not in {"directory", "full"}:
|
|
1418
|
+
raise ValueError("invalid todo view")
|
|
1405
1419
|
if type(include_overdue) is not bool or type(include_unscheduled) is not bool:
|
|
1406
1420
|
raise ValueError("todo inclusion flags must be booleans")
|
|
1407
1421
|
lower = self._todo_date(due_from, "due_from")
|
|
@@ -1484,6 +1498,10 @@ class Memleaf:
|
|
|
1484
1498
|
"assignee": record.memory.extra.get("assignee"),
|
|
1485
1499
|
"responsibility": _todo_responsibility(record.memory),
|
|
1486
1500
|
"history": record.area == "history",
|
|
1501
|
+
**({"body": record.memory.body[:512],
|
|
1502
|
+
"body_has_more": len(record.memory.body) > 512,
|
|
1503
|
+
"body_next_offset": 512 if len(record.memory.body) > 512 else None,
|
|
1504
|
+
"version": _memory_version(record.memory)} if view == "full" else {}),
|
|
1487
1505
|
**(
|
|
1488
1506
|
{"active_memory_id": record.memory.extra.get("active_memory_id")}
|
|
1489
1507
|
if record.area == "history" and isinstance(record.memory.extra.get("active_memory_id"), str)
|
|
@@ -1504,6 +1522,7 @@ class Memleaf:
|
|
|
1504
1522
|
"include_unscheduled": include_unscheduled,
|
|
1505
1523
|
"clock": clock,
|
|
1506
1524
|
"generation": snapshot.generation,
|
|
1525
|
+
**({"view": view} if view == "full" else {}),
|
|
1507
1526
|
},
|
|
1508
1527
|
"records": [
|
|
1509
1528
|
{
|
|
@@ -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,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.88。**
|
|
27
27
|
> 自动提炼和复核共用同一条“未来记忆价值”标准:模型综合未来复用、信息增量、再次读取时的直接可用性和忘记成本,只保留对未来理解、判断或行动有实质影响的最小核心;没有明确价值的信息不提炼,不按具体业务场景硬编码排除。
|
|
28
28
|
> 提炼先比较旧事项中的编号、目标、进展、责任和期限;补全未知信息也应维护原事项,另建独立待办不能替代项目事实更新。确认重复的记忆在现有处理请求中合并,保留历史与来源;项目和独立任务保持各自生命周期,Core 不按标题强行合并。
|
|
29
29
|
> 新请求兼容等价的字段表示;未知执行人保持 `null`,新增或变更负责人和期限需要真实来源;等待条件在正文维护,不再提炼 `waiting_on`。行动及相关字段经过有界语义复核,提炼与复核共用每份工作的最多五次模型请求额度,成功即停止;复核失败不写入。来源时间用于解析相对日期,不补造事实日期。
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|