memleaf 0.2.84__tar.gz → 0.2.86__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.84 → memleaf-0.2.86}/CHANGELOG.md +48 -0
- {memleaf-0.2.84/src/memleaf.egg-info → memleaf-0.2.86}/PKG-INFO +8 -4
- {memleaf-0.2.84 → memleaf-0.2.86}/README.en.md +5 -3
- {memleaf-0.2.84 → memleaf-0.2.86}/README.md +7 -3
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/incremental-planner-preview.md +9 -6
- {memleaf-0.2.84 → memleaf-0.2.86}/pyproject.toml +1 -1
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/__init__.py +1 -1
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/acceptance.py +1 -1
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/adapters/hermes.py +52 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/hermes_provider/README.md +5 -1
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/hermes_provider/_provider.py +30 -9
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/hermes_provider/plugin.yaml +1 -1
- memleaf-0.2.86/src/memleaf/host_retention.py +167 -0
- memleaf-0.2.86/src/memleaf/host_turn_identity.py +33 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/incremental_commit.py +11 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/incremental_execution.py +49 -4
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/incremental_explicit.py +20 -3
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/incremental_journal.py +3 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/incremental_partial.py +4 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/incremental_preview.py +5 -28
- memleaf-0.2.86/src/memleaf/incremental_prompts.py +38 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/incremental_protocol.py +49 -10
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/incremental_recovery.py +2 -1
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/incremental_run_state.py +13 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/incremental_runtime.py +2 -2
- memleaf-0.2.86/src/memleaf/incremental_semantics.py +206 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/installer.py +3 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/mcp_server.py +32 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/memory_writer.py +12 -2
- memleaf-0.2.86/src/memleaf/planning_candidates.py +52 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/processing_route.py +20 -2
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/query_progress.py +3 -1
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/retrieval_gate.py +16 -1
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/runtime_retention.py +66 -41
- {memleaf-0.2.84 → memleaf-0.2.86/src/memleaf.egg-info}/PKG-INFO +8 -4
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf.egg-info/SOURCES.txt +4 -0
- memleaf-0.2.84/src/memleaf/incremental_prompts.py +0 -26
- {memleaf-0.2.84 → memleaf-0.2.86}/LICENSE +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/MANIFEST.in +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/acceptance-evidence-guide.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/automatic-processing-route.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/body-compaction.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/candidate-reconciliation.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/capture-budget-design.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/config-migrations.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/core-refactor.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/development-closeout.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/evidence-retention.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/explicit-memory-update.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/extraction-latency.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/failed-run-recovery.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/gate-evidence-boundary.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/general-processing.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/hermes-mcp-runtime.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/hermes-provider-compatibility.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/hermes-source-comparison-context.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/host-acceptance-runbook.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/immutable-release-assets.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/incremental-commit-contract.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/incremental-execution.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/incremental-native-comparison.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/incremental-partial-recovery.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/incremental-runtime-contract.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/incremental-scope-registration.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/incremental-selected-retention.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/installed-artifact-verification.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/memory-lifecycle-tools.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/memory-state-contract.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/migration-preflight.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/processing-quality-acceptance.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/public-query-integrity.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/query-performance.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/remember-incremental-route.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/runtime-state-retention.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/semantic-extraction-protocol.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/source-work-contract.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/docs/v0.2.26-processing-status.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/examples/README.md +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/examples/basic_usage.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/examples/incremental_acceptance.json +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/examples/mcp_stdio.ndjson +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/examples/query_benchmark.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/install.ps1 +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/install.sh +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/setup.cfg +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/__main__.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/adapters/__init__.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/adapters/antigravity.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/adapters/base.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/adapters/codex.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/admission.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/batch_review.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/budget.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/capture.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/cli.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/compaction.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/config.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/create_coordinator.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/credentials.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/evidence_budget.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/evidence_policy.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/evidence_structure.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/evidence_syntax.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/explicit_text_source.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/extraction_budget.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/extraction_capability.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/extraction_work_state.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/frontmatter.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/hermes_provider/__init__.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/hermes_provider/_mcp_client.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/hermes_provider/_shared.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/hermes_provider/evidence_budget.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/hermes_provider/provider_compatibility.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/hermes_runtime.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/host_events.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/host_runtime.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/inbox.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/incremental_dates.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/incremental_failed_recovery.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/incremental_native.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/incremental_scopes.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/incremental_selection.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/index.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/inspection.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/legacy_update_journal.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/llm/__init__.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/llm/base.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/llm/claude_compatible.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/llm/gemini.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/llm/openai_compatible.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/llm/router.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/llm/thinking.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/locking.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/memory_commit.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/memory_planner.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/memory_retraction.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/memory_update.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/migration.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/model_capabilities.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/model_discovery.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/model_execution.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/models.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/native_index.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/native_registration.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/parallel_model.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/planning_context.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/process_common.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/process_jobs.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/process_journal.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/process_owner.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/processing.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/prompts.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/provenance.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/provider_compatibility.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/query_clock.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/query_scan.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/receipt_codec.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/recording_policy.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/redaction.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/remember_route.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/retention.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/retrieval.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/scope_maintenance.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/scope_state.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/semantic_maintenance.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/semantic_protocol.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/service.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/single_pass_memory_planner.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/single_pass_plan.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/source_policy.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/state_layout.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/subprocess_flags.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/summary_batch.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/target_reconciliation.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/turn_audit.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/turn_plan.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/update_coordinator.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/update_review.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/validation.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf/vault.py +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf.egg-info/dependency_links.txt +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf.egg-info/entry_points.txt +0 -0
- {memleaf-0.2.84 → memleaf-0.2.86}/src/memleaf.egg-info/top_level.txt +0 -0
|
@@ -4,6 +4,54 @@ All notable changes to memleaf are documented here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.2.86 — 2026-10-04
|
|
8
|
+
|
|
9
|
+
### Source-bound Hermes retention and general extraction compatibility
|
|
10
|
+
|
|
11
|
+
- Queue Hermes explicit retention against a stable host turn, then use the
|
|
12
|
+
actual captured user messages, IDs, timestamps and order. Register the native
|
|
13
|
+
`memleaf_remember` tool before Provider initialization and configure Hermes to
|
|
14
|
+
exclude the unbound standalone MCP `remember` tool without changing the public
|
|
15
|
+
MCP tool list. Queued retention is not reported as a completed save.
|
|
16
|
+
- Preserve bound intent identity across a unique declared compression lineage.
|
|
17
|
+
Atomically admit one run per intent and check recording authorization across
|
|
18
|
+
the whole lineage before model dispatch, commit and frozen-work recovery.
|
|
19
|
+
Unrelated sessions, stale live tokens and ambiguous lineage remain rejected.
|
|
20
|
+
- Normalize equivalent flat/nested field representations with the same compiler
|
|
21
|
+
and review rules; reject conflicts, retain unknown fields as unknown, and store
|
|
22
|
+
retraction explanations separately from the empty retracted body.
|
|
23
|
+
- Add a bounded semantic veto for proposed actions, responsibility and deadlines.
|
|
24
|
+
It can only reject unsupported proposals or remove unverified fields, never
|
|
25
|
+
invent facts or authorization. Extraction and verification share the existing
|
|
26
|
+
two-request limit; malformed verification blocks writes.
|
|
27
|
+
- Rank existing comparison candidates using generic normalized terms and their
|
|
28
|
+
rarity. Keep scope, revision and candidate bounds; explicit write receipts
|
|
29
|
+
prioritize comparison without skipping independent new information or forcing
|
|
30
|
+
similar-title merges.
|
|
31
|
+
- Maintain proven completed runtime records before admitting new work, using
|
|
32
|
+
existing lossless compaction under the writer lock. Preserve unresolved runs
|
|
33
|
+
and spent allowances. Return specific capacity errors and explicit partial or
|
|
34
|
+
stale-query guidance instead of implying all work completed.
|
|
35
|
+
- Fix history source statistics double counting while retaining exact legacy
|
|
36
|
+
recovery compatibility. No historical memory or failed-run migration occurs.
|
|
37
|
+
- Validation: 587 affected checks, 32 checks in the actual Hermes interpreter,
|
|
38
|
+
64 isolated real-model turns and 58 developer semantic checks passed. Native
|
|
39
|
+
Hermes MemoryManager/stdio MCP tests covered source times, compression,
|
|
40
|
+
cross-midnight updates and repeated callbacks. Full Desktop acceptance still
|
|
41
|
+
requires installation and a Hermes restart.
|
|
42
|
+
|
|
43
|
+
## 0.2.85 — 2026-10-03
|
|
44
|
+
|
|
45
|
+
### Incremental field protocol
|
|
46
|
+
|
|
47
|
+
- Stop requiring redundant model `deadline_decision` assertions; absent UPDATE
|
|
48
|
+
deadline fields preserve the current deadline. Older assertions remain checked
|
|
49
|
+
for contradictions, and selected/cancelled deadlines still require source quotes.
|
|
50
|
+
- Allow CREATE to omit an unknown assignee and store null without inventing a
|
|
51
|
+
responsibility observation. Non-null responsibility changes still require proof.
|
|
52
|
+
- Consolidate the model field instructions and examples without extra model calls;
|
|
53
|
+
advance the semantic protocol to `incremental-turn-v12` for cached-run guards.
|
|
54
|
+
|
|
7
55
|
## 0.2.84 — 2026-09-30
|
|
8
56
|
|
|
9
57
|
### Task lifecycle, Hermes source times and read-linked comparison
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.86
|
|
4
4
|
Summary: A local-first Markdown memory core for AI agents
|
|
5
5
|
Author: memleaf contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -23,10 +23,10 @@ Dynamic: license-file
|
|
|
23
23
|
|
|
24
24
|
[English](README.en.md) · [PyPI](https://pypi.org/project/memleaf/) · [GitHub](https://github.com/miffyblueboo/memleaf)
|
|
25
25
|
|
|
26
|
-
> **版本:0.2.
|
|
26
|
+
> **版本:0.2.86。**
|
|
27
27
|
> 自动提炼和复核共用同一条“未来记忆价值”标准:模型综合未来复用、信息增量、再次读取时的直接可用性和忘记成本,只保留对未来理解、判断或行动有实质影响的最小核心;没有明确价值的信息不提炼,不按具体业务场景硬编码排除。
|
|
28
28
|
> 提炼先比较旧事项中的编号、目标、进展、责任和期限;补全未知信息也应维护原事项,另建独立待办不能替代项目事实更新。Core 不按标题强行合并。
|
|
29
|
-
>
|
|
29
|
+
> 新请求兼容等价的字段表示;未知执行人保持 `null`,新增或变更负责人、等待方和期限需要真实来源。行动及相关字段经过有界语义复核,提炼与复核共用每份工作的最多两次模型请求额度;复核失败不写入。来源时间用于解析相对日期,不补造事实日期。
|
|
30
30
|
> 待办 `due_date` 只表示待办动作本身明确声明的截止日;属于待办主题或预期结果的日期不会被误作截止日。项目归属由模型按语义判断,不要求项目名在证据里逐字出现。复核阶段会保留首轮已选定的更新目标,避免“状态变更”被降级成新增记忆。Markdown 仍是唯一事实源。
|
|
31
31
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
32
32
|
|
|
@@ -166,12 +166,14 @@ python -m pip install -U memleaf && python -m memleaf install
|
|
|
166
166
|
3. 发现并保存可用的聊天模型路由;Hermes CLI 返回的脱敏凭证不会被当成真实 API key,而会继续尝试环境变量和 Hermes `.env`;已有有效 memleaf 路由会保留;
|
|
167
167
|
4. 激活 `memory.provider=memleaf`;
|
|
168
168
|
5. 通过 Hermes 官方 CLI 配置 memleaf MCP;
|
|
169
|
-
6. 配置 MCP lazy/idle
|
|
169
|
+
6. 配置 MCP lazy/idle 生命周期,以及绑定真实用户消息的 Hermes 原生保存工具;
|
|
170
170
|
7. 验证 MCP Server 能发现当前版本声明的全部工具;
|
|
171
171
|
8. 写入本地 Agent 状态索引。
|
|
172
172
|
|
|
173
173
|
完成后重启 Hermes。
|
|
174
174
|
|
|
175
|
+
Hermes 显式保存使用原生 `memleaf_remember`:先绑定本轮并排队,轮次完成后再以真实用户消息提炼,排队不表示已保存。安装器会在 Hermes 中屏蔽独立 MCP `remember`,保留其他工具选择;Core/Python API 和其他客户端的 `remember` 继续可用。仅升级 pip 包不足以更新 Provider 和这项工具配置。
|
|
176
|
+
|
|
175
177
|
如果没有检测到 Hermes、无法取得完整模型路由、Provider 激活失败或 MCP 工具发现验证失败,安装会明确返回失败,不会把未完成的接入报告为成功。
|
|
176
178
|
|
|
177
179
|
仓库中的 `install.sh` 保留给源码开发、离线源码安装和故障排查;普通 PyPI 用户不需要执行它。
|
|
@@ -359,6 +361,8 @@ python -m memleaf.mcp_server --vault /path/to/your/vault \
|
|
|
359
361
|
|
|
360
362
|
捕获、索引、目录检索和读取可离线运行;`process()`、`remember()` 和 `compact()` 需要可用的模型能力。
|
|
361
363
|
|
|
364
|
+
包含行动或责任、期限变更的增量候选通常会追加一次语义复核;每份增量工作最多两次请求,格式修复也计入同一额度。
|
|
365
|
+
|
|
362
366
|
`llm.mode` 支持:
|
|
363
367
|
|
|
364
368
|
- `auto`:优先使用显式注入的 host backend,失败后回退到完整 API 路由;
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
[中文](README.md) · [PyPI](https://pypi.org/project/memleaf/) · [GitHub](https://github.com/miffyblueboo/memleaf)
|
|
6
6
|
|
|
7
|
-
> **Version: 0.2.
|
|
7
|
+
> **Version: 0.2.86.**
|
|
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. Core does not merge matching titles.
|
|
10
|
-
> New requests
|
|
10
|
+
> New requests accept equivalent field representations. Unknown executors remain `null`; new or changed responsibility, dependencies and deadlines require actual source evidence. Proposed actions and related fields undergo bounded semantic verification within the existing two-request limit per work item; failed verification blocks writes. Source time resolves relative dates without inventing factual dates.
|
|
11
11
|
> A todo's `due_date` means only the explicit deadline of the todo action; dates describing its subject or desired outcome are not deadlines. Project ownership remains the model's semantic judgement and needs no literal name match. The review stage now keeps the update target chosen by the first pass, so a state change is no longer downgraded into a new memory. Markdown remains the sole source of truth.
|
|
12
12
|
> **The current release supports Hermes and Codex.** Antigravity is not detected, installed, or configured.
|
|
13
13
|
|
|
@@ -147,12 +147,14 @@ Both installation paths automatically:
|
|
|
147
147
|
3. Discover and save a callable chat-model route. Redacted credentials returned by the Hermes CLI are never treated as real API keys; discovery falls through to environment variables and Hermes `.env`, while preserving an existing valid memleaf route.
|
|
148
148
|
4. Activate `memory.provider=memleaf`.
|
|
149
149
|
5. Configure the memleaf MCP entry through Hermes' official CLI.
|
|
150
|
-
6. Configure MCP lazy/idle lifecycle settings.
|
|
150
|
+
6. Configure MCP lazy/idle lifecycle settings and the source-bound native Hermes retention tool.
|
|
151
151
|
7. Verify that the MCP server exposes all tools declared by the installed version.
|
|
152
152
|
8. Record the local Agent integration status.
|
|
153
153
|
|
|
154
154
|
Restart Hermes after installation.
|
|
155
155
|
|
|
156
|
+
Hermes uses native `memleaf_remember` to queue a request bound to the current turn, then retain the actual user messages after the turn completes. Queued does not mean saved. The installer excludes standalone MCP `remember` in Hermes while preserving other tool choices; the Core/Python API and other clients keep `remember`. Upgrading only the pip package does not refresh the Provider or this tool configuration.
|
|
157
|
+
|
|
156
158
|
If Hermes cannot be detected, no complete model route can be configured, Provider activation fails, or MCP tool-discovery verification fails, the installer returns an explicit failure rather than reporting an incomplete integration as successful.
|
|
157
159
|
|
|
158
160
|
The repository `install.sh` remains for source development, offline source installation, and troubleshooting. Normal PyPI users do not need to run it.
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
[English](README.en.md) · [PyPI](https://pypi.org/project/memleaf/) · [GitHub](https://github.com/miffyblueboo/memleaf)
|
|
6
6
|
|
|
7
|
-
> **版本:0.2.
|
|
7
|
+
> **版本:0.2.86。**
|
|
8
8
|
> 自动提炼和复核共用同一条“未来记忆价值”标准:模型综合未来复用、信息增量、再次读取时的直接可用性和忘记成本,只保留对未来理解、判断或行动有实质影响的最小核心;没有明确价值的信息不提炼,不按具体业务场景硬编码排除。
|
|
9
9
|
> 提炼先比较旧事项中的编号、目标、进展、责任和期限;补全未知信息也应维护原事项,另建独立待办不能替代项目事实更新。Core 不按标题强行合并。
|
|
10
|
-
>
|
|
10
|
+
> 新请求兼容等价的字段表示;未知执行人保持 `null`,新增或变更负责人、等待方和期限需要真实来源。行动及相关字段经过有界语义复核,提炼与复核共用每份工作的最多两次模型请求额度;复核失败不写入。来源时间用于解析相对日期,不补造事实日期。
|
|
11
11
|
> 待办 `due_date` 只表示待办动作本身明确声明的截止日;属于待办主题或预期结果的日期不会被误作截止日。项目归属由模型按语义判断,不要求项目名在证据里逐字出现。复核阶段会保留首轮已选定的更新目标,避免“状态变更”被降级成新增记忆。Markdown 仍是唯一事实源。
|
|
12
12
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
13
13
|
|
|
@@ -147,12 +147,14 @@ python -m pip install -U memleaf && python -m memleaf install
|
|
|
147
147
|
3. 发现并保存可用的聊天模型路由;Hermes CLI 返回的脱敏凭证不会被当成真实 API key,而会继续尝试环境变量和 Hermes `.env`;已有有效 memleaf 路由会保留;
|
|
148
148
|
4. 激活 `memory.provider=memleaf`;
|
|
149
149
|
5. 通过 Hermes 官方 CLI 配置 memleaf MCP;
|
|
150
|
-
6. 配置 MCP lazy/idle
|
|
150
|
+
6. 配置 MCP lazy/idle 生命周期,以及绑定真实用户消息的 Hermes 原生保存工具;
|
|
151
151
|
7. 验证 MCP Server 能发现当前版本声明的全部工具;
|
|
152
152
|
8. 写入本地 Agent 状态索引。
|
|
153
153
|
|
|
154
154
|
完成后重启 Hermes。
|
|
155
155
|
|
|
156
|
+
Hermes 显式保存使用原生 `memleaf_remember`:先绑定本轮并排队,轮次完成后再以真实用户消息提炼,排队不表示已保存。安装器会在 Hermes 中屏蔽独立 MCP `remember`,保留其他工具选择;Core/Python API 和其他客户端的 `remember` 继续可用。仅升级 pip 包不足以更新 Provider 和这项工具配置。
|
|
157
|
+
|
|
156
158
|
如果没有检测到 Hermes、无法取得完整模型路由、Provider 激活失败或 MCP 工具发现验证失败,安装会明确返回失败,不会把未完成的接入报告为成功。
|
|
157
159
|
|
|
158
160
|
仓库中的 `install.sh` 保留给源码开发、离线源码安装和故障排查;普通 PyPI 用户不需要执行它。
|
|
@@ -340,6 +342,8 @@ python -m memleaf.mcp_server --vault /path/to/your/vault \
|
|
|
340
342
|
|
|
341
343
|
捕获、索引、目录检索和读取可离线运行;`process()`、`remember()` 和 `compact()` 需要可用的模型能力。
|
|
342
344
|
|
|
345
|
+
包含行动或责任、期限变更的增量候选通常会追加一次语义复核;每份增量工作最多两次请求,格式修复也计入同一额度。
|
|
346
|
+
|
|
343
347
|
`llm.mode` 支持:
|
|
344
348
|
|
|
345
349
|
- `auto`:优先使用显式注入的 host backend,失败后回退到完整 API 路由;
|
|
@@ -61,12 +61,15 @@ The version is `incremental-items-v1`; only `{"items": [...]}` is accepted.
|
|
|
61
61
|
|
|
62
62
|
New prepared requests also carry `extraction_contract=field-reviewed-v1`:
|
|
63
63
|
|
|
64
|
-
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
- CREATE supplies `memory.deadline` only for an established deadline. UPDATE
|
|
65
|
+
supplies `patch.deadline` for a change or explicit cancellation; omission
|
|
66
|
+
preserves the current deadline. The model does not need `deadline_decision`.
|
|
67
|
+
Older outputs may retain it, but contradictions with the actual fields are
|
|
68
|
+
still rejected. A deadline mentioned only in prose is insufficient; Core
|
|
69
|
+
does not infer a deadline from the body or detect every semantic omission.
|
|
70
|
+
- CREATE may omit `memory.assignee` when the executor is unknown; Core stores
|
|
71
|
+
null. UPDATE omission preserves the current executor. New or changed non-null
|
|
72
|
+
`assignee`/`waiting_on` requires row-level
|
|
70
73
|
`responsibility_basis`, keyed by that field, with `{ref, text}` citing an exact
|
|
71
74
|
source quote in the row's evidence. An unchanged inherited owner needs no new
|
|
72
75
|
quote. Field provenance uses the selected source rather than the generic row
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Local-first Markdown memory core for AI agents."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.2.
|
|
3
|
+
__version__ = "0.2.86"
|
|
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
|
|
@@ -196,7 +196,7 @@ def plan_suite(suite: dict, *, repeats: int = 5) -> dict:
|
|
|
196
196
|
turns = sum(len(c["turns"]) for c in suite["cases"])
|
|
197
197
|
return {"version": VERSION, "suite_id": suite["suite_id"], "suite_sha256": _hash(_canonical(suite)),
|
|
198
198
|
"cases": len(suite["cases"]), "turns_per_repeat": turns, "repeats": repeats,
|
|
199
|
-
"normal_request_upper_bound": turns * repeats,
|
|
199
|
+
"normal_request_upper_bound": turns * repeats * 2,
|
|
200
200
|
"recovery_request_upper_bound": turns * repeats * 2,
|
|
201
201
|
"model_calls": 0, "mode": "plan", "semantic_status": "not_run",
|
|
202
202
|
"switch_authorized": False, "prompt_sha256": _hash(INCREMENTAL_SYSTEM.encode("utf-8")),
|
|
@@ -420,6 +420,58 @@ class HermesAdapter:
|
|
|
420
420
|
return False
|
|
421
421
|
return True
|
|
422
422
|
|
|
423
|
+
def configure_retention_tools(self, detection=None):
|
|
424
|
+
"""Keep one host-bound retention entry, preserving unrelated tool choices."""
|
|
425
|
+
import fnmatch
|
|
426
|
+
detection = detection or self.detect()
|
|
427
|
+
if detection.confidence != "high" or not detection.executable:
|
|
428
|
+
return False
|
|
429
|
+
key = "mcp_servers.memleaf.tools"
|
|
430
|
+
command = [detection.executable, "config", "get", key, "--json"]
|
|
431
|
+
try:
|
|
432
|
+
result = run_argv(self.runner, command, env=self.env)
|
|
433
|
+
if result.returncode == 0:
|
|
434
|
+
filters = json.loads(result.stdout)
|
|
435
|
+
if not isinstance(filters, dict):
|
|
436
|
+
return False
|
|
437
|
+
elif "Config key not set: " + key in result.stdout + result.stderr:
|
|
438
|
+
filters = {}
|
|
439
|
+
else:
|
|
440
|
+
return False
|
|
441
|
+
updated = dict(filters)
|
|
442
|
+
from ..mcp_server import _TOOLS
|
|
443
|
+
def patterns(value):
|
|
444
|
+
if isinstance(value, str):
|
|
445
|
+
return [value]
|
|
446
|
+
if isinstance(value, list) and all(isinstance(x, str) for x in value):
|
|
447
|
+
return value
|
|
448
|
+
raise ValueError("invalid tool filter")
|
|
449
|
+
if "include" in updated:
|
|
450
|
+
selected = patterns(updated["include"])
|
|
451
|
+
managed = updated.pop("memleaf_bound_retention", None)
|
|
452
|
+
if (isinstance(managed, dict) and set(managed) == {"patterns", "applied"}
|
|
453
|
+
and managed["applied"] == selected):
|
|
454
|
+
selected = patterns(managed["patterns"])
|
|
455
|
+
known = [tool["name"] for tool in _TOOLS if tool["name"] != "remember"
|
|
456
|
+
and any(fnmatch.fnmatchcase(tool["name"], pattern) for pattern in selected)]
|
|
457
|
+
# Retain explicit future names and original wildcard intent so
|
|
458
|
+
# reinstalling a newer declaration set does not freeze it.
|
|
459
|
+
literal = [name for name in selected if name != "remember" and not any(c in name for c in "*?[")]
|
|
460
|
+
updated["include"] = list(dict.fromkeys([*known, *literal]))
|
|
461
|
+
updated["memleaf_bound_retention"] = {"patterns": selected, "applied": updated["include"]}
|
|
462
|
+
else:
|
|
463
|
+
updated["exclude"] = list(dict.fromkeys([*patterns(updated.get("exclude", [])), "remember"]))
|
|
464
|
+
if updated == filters:
|
|
465
|
+
return True
|
|
466
|
+
written = run_argv(self.runner, [detection.executable, "config", "set", key,
|
|
467
|
+
json.dumps(updated, ensure_ascii=False)], env=self.env)
|
|
468
|
+
if written.returncode != 0:
|
|
469
|
+
return False
|
|
470
|
+
confirmed = run_argv(self.runner, command, env=self.env)
|
|
471
|
+
return confirmed.returncode == 0 and json.loads(confirmed.stdout) == updated
|
|
472
|
+
except (OSError, ValueError, TypeError):
|
|
473
|
+
return False
|
|
474
|
+
|
|
423
475
|
def test_mcp(
|
|
424
476
|
self,
|
|
425
477
|
detection: Detection | None = None,
|
|
@@ -10,7 +10,11 @@ Automatic capture and processing use only the visible user and assistant text.
|
|
|
10
10
|
Tool calls and results, email or attachment bodies, and other hidden payloads are
|
|
11
11
|
not automatic memory input.
|
|
12
12
|
Hermes uses MCP `search` and `read` with the current `retrieval_id` for recall.
|
|
13
|
-
|
|
13
|
+
The native `memleaf_remember` tool queues explicit retention bound to the current
|
|
14
|
+
turn; the completed capture supplies the original user messages and source times.
|
|
15
|
+
A queued response is not a saved memory. The installer excludes standalone MCP
|
|
16
|
+
`remember` in Hermes while preserving other configured tool choices. MCP still
|
|
17
|
+
supports `remember` for other clients, plus `forget` and maintenance operations.
|
|
14
18
|
This is a Soft Gate, not a guarantee that every answer has performed retrieval.
|
|
15
19
|
|
|
16
20
|
Hermes discovers this directory when it is installed as:
|
|
@@ -1380,6 +1380,7 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
1380
1380
|
"current turn's retrieval_id exactly as supplied; a missing or mismatched "
|
|
1381
1381
|
"token is a read failure, never a reason to fall back to a file tool. "
|
|
1382
1382
|
"Read more only if needed; for ordinary relevance queries, do not read all entries to filter unrelated items. "
|
|
1383
|
+
"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. "
|
|
1383
1384
|
"When the user asks for current "
|
|
1384
1385
|
"todos, all unfinished work, urgent work, or work due in a time range, call memleaf MCP "
|
|
1385
1386
|
"list_todos instead of relevance search; omit scope for a global query, follow every "
|
|
@@ -1390,7 +1391,7 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
1390
1391
|
"turns are durably captured into the local memleaf inbox. Automatic "
|
|
1391
1392
|
"capture → process runs after your final answer and is authoritative: "
|
|
1392
1393
|
"a capture only proves that the inbox received the turn. Until a "
|
|
1393
|
-
"
|
|
1394
|
+
"explicit tool result proves execution_status=completed and complete coverage, never "
|
|
1394
1395
|
"say that the turn was saved to permanent memory, persisted, or "
|
|
1395
1396
|
"记好了/已落库. For automatic handling, say only that you will process "
|
|
1396
1397
|
"it through the workflow or that it is recorded in the current "
|
|
@@ -1402,7 +1403,8 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
1402
1403
|
"file writes to simulate success, and do not infer automatic success merely because "
|
|
1403
1404
|
"active or history files exist. Automatic recall is a directory of "
|
|
1404
1405
|
"scope identifiers, hierarchy, and aliases only; it never contains "
|
|
1405
|
-
"memory IDs, titles, or bodies.
|
|
1406
|
+
"memory IDs, titles, or bodies. For explicit retention use memleaf_remember with the current retrieval_id; it retains verified user messages after the complete turn is captured. Pending means not yet saved. Do not submit assistant-written facts through legacy MCP remember. "
|
|
1407
|
+
"Use deliberate remember/forget tools "
|
|
1406
1408
|
"only when the user explicitly asks for that operation. Automatic "
|
|
1407
1409
|
"capture and processing use only visible user and assistant text; "
|
|
1408
1410
|
"tool calls/results, email or attachment bodies, and other hidden "
|
|
@@ -2027,6 +2029,10 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
2027
2029
|
retrieval_turn_id=retrieval_turn_id if role == "assistant" else None,
|
|
2028
2030
|
):
|
|
2029
2031
|
return
|
|
2032
|
+
if self._gate_enabled:
|
|
2033
|
+
self._call("remember_turn", {"phase": "complete", "source": "hermes",
|
|
2034
|
+
"session_id": effective_session, "turn_id": turn_id},
|
|
2035
|
+
stage="remember_turn", session_id=effective_session, turn_id=turn_id)
|
|
2030
2036
|
if not self._auto_process:
|
|
2031
2037
|
return
|
|
2032
2038
|
if not lineage_ready:
|
|
@@ -2072,9 +2078,9 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
2072
2078
|
bound.update(source="hermes", session_id=session, turn_id=turn_id,
|
|
2073
2079
|
intent_id="host-op-" + sha256((turn_id + "/" + tool_call_id + "/" + name).encode()).hexdigest())
|
|
2074
2080
|
bound.pop("event_id", None)
|
|
2075
|
-
#
|
|
2076
|
-
#
|
|
2077
|
-
|
|
2081
|
+
# New hosts route retention to the source-bound lifecycle tool.
|
|
2082
|
+
# Never authorize the model's rewritten text as user evidence.
|
|
2083
|
+
raise ValueError("memory_host_retention_requires_bound_tool")
|
|
2078
2084
|
elif name == "scope_catalog":
|
|
2079
2085
|
bound.update(source="hermes", session_id=session, turn_id=turn_id)
|
|
2080
2086
|
else:
|
|
@@ -2092,10 +2098,25 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
2092
2098
|
return bound
|
|
2093
2099
|
|
|
2094
2100
|
def get_tool_schemas(self) -> List[Dict[str, Any]]:
|
|
2095
|
-
#
|
|
2096
|
-
#
|
|
2097
|
-
|
|
2098
|
-
|
|
2101
|
+
# Hermes registers schemas before initialize_all. Execution remains
|
|
2102
|
+
# gated by the initialized host identity and write policy.
|
|
2103
|
+
if not self._write_enabled:
|
|
2104
|
+
return []
|
|
2105
|
+
return [{"name": "memleaf_remember", "description": "Use only for an explicit user request to remember. Bind the actual complete user turn; never submit rewritten facts or scopes. Returns pending until the turn is captured and processed.",
|
|
2106
|
+
"parameters": {"type": "object", "properties": {"retrieval_id": {"type": "string"}},
|
|
2107
|
+
"required": ["retrieval_id"], "additionalProperties": False}}]
|
|
2108
|
+
|
|
2109
|
+
def handle_tool_call(self, tool_name: str, args: Dict[str, Any], **kwargs: Any) -> str:
|
|
2110
|
+
if (tool_name != "memleaf_remember" or not self._write_enabled or not self._gate_enabled
|
|
2111
|
+
or not isinstance(args, dict) or set(args) != {"retrieval_id"}):
|
|
2112
|
+
return json.dumps({"error": {"code": "invalid_host_retention_request"}})
|
|
2113
|
+
session = self._canonical_session_id(self._session_id)
|
|
2114
|
+
if not isinstance(args["retrieval_id"], str) or args["retrieval_id"] != self._current_gate_id(session):
|
|
2115
|
+
return json.dumps({"error": {"code": "host_retention_turn_mismatch"}, "saved": False})
|
|
2116
|
+
result = self._call("remember_turn", {"phase": "queue", "retrieval_id": args["retrieval_id"]}, stage="remember_turn")
|
|
2117
|
+
if result is _CALL_FAILED:
|
|
2118
|
+
return json.dumps({"error": {"code": "host_retention_not_queued"}, "saved": False})
|
|
2119
|
+
return json.dumps(result, ensure_ascii=False)
|
|
2099
2120
|
|
|
2100
2121
|
def shutdown(self) -> None:
|
|
2101
2122
|
with self._sync_lock:
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Durable retention intent bound to a host turn, never model-written facts."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .incremental_journal import digest
|
|
5
|
+
from .incremental_selection import explicit_run_id, bind_selection
|
|
6
|
+
from .incremental_run_state import load_run, public_result
|
|
7
|
+
from .process_common import _read_processed
|
|
8
|
+
from .locking import atomic_write_json
|
|
9
|
+
from .retrieval_gate import validate_turn, validate_current_turn
|
|
10
|
+
|
|
11
|
+
KEY = "host_retention_intents"
|
|
12
|
+
MAX_PENDING = 128
|
|
13
|
+
INTENT_PREFIX = "host-retain-"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def pending_intents(processed):
|
|
17
|
+
pending = processed.get(KEY, {})
|
|
18
|
+
if not isinstance(pending, dict) or len(pending) > MAX_PENDING:
|
|
19
|
+
raise ValueError("invalid_host_retention_intents")
|
|
20
|
+
for identity, row in pending.items():
|
|
21
|
+
if (not isinstance(row, dict) or set(row) != {"source", "session_id", "turn_id", "intent_id"}
|
|
22
|
+
or row["source"] != "hermes"
|
|
23
|
+
or any(not isinstance(row[k], str) or not row[k] for k in ("session_id", "turn_id"))
|
|
24
|
+
or identity != digest([row["source"], row["session_id"], row["turn_id"]])
|
|
25
|
+
or row["intent_id"] != "host-retain-" + identity):
|
|
26
|
+
raise ValueError("invalid_host_retention_intents")
|
|
27
|
+
return pending
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _bound_intent(processed, source, session_id, *, turn_id=None, captured_key=None):
|
|
31
|
+
"""Follow only declared ancestry for the SAME turn, never a latest session."""
|
|
32
|
+
from .index import turn_key
|
|
33
|
+
from .host_turn_identity import ancestors, continuation_path
|
|
34
|
+
lineage = ancestors(processed, source, session_id)
|
|
35
|
+
key = captured_key if captured_key is not None else turn_key(turn_id)
|
|
36
|
+
matches = [(identity, row) for identity, row in pending_intents(processed).items()
|
|
37
|
+
if row["source"] == source and row["session_id"] in lineage
|
|
38
|
+
and turn_key(row["turn_id"]) == key]
|
|
39
|
+
if len(matches) > 1:
|
|
40
|
+
raise ValueError("ambiguous_host_retention_intent")
|
|
41
|
+
if not matches:
|
|
42
|
+
return None
|
|
43
|
+
identity, row = matches[0]
|
|
44
|
+
# A fork is not proof that this branch owns the original queued request.
|
|
45
|
+
continuation_path(processed, source, session_id, row["session_id"])
|
|
46
|
+
return identity, row
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def check_run_authority(processed, run):
|
|
50
|
+
"""Recheck the whole frozen authority path before dispatch and commit."""
|
|
51
|
+
origin = run.get("host_retention_origin")
|
|
52
|
+
if origin is None:
|
|
53
|
+
return
|
|
54
|
+
from .host_turn_identity import continuation_path
|
|
55
|
+
from .recording_policy import recording_allowed
|
|
56
|
+
path = continuation_path(processed, run["source"], run["session_id"], origin["session_id"])
|
|
57
|
+
if path is None or any(not recording_allowed(processed, run["source"], session, run["turn_key"])
|
|
58
|
+
for session in path):
|
|
59
|
+
raise ValueError("source_recording_revoked")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def validate_origin(origin, *, source, turn_key, intent_id):
|
|
63
|
+
if origin is None:
|
|
64
|
+
return
|
|
65
|
+
from .index import turn_key as key
|
|
66
|
+
if not isinstance(origin, dict):
|
|
67
|
+
raise ValueError("invalid_host_retention_origin")
|
|
68
|
+
pending_intents({KEY: {digest([origin.get(k) for k in ("source", "session_id", "turn_id")]): origin}})
|
|
69
|
+
if (origin["source"] != source or origin["intent_id"] != intent_id or key(origin["turn_id"]) != turn_key):
|
|
70
|
+
raise ValueError("invalid_host_retention_origin")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def admit_unlocked(processed, *, source, session_id, turn_key, intent_id, run_id):
|
|
74
|
+
"""Bind a reserved host authorization in the SAME lock as run creation."""
|
|
75
|
+
if not intent_id.startswith(INTENT_PREFIX):
|
|
76
|
+
return None # Existing caller-defined explicit intent namespaces remain scoped.
|
|
77
|
+
from .incremental_run_state import KEY as RUNS
|
|
78
|
+
for identity in processed.get(RUNS, {}):
|
|
79
|
+
prior = load_run(processed, identity)
|
|
80
|
+
if prior.get("authorization_intent") == intent_id and identity != run_id:
|
|
81
|
+
raise ValueError("host_retention_source_binding_changed")
|
|
82
|
+
bound = _bound_intent(processed, source, session_id, captured_key=turn_key)
|
|
83
|
+
if bound is None or bound[1]["intent_id"] != intent_id:
|
|
84
|
+
raise ValueError("host_retention_intent_not_pending")
|
|
85
|
+
origin = dict(bound[1])
|
|
86
|
+
check_run_authority(processed, {"host_retention_origin": origin, "source": source,
|
|
87
|
+
"session_id": session_id, "turn_key": turn_key})
|
|
88
|
+
return origin
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def remember_turn(service, *, phase, retrieval_id=None, source=None, session_id=None, turn_id=None, model=None, router=None):
|
|
92
|
+
if phase == "queue":
|
|
93
|
+
state = validate_turn(service.vault, retrieval_id)
|
|
94
|
+
source, session_id, turn_id = (state.get(k) for k in ("source", "session_id", "turn_id"))
|
|
95
|
+
if source != "hermes":
|
|
96
|
+
raise ValueError("host_retention_identity_required")
|
|
97
|
+
validate_current_turn(service.vault, retrieval_id, source)
|
|
98
|
+
elif phase != "complete" or source != "hermes" or not all(isinstance(v, str) and v for v in (session_id, turn_id)):
|
|
99
|
+
raise ValueError("invalid_host_retention_phase")
|
|
100
|
+
identity = digest([source, session_id, turn_id])
|
|
101
|
+
intent = "host-retain-" + identity
|
|
102
|
+
with service.vault.lock():
|
|
103
|
+
processed = _read_processed(service.vault.processed_state_path)
|
|
104
|
+
pending = pending_intents(processed)
|
|
105
|
+
bound = _bound_intent(processed, source, session_id, turn_id=turn_id)
|
|
106
|
+
if bound is not None:
|
|
107
|
+
identity, expected = bound
|
|
108
|
+
intent = expected["intent_id"]
|
|
109
|
+
from .index import turn_key
|
|
110
|
+
from .recording_policy import recording_allowed
|
|
111
|
+
if (not recording_allowed(processed, source, session_id, turn_key(turn_id))
|
|
112
|
+
or bound is not None and not recording_allowed(processed, source, expected["session_id"], turn_key(turn_id))):
|
|
113
|
+
raise ValueError("source_recording_revoked")
|
|
114
|
+
if bound is not None:
|
|
115
|
+
check_run_authority(processed, {"host_retention_origin": expected, "source": source,
|
|
116
|
+
"session_id": session_id, "turn_key": turn_key(turn_id)})
|
|
117
|
+
if phase == "queue":
|
|
118
|
+
if identity not in pending and len(pending) >= MAX_PENDING:
|
|
119
|
+
raise ValueError("host_retention_intents_full")
|
|
120
|
+
if bound is None:
|
|
121
|
+
pending[identity] = {"source": source, "session_id": session_id, "turn_id": turn_id, "intent_id": intent}
|
|
122
|
+
processed[KEY] = pending
|
|
123
|
+
atomic_write_json(service.vault.processed_state_path, processed)
|
|
124
|
+
return {"execution_status": "queued", "saved": False,
|
|
125
|
+
"guidance": "The actual user messages will be retained after this turn finishes. Do not claim permanent memory was saved yet."}
|
|
126
|
+
if bound is None:
|
|
127
|
+
return {"execution_status": "not_requested", "saved": False, "model_calls": 0}
|
|
128
|
+
# A started intent already owns its source and allowance. Compression
|
|
129
|
+
# cannot turn it into a fresh authorization with a fresh budget.
|
|
130
|
+
from .incremental_run_state import KEY as RUNS
|
|
131
|
+
for run_id in processed.get(RUNS, {}):
|
|
132
|
+
prior = load_run(processed, run_id)
|
|
133
|
+
if (prior.get("authorization_intent") == intent
|
|
134
|
+
and (prior["source"], prior["session_id"]) != (source, session_id)):
|
|
135
|
+
raise ValueError("host_retention_source_binding_changed")
|
|
136
|
+
from .incremental_commit import _window
|
|
137
|
+
from .index import turn_key
|
|
138
|
+
turn, _ = _window(service, source, session_id, turn_key(turn_id))
|
|
139
|
+
users = [event for event in turn.events if event.role == "user"]
|
|
140
|
+
request = "\n\n".join(event.content for event in users)
|
|
141
|
+
refs = [event.event_key for event in users]
|
|
142
|
+
selection, request = bind_selection(intent, refs, request)
|
|
143
|
+
run = load_run(processed, explicit_run_id(source, session_id, selection))
|
|
144
|
+
if run is not None and run["status"] in {"completed", "completed_with_unresolved", "blocked", "failed", "cancelled"}:
|
|
145
|
+
result = public_result(run)
|
|
146
|
+
else:
|
|
147
|
+
result = service.remember_incremental(source=source, session_id=session_id, turn_id=turn_id,
|
|
148
|
+
intent_id=intent, selected_source_refs=refs, retention_request=request, model=model, router=router)
|
|
149
|
+
if result["execution_status"] == "completed":
|
|
150
|
+
with service.vault.lock():
|
|
151
|
+
processed = _read_processed(service.vault.processed_state_path)
|
|
152
|
+
if processed.get(KEY, {}).get(identity) == expected:
|
|
153
|
+
processed[KEY].pop(identity)
|
|
154
|
+
atomic_write_json(service.vault.processed_state_path, processed)
|
|
155
|
+
return result
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def complete_for_captured_turn(service, turn, *, model=None, router=None):
|
|
159
|
+
"""Resume durable queued intent after host restart, without borrowing a newer turn."""
|
|
160
|
+
with service.vault.lock():
|
|
161
|
+
processed = _read_processed(service.vault.processed_state_path)
|
|
162
|
+
bound = _bound_intent(processed, turn.source, turn.session_id, captured_key=turn.turn_key)
|
|
163
|
+
if bound is None:
|
|
164
|
+
return None
|
|
165
|
+
_, row = bound
|
|
166
|
+
return remember_turn(service, phase="complete", source=turn.source, session_id=turn.session_id,
|
|
167
|
+
turn_id=row["turn_id"], model=model, router=router)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Declared, unambiguous continuation of a host session; no latest-turn lookup."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .vault import safe_component
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def ancestors(processed, source, session_id):
|
|
8
|
+
sessions = processed.get("sessions", {})
|
|
9
|
+
path = [session_id]
|
|
10
|
+
while True:
|
|
11
|
+
state = sessions.get(f"{source}/{path[-1]}", {})
|
|
12
|
+
parent = state.get("lineage_parent_session_id")
|
|
13
|
+
if parent is None:
|
|
14
|
+
return path
|
|
15
|
+
parent = safe_component(parent, "parent session id")
|
|
16
|
+
if parent in path or len(path) >= 16:
|
|
17
|
+
raise ValueError("invalid_host_retention_lineage")
|
|
18
|
+
path.append(parent)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def continuation_path(processed, source, session_id, origin_session_id):
|
|
22
|
+
path = ancestors(processed, source, session_id)
|
|
23
|
+
if origin_session_id not in path:
|
|
24
|
+
return None
|
|
25
|
+
path = path[:path.index(origin_session_id) + 1]
|
|
26
|
+
sessions = processed.get("sessions", {})
|
|
27
|
+
for parent in path[1:]:
|
|
28
|
+
children = [name for name, state in sessions.items()
|
|
29
|
+
if name.startswith(source + "/")
|
|
30
|
+
and state.get("lineage_parent_session_id") == parent]
|
|
31
|
+
if len(children) != 1:
|
|
32
|
+
raise ValueError("ambiguous_host_retention_lineage")
|
|
33
|
+
return path
|