memleaf 0.2.89__tar.gz → 0.2.91__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.89 → memleaf-0.2.91}/CHANGELOG.md +44 -0
- {memleaf-0.2.89/src/memleaf.egg-info → memleaf-0.2.91}/PKG-INFO +2 -2
- {memleaf-0.2.89 → memleaf-0.2.91}/README.en.md +1 -1
- {memleaf-0.2.89 → memleaf-0.2.91}/README.md +1 -1
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/memory-state-contract.md +20 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/pyproject.toml +1 -1
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/__init__.py +1 -1
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/hermes_provider/plugin.yaml +1 -1
- memleaf-0.2.91/src/memleaf/historical_context.py +83 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/incremental_preview.py +32 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/incremental_prompts.py +1 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/incremental_protocol.py +10 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/incremental_recovery.py +6 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/mcp_server.py +13 -3
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/retrieval_gate.py +3 -1
- memleaf-0.2.91/src/memleaf/retrieval_lifecycle.py +60 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/service.py +13 -4
- {memleaf-0.2.89 → memleaf-0.2.91/src/memleaf.egg-info}/PKG-INFO +2 -2
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf.egg-info/SOURCES.txt +2 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/LICENSE +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/MANIFEST.in +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/acceptance-evidence-guide.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/automatic-processing-route.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/body-compaction.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/candidate-reconciliation.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/capture-budget-design.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/config-migrations.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/core-refactor.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/development-closeout.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/evidence-retention.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/explicit-memory-update.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/extraction-latency.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/failed-run-recovery.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/gate-evidence-boundary.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/general-processing.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/hermes-mcp-runtime.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/hermes-provider-compatibility.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/hermes-source-comparison-context.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/host-acceptance-runbook.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/immutable-release-assets.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/incremental-commit-contract.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/incremental-execution.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/incremental-native-comparison.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/incremental-partial-recovery.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/incremental-planner-preview.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/incremental-runtime-contract.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/incremental-scope-registration.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/incremental-selected-retention.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/installed-artifact-verification.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/memory-lifecycle-tools.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/migration-preflight.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/processing-quality-acceptance.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/public-query-integrity.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/query-performance.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/remember-incremental-route.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/runtime-state-retention.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/semantic-extraction-protocol.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/source-work-contract.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/docs/v0.2.26-processing-status.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/examples/README.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/examples/basic_usage.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/examples/incremental_acceptance.json +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/examples/mcp_stdio.ndjson +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/examples/query_benchmark.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/install.ps1 +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/install.sh +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/setup.cfg +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/__main__.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/acceptance.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/adapters/__init__.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/adapters/antigravity.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/adapters/base.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/adapters/codex.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/adapters/hermes.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/admission.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/batch_review.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/budget.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/capture.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/cli.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/compaction.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/config.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/create_coordinator.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/credentials.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/evidence_budget.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/evidence_policy.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/evidence_structure.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/evidence_syntax.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/explicit_text_source.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/extraction_budget.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/extraction_capability.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/extraction_work_state.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/frontmatter.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/hermes_provider/README.md +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/hermes_provider/__init__.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/hermes_provider/_mcp_client.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/hermes_provider/_provider.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/hermes_provider/_shared.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/hermes_provider/evidence_budget.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/hermes_provider/provider_compatibility.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/hermes_runtime.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/host_events.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/host_retention.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/host_runtime.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/host_turn_identity.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/inbox.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/incremental_commit.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/incremental_dates.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/incremental_execution.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/incremental_explicit.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/incremental_failed_recovery.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/incremental_journal.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/incremental_merge.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/incremental_native.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/incremental_partial.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/incremental_run_state.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/incremental_runtime.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/incremental_scopes.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/incremental_selection.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/incremental_semantics.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/index.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/inspection.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/installer.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/legacy_update_journal.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/llm/__init__.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/llm/base.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/llm/claude_compatible.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/llm/gemini.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/llm/openai_compatible.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/llm/router.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/llm/thinking.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/locking.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/memory_commit.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/memory_planner.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/memory_retraction.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/memory_update.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/memory_writer.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/migration.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/model_capabilities.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/model_discovery.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/model_execution.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/models.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/native_index.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/native_registration.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/parallel_model.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/planning_candidates.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/planning_context.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/process_common.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/process_jobs.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/process_journal.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/process_owner.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/processing.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/processing_route.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/prompts.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/provenance.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/provider_compatibility.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/query_clock.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/query_progress.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/query_scan.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/receipt_codec.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/recording_policy.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/redaction.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/remember_route.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/retention.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/retrieval.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/runtime_retention.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/scope_maintenance.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/scope_state.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/semantic_maintenance.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/semantic_protocol.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/single_pass_memory_planner.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/single_pass_plan.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/source_policy.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/state_layout.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/subprocess_flags.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/summary_batch.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/target_reconciliation.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/turn_audit.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/turn_plan.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/update_coordinator.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/update_review.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/validation.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf/vault.py +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf.egg-info/dependency_links.txt +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf.egg-info/entry_points.txt +0 -0
- {memleaf-0.2.89 → memleaf-0.2.91}/src/memleaf.egg-info/top_level.txt +0 -0
|
@@ -4,6 +4,50 @@ All notable changes to memleaf are documented here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.2.91 — 2026-10-05
|
|
8
|
+
|
|
9
|
+
### Preserve historical reads in incremental comparison
|
|
10
|
+
|
|
11
|
+
- Accept successfully read historical versions as read-only comparison context
|
|
12
|
+
instead of blocking the whole turn with required_target_unavailable. Preserve
|
|
13
|
+
every required read identity and include its current head only through a
|
|
14
|
+
validated exact link; missing or conflicting links remain unknown.
|
|
15
|
+
- Keep historical versions outside operation targets and source evidence. Their
|
|
16
|
+
scopes do not grant write authority, and current heads retain existing scope
|
|
17
|
+
boundaries. Ordinary turns continue to scan current knowledge only.
|
|
18
|
+
- Protect historical content and lifecycle links in planning snapshots and
|
|
19
|
+
preserve them through partial recovery with stable current-target references.
|
|
20
|
+
Existing snapshots without historical context retain their original digest.
|
|
21
|
+
- No new model stage, model output field or tool is added. Only required archive
|
|
22
|
+
versions enter model input; a short prompt clarification distinguishes them
|
|
23
|
+
from current records and new evidence.
|
|
24
|
+
- Validation: 189 focused checks passed, including 19 new history-context
|
|
25
|
+
regressions, plus isolated replay of the original 19-read real turn. Six
|
|
26
|
+
existing legacy-suite failures reproduce on 0.2.90 and are recorded locally.
|
|
27
|
+
Replay used supplied responses with zero external model calls and left the
|
|
28
|
+
formal Vault unchanged; new live-model acceptance remains separate.
|
|
29
|
+
|
|
30
|
+
## 0.2.90 — 2026-10-05
|
|
31
|
+
|
|
32
|
+
### Expose withdrawal state in explicit historical retrieval
|
|
33
|
+
|
|
34
|
+
- Include matching retracted current heads when historical retrieval is
|
|
35
|
+
explicitly requested. Ordinary retrieval continues to exclude them.
|
|
36
|
+
- Project bounded lifecycle facts through search and paged MCP reads: the
|
|
37
|
+
historical snapshot flag, exact current identity link, invalidation reason,
|
|
38
|
+
and current validity from the same validated query scan. Keep missing,
|
|
39
|
+
unreadable, conflicting or ambiguous links unknown; support legacy links
|
|
40
|
+
without guessing from titles or content.
|
|
41
|
+
- Preserve archived validity and distinguish past withdrawal from later
|
|
42
|
+
restoration. Associate successful lifecycle-only reads with the captured
|
|
43
|
+
turn's comparison context without fabricating body characters.
|
|
44
|
+
- Reuse query scans and existing pagination, permissions and version checks.
|
|
45
|
+
No model stage, model call, tool or persistence format is added.
|
|
46
|
+
- Validation: 97 affected checks passed, plus an isolated real-conversation
|
|
47
|
+
replay through a source MCP process. One legacy test still expects the
|
|
48
|
+
previously removed waiting_on field and fails identically on the prior
|
|
49
|
+
release. Replay used zero model calls and left the formal Vault unchanged.
|
|
50
|
+
|
|
7
51
|
## 0.2.89 — 2026-10-04
|
|
8
52
|
|
|
9
53
|
### Complete comparison context and bound tool continuation
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.91
|
|
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.91。**
|
|
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.91.**
|
|
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.91。**
|
|
8
8
|
> 自动提炼和复核共用同一条“未来记忆价值”标准:模型综合未来复用、信息增量、再次读取时的直接可用性和忘记成本,只保留对未来理解、判断或行动有实质影响的最小核心;没有明确价值的信息不提炼,不按具体业务场景硬编码排除。
|
|
9
9
|
> 提炼先比较旧事项中的编号、目标、进展、责任和期限;补全未知信息也应维护原事项,另建独立待办不能替代项目事实更新。确认重复的记忆在现有处理请求中合并,保留历史与来源;项目和独立任务保持各自生命周期,Core 不按标题强行合并。
|
|
10
10
|
> 新请求兼容等价的字段表示;未知执行人保持 `null`,新增或变更负责人和期限需要真实来源;等待条件在正文维护,不再提炼 `waiting_on`。行动及相关字段经过有界语义复核,提炼与复核共用每份工作的最多五次模型请求额度,成功即停止;复核失败不写入。来源时间用于解析相对日期,不补造事实日期。
|
|
@@ -83,6 +83,26 @@ Ordinary `read`, `read_page`, `search`, `context`, and `list_todos` exclude retr
|
|
|
83
83
|
|
|
84
84
|
The derived active index excludes retracted heads. History indexing remains available only through APIs that explicitly include history. Closed todos remain in `knowledge/`; status filtering, rather than age-based deletion, controls their normal presentation.
|
|
85
85
|
|
|
86
|
+
For explicit historical questions, `search_candidates(include_history=True)` and
|
|
87
|
+
MCP `search` also expose matching retracted current heads. Candidate entries and
|
|
88
|
+
`read_page`/MCP `read` with `include_history=True` carry a bounded `lifecycle`:
|
|
89
|
+
|
|
90
|
+
- `historical` distinguishes an archived snapshot from a current head.
|
|
91
|
+
- `active_memory_id`, when present, is the stored exact current-identity link.
|
|
92
|
+
- `invalidated_reason`, when present, describes why that snapshot was archived.
|
|
93
|
+
- `current_validity` comes from the validated current head in the same query
|
|
94
|
+
observation. It is `null` when the linked head is missing, unreadable or
|
|
95
|
+
ambiguous; conflicting or missing legacy links are not inferred from titles.
|
|
96
|
+
|
|
97
|
+
An archived snapshot's own `validity=valid` describes its past assertion. A past
|
|
98
|
+
`invalidated_reason=retracted` does not establish current withdrawal after a
|
|
99
|
+
later restoration. Use `lifecycle.current_validity` for that distinction. The
|
|
100
|
+
projection contains no current body and grants no edit authority. Ordinary
|
|
101
|
+
retrieval remains unchanged and excludes retracted assertions. Historical body
|
|
102
|
+
pages retain their existing explicit history permission and version checks.
|
|
103
|
+
Successful lifecycle-only reads also associate the exact ID with the captured
|
|
104
|
+
turn's comparison context; an empty assertion does not fabricate body characters.
|
|
105
|
+
|
|
86
106
|
## Compatibility boundary
|
|
87
107
|
|
|
88
108
|
`closed_todo_retention_days` remains accepted so existing config files continue to load, but it no longer retires the stable current identity. Optional structured fields stored in frontmatter participate in the protected revision. The incremental model protocol supports UPDATE.patch.validity and an explicit restore with current content. The legacy planner still excludes retracted heads; its older protocol is not silently extended. Deterministic Python retraction and explicit update use the same current identity and protected revision semantics.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Local-first Markdown memory core for AI agents."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.2.
|
|
3
|
+
__version__ = "0.2.91"
|
|
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
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Read-only historical comparisons, separate from actionable memory targets."""
|
|
2
|
+
from collections.abc import Mapping
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .models import Memory
|
|
7
|
+
from .retrieval_lifecycle import project_lifecycle
|
|
8
|
+
from .turn_plan import revision_digest
|
|
9
|
+
from .vault import safe_component
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def freeze_history(value: Any, targets: Mapping[str, dict[str, Any]]) -> dict[str, Any]:
|
|
13
|
+
if value is None:
|
|
14
|
+
return {}
|
|
15
|
+
if not isinstance(value, Mapping):
|
|
16
|
+
raise ValueError("invalid_history_context")
|
|
17
|
+
current = {t["memory"]["memory_id"]: t for t in targets.values()}
|
|
18
|
+
identities = {identity.casefold() for identity in current}
|
|
19
|
+
result = {}
|
|
20
|
+
for identity, item in value.items():
|
|
21
|
+
if (not isinstance(identity, str) or not isinstance(item, Mapping)
|
|
22
|
+
or set(item) != {"memory", "lifecycle"}
|
|
23
|
+
or not isinstance(item["memory"], Memory)
|
|
24
|
+
or identity != item["memory"].memory_id):
|
|
25
|
+
raise ValueError("invalid_history_context")
|
|
26
|
+
safe_component(identity, "memory id")
|
|
27
|
+
if identity.casefold() in identities:
|
|
28
|
+
raise ValueError("duplicate_memory_id")
|
|
29
|
+
identities.add(identity.casefold())
|
|
30
|
+
life = project_lifecycle(item["lifecycle"])
|
|
31
|
+
if not life["historical"]:
|
|
32
|
+
raise ValueError("invalid_history_context")
|
|
33
|
+
head = current.get(life.get("active_memory_id"))
|
|
34
|
+
validity = head["memory"].get("validity", "valid") if head and "native" not in head else None
|
|
35
|
+
if life["current_validity"] != validity:
|
|
36
|
+
raise ValueError("invalid_history_context")
|
|
37
|
+
result[identity] = {"memory": item["memory"].to_dict(),
|
|
38
|
+
"revision": revision_digest(item["memory"]), "lifecycle": life}
|
|
39
|
+
return result
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def thaw_history(value: Any) -> dict[str, Any] | None:
|
|
43
|
+
if value is None:
|
|
44
|
+
return None
|
|
45
|
+
if not isinstance(value, Mapping):
|
|
46
|
+
raise ValueError("invalid_history_context")
|
|
47
|
+
result = {}
|
|
48
|
+
for identity, item in value.items():
|
|
49
|
+
if (not isinstance(item, Mapping) or set(item) != {"memory", "revision", "lifecycle"}
|
|
50
|
+
or not isinstance(item["memory"], Mapping)):
|
|
51
|
+
raise ValueError("invalid_history_context")
|
|
52
|
+
memory = Memory.from_mapping(item["memory"])
|
|
53
|
+
if revision_digest(memory) != item["revision"]:
|
|
54
|
+
raise ValueError("invalid_history_context")
|
|
55
|
+
result[identity] = {"memory": memory, "lifecycle": item["lifecycle"]}
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def history_signature(state: Mapping[str, Any]) -> dict[str, Any]:
|
|
60
|
+
# Current validity is derived from targets, whose changes (including this
|
|
61
|
+
# work's own writes) are already checked by partial recovery.
|
|
62
|
+
return {identity: {"revision": item["revision"],
|
|
63
|
+
"lifecycle": {k: v for k, v in item["lifecycle"].items()
|
|
64
|
+
if k != "current_validity"}}
|
|
65
|
+
for identity, item in state.get("history_context", {}).items()}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def project_history(state: Mapping[str, Any], scope_refs: Mapping[str, str]) -> list[dict[str, Any]]:
|
|
69
|
+
targets = {t["memory"]["memory_id"]: ref for ref, t in state["targets"].items()
|
|
70
|
+
if "native" not in t}
|
|
71
|
+
result = []
|
|
72
|
+
for item in state.get("history_context", {}).values():
|
|
73
|
+
memory = item["memory"]
|
|
74
|
+
row = {key: deepcopy(memory[key]) for key in (
|
|
75
|
+
"type", "title", "body", "status", "validity", "actionable", "assignee",
|
|
76
|
+
"due_date", "due_text", "due_status", "completed_at") if key in memory}
|
|
77
|
+
scopes = memory.get("scopes", ["global"])
|
|
78
|
+
row["scope"] = scope_refs.get(scopes[0], scopes[0]) if len(scopes) == 1 else scopes
|
|
79
|
+
life = dict(item["lifecycle"])
|
|
80
|
+
row["current_target"] = targets.get(life.pop("active_memory_id", None))
|
|
81
|
+
row["lifecycle"] = life
|
|
82
|
+
result.append(row)
|
|
83
|
+
return result
|
|
@@ -155,6 +155,37 @@ def _prepare_incremental_unlocked(service: Any, *, source: str, session_id: str,
|
|
|
155
155
|
read_ids.extend(identities)
|
|
156
156
|
priority = list(dict.fromkeys(explicit_ids + read_ids + priority))
|
|
157
157
|
priority += sorted(x for x in prior_ids if x.casefold() in records and x not in priority)
|
|
158
|
+
historical = {}
|
|
159
|
+
explicit_keys = {identity.casefold() for identity in explicit_ids}
|
|
160
|
+
if any(identity.casefold() not in records for identity in priority):
|
|
161
|
+
# Ordinary turns keep their knowledge-only scan. Read/caller-required
|
|
162
|
+
# identities that are absent there may be exact historical versions.
|
|
163
|
+
# Reuse the public scanner's duplicate, path and generation checks.
|
|
164
|
+
library = scan_memories(service.vault, include_history=True)
|
|
165
|
+
if library.ambiguous or native_keys.intersection(r.memory.memory_id.casefold() for r in library.records):
|
|
166
|
+
raise ValueError("duplicate_memory_id")
|
|
167
|
+
if library.issues:
|
|
168
|
+
raise ValueError("incomplete_library")
|
|
169
|
+
records = {r.memory.memory_id.casefold(): r.memory for r in library.area("knowledge")}
|
|
170
|
+
records.update({key.casefold(): memory for key, memory in native.memories.items()})
|
|
171
|
+
archives = {r.memory.memory_id.casefold(): r for r in library.area("history")}
|
|
172
|
+
from .retrieval_lifecycle import current_validities, lifecycle
|
|
173
|
+
current = current_validities(library)
|
|
174
|
+
current_priority = []
|
|
175
|
+
for identity in priority:
|
|
176
|
+
if identity.casefold() in records:
|
|
177
|
+
current_priority.append(identity)
|
|
178
|
+
continue
|
|
179
|
+
archive = archives.get(identity.casefold())
|
|
180
|
+
if archive is None or identity.casefold() in explicit_keys:
|
|
181
|
+
raise ValueError("required_target_unavailable")
|
|
182
|
+
life = lifecycle(archive, current)
|
|
183
|
+
historical[archive.memory.memory_id] = {"memory": archive.memory, "lifecycle": life}
|
|
184
|
+
# A stored unambiguous local link supplies comparison context, not
|
|
185
|
+
# new write authority; the existing scope boundary still applies.
|
|
186
|
+
if life["current_validity"] is not None:
|
|
187
|
+
current_priority.append(life["active_memory_id"])
|
|
188
|
+
priority = list(dict.fromkeys(current_priority))
|
|
158
189
|
chosen = []
|
|
159
190
|
for identity in priority:
|
|
160
191
|
if identity.casefold() not in records:
|
|
@@ -202,6 +233,7 @@ def _prepare_incremental_unlocked(service: Any, *, source: str, session_id: str,
|
|
|
202
233
|
return PlanningSnapshot.build(evidence=evidence, targets=targets, scopes=scope_refs,
|
|
203
234
|
write_scopes=boundary, writable=writable,
|
|
204
235
|
allow_new_scopes=allow_new_scopes, native_targets=native_targets,
|
|
236
|
+
history_context=historical,
|
|
205
237
|
native_guard=native.guard if native.guard["sources"] else None,
|
|
206
238
|
request_kind="explicit_remember" if selection else "automatic",
|
|
207
239
|
retention_request=retention_request, scope_guard=scope_guard,
|
|
@@ -39,4 +39,5 @@ patch 只含 title/body/scope/status/actionable/assignee/deadline/validity;typ
|
|
|
39
39
|
request_kind=explicit_remember:所选 new 有保留授权,仍按用户实际要求选择内容并整理去重,不得 NO_MEMORY;独立不被要求保留的内容不强行建项。
|
|
40
40
|
仅 whole_host_turn=true 的宿主完整回合显式请求:遵守用户指定的保存/排除范围,同时按自动记忆标准处理整轮其余有价值的独立新信息;不要因已保存一项而跳过其他。助手建议仍不能变成用户义务。
|
|
41
41
|
explicit_writes 是系统核验的已写目标关联,不是新证据或整轮已覆盖声明:先比较当前目标与 new,完整无变化 NO_CHANGE,同一事项新增 UPDATE,独立新事项仍 CREATE。不用旧回执覆盖较新目标。
|
|
42
|
+
history_context 是已读历史版本,只供比较,不是新事实或操作 target;current_target 指向当前候选,current_validity=null 表示未知。历史正文及当时状态不覆盖当前候选。
|
|
42
43
|
"""
|
|
@@ -287,6 +287,7 @@ class PlanningSnapshot:
|
|
|
287
287
|
allow_new_scopes: bool = False, context_complete: bool = True,
|
|
288
288
|
native_targets: Mapping[str, dict[str, Any]] | None = None,
|
|
289
289
|
native_guard: dict[str, Any] | None = None,
|
|
290
|
+
history_context: Mapping[str, Any] | None = None,
|
|
290
291
|
retention_request: str | None = None, scope_guard: dict[str, Any] | None = None,
|
|
291
292
|
scope_aliases: Mapping[str, list[str]] | None = None,
|
|
292
293
|
basis_statuses: Mapping[str, str] | None = None,
|
|
@@ -371,6 +372,10 @@ class PlanningSnapshot:
|
|
|
371
372
|
state = {"protocol_version": PROTOCOL_VERSION, "evidence": evidence, "targets": target_values,
|
|
372
373
|
"scopes": scopes, "write_scopes": write_scopes, "request_kind": request_kind,
|
|
373
374
|
"allow_new_scopes": allow_new_scopes, "context_complete": context_complete}
|
|
375
|
+
from .historical_context import freeze_history
|
|
376
|
+
historical = freeze_history(history_context, target_values)
|
|
377
|
+
if historical:
|
|
378
|
+
state["history_context"] = historical
|
|
374
379
|
if extraction_contract is not None:
|
|
375
380
|
state["extraction_contract"] = extraction_contract
|
|
376
381
|
from .incremental_explicit import validate_links
|
|
@@ -415,6 +420,9 @@ class PlanningSnapshot:
|
|
|
415
420
|
state["targets"] = {ref: {"revision": item["revision"], "writable": item["writable"],
|
|
416
421
|
**({"native": item["native"]} if "native" in item else {})}
|
|
417
422
|
for ref, item in state["targets"].items()}
|
|
423
|
+
if "history_context" in state:
|
|
424
|
+
state["history_context"] = {identity: {"revision": item["revision"], "lifecycle": item["lifecycle"]}
|
|
425
|
+
for identity, item in state["history_context"].items()}
|
|
418
426
|
return hashlib.sha256(_json(state).encode("utf-8")).hexdigest()
|
|
419
427
|
|
|
420
428
|
def state(self) -> dict[str, Any]:
|
|
@@ -454,6 +462,7 @@ class PlanningSnapshot:
|
|
|
454
462
|
projection["calendar_hints"] = hints
|
|
455
463
|
projected_evidence.append(projection)
|
|
456
464
|
from .incremental_explicit import project_links
|
|
465
|
+
from .historical_context import project_history
|
|
457
466
|
return {
|
|
458
467
|
"protocol_version": PROTOCOL_VERSION, "request_kind": state["request_kind"],
|
|
459
468
|
**({"extraction_contract": state["extraction_contract"]} if "extraction_contract" in state else {}),
|
|
@@ -463,6 +472,7 @@ class PlanningSnapshot:
|
|
|
463
472
|
if state.get("scope_aliases") else {}),
|
|
464
473
|
"allow_new_scopes": state["allow_new_scopes"], "context_complete": state["context_complete"],
|
|
465
474
|
"evidence": projected_evidence, "memories": memories,
|
|
475
|
+
**({"history_context": project_history(state, reverse_scope)} if state.get("history_context") else {}),
|
|
466
476
|
**({"explicit_writes": project_links(state["explicit_writes"], state["targets"])}
|
|
467
477
|
if state.get("explicit_writes") else {}),
|
|
468
478
|
}
|
|
@@ -12,6 +12,7 @@ from typing import Any
|
|
|
12
12
|
from .incremental_protocol import PlanningSnapshot, MAX_ITEMS
|
|
13
13
|
from .models import Memory
|
|
14
14
|
from .validation import parse_strict_json
|
|
15
|
+
from .historical_context import thaw_history, history_signature
|
|
15
16
|
|
|
16
17
|
RECOVERY_SYSTEM = "\n本次仅重规划未结算的 new;settled 只读,不重出、不改写。新增前文和已有记忆仅帮助理解原事项,不据此新建无关事项。沿用 JSON 协议。\n"
|
|
17
18
|
|
|
@@ -36,6 +37,7 @@ def restore_snapshot(state: Any) -> PlanningSnapshot:
|
|
|
36
37
|
vault_binding=state.get("vault_binding"),
|
|
37
38
|
explicit_writes=state.get("explicit_writes"),
|
|
38
39
|
extraction_contract=state.get("extraction_contract"),
|
|
40
|
+
history_context=thaw_history(state.get("history_context")),
|
|
39
41
|
)
|
|
40
42
|
except (KeyError, TypeError) as error:
|
|
41
43
|
raise ValueError("invalid_partial_snapshot") from error
|
|
@@ -175,6 +177,7 @@ def context_changed(original: PlanningSnapshot, current: PlanningSnapshot, work:
|
|
|
175
177
|
new_e = {e["event_key"]: {k: v for k, v in e.items() if k != "ref"} for e in new["evidence"]}
|
|
176
178
|
from .incremental_scopes import guard_matches, applied_additions
|
|
177
179
|
if (old_e != new_e or old.get("native_guard") != new.get("native_guard")
|
|
180
|
+
or history_signature(old) != history_signature(new)
|
|
178
181
|
or not guard_matches(new.get("scope_guard"), old.get("scope_guard"), applied_additions(work))):
|
|
179
182
|
return True
|
|
180
183
|
expected = {t["memory"]["memory_id"]: t["revision"] for t in old["targets"].values()}
|
|
@@ -247,6 +250,8 @@ def recovery_snapshot(original: PlanningSnapshot, current: PlanningSnapshot, wor
|
|
|
247
250
|
raise ValueError("repair_context_changed")
|
|
248
251
|
if old.get("native_guard") != new.get("native_guard"):
|
|
249
252
|
raise ValueError("repair_context_changed")
|
|
253
|
+
if history_signature(old) != history_signature(new):
|
|
254
|
+
raise ValueError("repair_context_changed")
|
|
250
255
|
# A structural repair may not silently adapt any referenced target's
|
|
251
256
|
# earlier business decision to a different complete target snapshot.
|
|
252
257
|
for row in work.get("_repair_rows", []):
|
|
@@ -270,6 +275,7 @@ def recovery_snapshot(original: PlanningSnapshot, current: PlanningSnapshot, wor
|
|
|
270
275
|
scope_guard=new.get("scope_guard"), scope_aliases=new.get("scope_aliases"),
|
|
271
276
|
basis_statuses=statuses, vault_binding=new.get("vault_binding"),
|
|
272
277
|
explicit_writes=explicit_links,
|
|
278
|
+
history_context=thaw_history(new.get("history_context")),
|
|
273
279
|
extraction_contract=(old if mode == "repair" else new).get("extraction_contract"))
|
|
274
280
|
|
|
275
281
|
|
|
@@ -244,6 +244,8 @@ _TOOLS: tuple[dict[str, Any], ...] = (
|
|
|
244
244
|
"MCP client may omit it only on the first page; memleaf returns a short-lived retrieval_id "
|
|
245
245
|
"for pagination/read. Errors are failures, never no_match. Managed MCP search is "
|
|
246
246
|
"directory-only; view=full is rejected. "
|
|
247
|
+
"For explicit history/withdrawal questions use include_history=true. Results include lifecycle: "
|
|
248
|
+
"historical snapshot, linked current ID/validity and invalidation reason; null current validity is unknown. "
|
|
247
249
|
"Python search(view='full') remains the compatibility interface."
|
|
248
250
|
),
|
|
249
251
|
"inputSchema": _object_schema(
|
|
@@ -303,9 +305,10 @@ _TOOLS: tuple[dict[str, Any], ...] = (
|
|
|
303
305
|
"name": "read",
|
|
304
306
|
"description": (
|
|
305
307
|
"Read one memory by exact memory_id in pages. Each page is capped at 2000 body "
|
|
306
|
-
"characters
|
|
308
|
+
"characters with bounded content, state, and paging/version fields; "
|
|
307
309
|
"retrieval_id is required, and the retrieval turn must have a current FOUND search; "
|
|
308
310
|
"NO_MATCH, ERROR, and DEGRADED turns are rejected. "
|
|
311
|
+
"Read historical or retracted records with include_history=true; lifecycle distinguishes past invalidation from current validity. "
|
|
309
312
|
"Prefer returned body_continuation: pass only continue=true and retrieval_id to read the next unfinished body, with its ID/version/offset held by the server. "
|
|
310
313
|
"On memory_version_changed, add restart=true to that continuation to restart the same bound memory at offset 0 with its new version. "
|
|
311
314
|
"continue with next_offset only when has_more is true, passing the returned version "
|
|
@@ -693,6 +696,9 @@ def _read_page_result(value: Any) -> dict[str, Any] | None:
|
|
|
693
696
|
result[name] = field
|
|
694
697
|
if "due_anchor" in value:
|
|
695
698
|
result["due_anchor"] = _jsonable(value.get("due_anchor"))
|
|
699
|
+
if "lifecycle" in value:
|
|
700
|
+
from .retrieval_lifecycle import project_lifecycle
|
|
701
|
+
result["lifecycle"] = project_lifecycle(value["lifecycle"])
|
|
696
702
|
result.update(_query_metadata(value))
|
|
697
703
|
if "read_accounting" in value:
|
|
698
704
|
if not isinstance(value["read_accounting"], str) or value["read_accounting"] not in {"counted", "not_counted", "unavailable"}:
|
|
@@ -734,7 +740,7 @@ def _search_result(value: Any) -> dict[str, Any]:
|
|
|
734
740
|
entries = value.get("results")
|
|
735
741
|
if not isinstance(entries, list):
|
|
736
742
|
raise ValueError("invalid search results")
|
|
737
|
-
results: list[dict[str,
|
|
743
|
+
results: list[dict[str, Any]] = []
|
|
738
744
|
for entry in entries:
|
|
739
745
|
if not isinstance(entry, Mapping):
|
|
740
746
|
raise ValueError("invalid search entry")
|
|
@@ -744,7 +750,11 @@ def _search_result(value: Any) -> dict[str, Any]:
|
|
|
744
750
|
raise ValueError("invalid search identifiers")
|
|
745
751
|
# Search is intentionally narrower than the legacy directory helper:
|
|
746
752
|
# do not leak per-memory scope/tags/aliases/keywords into candidates.
|
|
747
|
-
|
|
753
|
+
row = {"memory_id": memory_id, "title": title}
|
|
754
|
+
if "lifecycle" in entry:
|
|
755
|
+
from .retrieval_lifecycle import project_lifecycle
|
|
756
|
+
row["lifecycle"] = project_lifecycle(entry["lifecycle"])
|
|
757
|
+
results.append(row)
|
|
748
758
|
if bool(results) != (value["status"] == "found"):
|
|
749
759
|
raise ValueError("inconsistent search results")
|
|
750
760
|
paging = _paging_fields(value)
|
|
@@ -762,7 +762,9 @@ def guarded_read(
|
|
|
762
762
|
elif prior is not None:
|
|
763
763
|
pending.pop(prior)
|
|
764
764
|
entry["body_continuations"] = pending
|
|
765
|
-
|
|
765
|
+
# A verified empty tombstone still supplies lifecycle facts and must
|
|
766
|
+
# remain available to the captured turn's comparison context.
|
|
767
|
+
if body or isinstance(result.get("lifecycle"), Mapping):
|
|
766
768
|
if memory_id not in read_ids:
|
|
767
769
|
read_ids.append(memory_id)
|
|
768
770
|
entry["read_ids"] = read_ids
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Bounded lifecycle facts for explicitly requested historical retrieval."""
|
|
2
|
+
from collections.abc import Mapping
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .query_scan import QueryScan, ScanRecord
|
|
6
|
+
from .vault import safe_component
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def current_validities(snapshot: QueryScan) -> dict[str, str]:
|
|
10
|
+
# Only validated, unambiguous current records can establish current state.
|
|
11
|
+
unavailable = snapshot.ambiguous | {i.identity.casefold() for i in snapshot.issues if i.identity}
|
|
12
|
+
return {r.memory.memory_id: r.memory.validity
|
|
13
|
+
for r in snapshot.area("knowledge")
|
|
14
|
+
if r.memory.memory_id.casefold() not in unavailable}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def lifecycle(record: ScanRecord, current: Mapping[str, str]) -> dict[str, Any]:
|
|
18
|
+
historical = record.area == "history"
|
|
19
|
+
result = {"historical": historical,
|
|
20
|
+
"current_validity": None if historical else record.memory.validity}
|
|
21
|
+
if not historical:
|
|
22
|
+
return result
|
|
23
|
+
extra = record.memory.extra
|
|
24
|
+
links = [extra[k] for k in ("active_memory_id", "original_memory_id") if k in extra]
|
|
25
|
+
if links and all(isinstance(v, str) and len(v) <= 800 and v == links[0] for v in links):
|
|
26
|
+
try:
|
|
27
|
+
identity = safe_component(links[0], "active memory id")
|
|
28
|
+
except ValueError:
|
|
29
|
+
pass
|
|
30
|
+
else:
|
|
31
|
+
result.update(active_memory_id=identity, current_validity=current.get(identity))
|
|
32
|
+
reason = extra.get("invalidated_reason")
|
|
33
|
+
if isinstance(reason, str) and 0 < len(reason) <= 120:
|
|
34
|
+
result["invalidated_reason"] = reason
|
|
35
|
+
return result
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def project_lifecycle(value: Any) -> dict[str, Any]:
|
|
39
|
+
"""Do not pass arbitrary archive extras through the MCP boundary."""
|
|
40
|
+
if (not isinstance(value, Mapping)
|
|
41
|
+
or not {"historical", "current_validity"} <= set(value)
|
|
42
|
+
or set(value) - {"historical", "current_validity", "active_memory_id", "invalidated_reason"}
|
|
43
|
+
or type(value["historical"]) is not bool
|
|
44
|
+
or value["current_validity"] not in (None, "valid", "retracted")):
|
|
45
|
+
raise ValueError("invalid retrieval lifecycle")
|
|
46
|
+
result = dict(value)
|
|
47
|
+
if not value["historical"] and (value["current_validity"] is None
|
|
48
|
+
or set(value) != {"historical", "current_validity"}):
|
|
49
|
+
raise ValueError("invalid current lifecycle")
|
|
50
|
+
if value["historical"] and value["current_validity"] is not None and "active_memory_id" not in value:
|
|
51
|
+
raise ValueError("current validity requires an exact archive link")
|
|
52
|
+
if "active_memory_id" in value:
|
|
53
|
+
if not isinstance(value["active_memory_id"], str) or len(value["active_memory_id"]) > 800:
|
|
54
|
+
raise ValueError("invalid active memory id")
|
|
55
|
+
safe_component(value["active_memory_id"], "active memory id")
|
|
56
|
+
if "invalidated_reason" in value:
|
|
57
|
+
reason = value["invalidated_reason"]
|
|
58
|
+
if not isinstance(reason, str) or not 0 < len(reason) <= 120:
|
|
59
|
+
raise ValueError("invalid invalidation reason")
|
|
60
|
+
return result
|
|
@@ -58,6 +58,7 @@ from .scope_state import (
|
|
|
58
58
|
)
|
|
59
59
|
from .vault import Vault, safe_component
|
|
60
60
|
from .query_scan import QueryScan, ScanRecord, scan_memories, ensure_scan_current
|
|
61
|
+
from .retrieval_lifecycle import current_validities, lifecycle
|
|
61
62
|
from .query_progress import observe_progress
|
|
62
63
|
|
|
63
64
|
|
|
@@ -989,7 +990,9 @@ class Memleaf:
|
|
|
989
990
|
revision=revision_digest(memory),
|
|
990
991
|
structured_fields=memory.extra,
|
|
991
992
|
record=record,
|
|
992
|
-
), **
|
|
993
|
+
), **({"lifecycle": lifecycle(record, current_validities(snapshot))}
|
|
994
|
+
if include_history else {}),
|
|
995
|
+
**self._query_envelope(snapshot, scope=memory.scopes)}
|
|
993
996
|
|
|
994
997
|
from .native_index import NativeIndexer
|
|
995
998
|
|
|
@@ -1157,8 +1160,8 @@ class Memleaf:
|
|
|
1157
1160
|
|
|
1158
1161
|
@staticmethod
|
|
1159
1162
|
def _candidate_directory(record: _Record) -> dict[str, Any]:
|
|
1160
|
-
#
|
|
1161
|
-
#
|
|
1163
|
+
# Ordinary candidate entries remain memory_id + title. Explicit
|
|
1164
|
+
# historical queries add lifecycle separately. Scope selection happened
|
|
1162
1165
|
# before search via the bounded Scope Map and search input.
|
|
1163
1166
|
display = directory_entry(record.memory)
|
|
1164
1167
|
return {
|
|
@@ -1259,6 +1262,8 @@ class Memleaf:
|
|
|
1259
1262
|
) -> dict[str, Any]:
|
|
1260
1263
|
"""Search and return only a bounded directory of candidate memories."""
|
|
1261
1264
|
|
|
1265
|
+
if type(include_history) is not bool:
|
|
1266
|
+
raise ValueError("include_history must be a boolean")
|
|
1262
1267
|
page_limit = self._page_limit(
|
|
1263
1268
|
limit,
|
|
1264
1269
|
MAX_SEARCH_CANDIDATE_ITEMS,
|
|
@@ -1289,6 +1294,7 @@ class Memleaf:
|
|
|
1289
1294
|
limit=None,
|
|
1290
1295
|
stable=True,
|
|
1291
1296
|
strict_candidates=True,
|
|
1297
|
+
include_retracted=include_history,
|
|
1292
1298
|
query_snapshot=snapshot,
|
|
1293
1299
|
)
|
|
1294
1300
|
records = [
|
|
@@ -1296,7 +1302,10 @@ class Memleaf:
|
|
|
1296
1302
|
for record in records
|
|
1297
1303
|
if candidate_matches_query(record.memory, query_value)
|
|
1298
1304
|
]
|
|
1299
|
-
|
|
1305
|
+
current = current_validities(snapshot) if include_history else {}
|
|
1306
|
+
candidates = [{**self._candidate_directory(record),
|
|
1307
|
+
**({"lifecycle": lifecycle(record, current)} if include_history else {})}
|
|
1308
|
+
for record in records]
|
|
1300
1309
|
fingerprint = self._search_fingerprint(
|
|
1301
1310
|
records,
|
|
1302
1311
|
query=query_value,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.91
|
|
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.91。**
|
|
27
27
|
> 自动提炼和复核共用同一条“未来记忆价值”标准:模型综合未来复用、信息增量、再次读取时的直接可用性和忘记成本,只保留对未来理解、判断或行动有实质影响的最小核心;没有明确价值的信息不提炼,不按具体业务场景硬编码排除。
|
|
28
28
|
> 提炼先比较旧事项中的编号、目标、进展、责任和期限;补全未知信息也应维护原事项,另建独立待办不能替代项目事实更新。确认重复的记忆在现有处理请求中合并,保留历史与来源;项目和独立任务保持各自生命周期,Core 不按标题强行合并。
|
|
29
29
|
> 新请求兼容等价的字段表示;未知执行人保持 `null`,新增或变更负责人和期限需要真实来源;等待条件在正文维护,不再提炼 `waiting_on`。行动及相关字段经过有界语义复核,提炼与复核共用每份工作的最多五次模型请求额度,成功即停止;复核失败不写入。来源时间用于解析相对日期,不补造事实日期。
|
|
@@ -72,6 +72,7 @@ src/memleaf/extraction_capability.py
|
|
|
72
72
|
src/memleaf/extraction_work_state.py
|
|
73
73
|
src/memleaf/frontmatter.py
|
|
74
74
|
src/memleaf/hermes_runtime.py
|
|
75
|
+
src/memleaf/historical_context.py
|
|
75
76
|
src/memleaf/host_events.py
|
|
76
77
|
src/memleaf/host_retention.py
|
|
77
78
|
src/memleaf/host_runtime.py
|
|
@@ -135,6 +136,7 @@ src/memleaf/remember_route.py
|
|
|
135
136
|
src/memleaf/retention.py
|
|
136
137
|
src/memleaf/retrieval.py
|
|
137
138
|
src/memleaf/retrieval_gate.py
|
|
139
|
+
src/memleaf/retrieval_lifecycle.py
|
|
138
140
|
src/memleaf/runtime_retention.py
|
|
139
141
|
src/memleaf/scope_maintenance.py
|
|
140
142
|
src/memleaf/scope_state.py
|
|
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
|