memleaf 0.2.80__tar.gz → 0.2.82__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.
Files changed (176) hide show
  1. {memleaf-0.2.80 → memleaf-0.2.82}/CHANGELOG.md +28 -0
  2. {memleaf-0.2.80/src/memleaf.egg-info → memleaf-0.2.82}/PKG-INFO +9 -5
  3. {memleaf-0.2.80 → memleaf-0.2.82}/README.en.md +7 -4
  4. {memleaf-0.2.80 → memleaf-0.2.82}/README.md +8 -4
  5. memleaf-0.2.82/docs/failed-run-recovery.md +56 -0
  6. {memleaf-0.2.80 → memleaf-0.2.82}/docs/hermes-mcp-runtime.md +1 -1
  7. {memleaf-0.2.80 → memleaf-0.2.82}/pyproject.toml +1 -1
  8. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/__init__.py +1 -1
  9. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/adapters/hermes.py +12 -4
  10. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/cli.py +22 -2
  11. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/hermes_provider/README.md +2 -1
  12. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/hermes_provider/plugin.yaml +1 -1
  13. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/incremental_execution.py +7 -2
  14. memleaf-0.2.82/src/memleaf/incremental_failed_recovery.py +184 -0
  15. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/incremental_run_state.py +18 -1
  16. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/installer.py +4 -3
  17. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/llm/base.py +7 -1
  18. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/mcp_server.py +21 -0
  19. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/processing_route.py +3 -1
  20. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/service.py +8 -0
  21. {memleaf-0.2.80 → memleaf-0.2.82/src/memleaf.egg-info}/PKG-INFO +9 -5
  22. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf.egg-info/SOURCES.txt +2 -0
  23. {memleaf-0.2.80 → memleaf-0.2.82}/LICENSE +0 -0
  24. {memleaf-0.2.80 → memleaf-0.2.82}/MANIFEST.in +0 -0
  25. {memleaf-0.2.80 → memleaf-0.2.82}/docs/acceptance-evidence-guide.md +0 -0
  26. {memleaf-0.2.80 → memleaf-0.2.82}/docs/automatic-processing-route.md +0 -0
  27. {memleaf-0.2.80 → memleaf-0.2.82}/docs/body-compaction.md +0 -0
  28. {memleaf-0.2.80 → memleaf-0.2.82}/docs/candidate-reconciliation.md +0 -0
  29. {memleaf-0.2.80 → memleaf-0.2.82}/docs/capture-budget-design.md +0 -0
  30. {memleaf-0.2.80 → memleaf-0.2.82}/docs/config-migrations.md +0 -0
  31. {memleaf-0.2.80 → memleaf-0.2.82}/docs/core-refactor.md +0 -0
  32. {memleaf-0.2.80 → memleaf-0.2.82}/docs/development-closeout.md +0 -0
  33. {memleaf-0.2.80 → memleaf-0.2.82}/docs/evidence-retention.md +0 -0
  34. {memleaf-0.2.80 → memleaf-0.2.82}/docs/explicit-memory-update.md +0 -0
  35. {memleaf-0.2.80 → memleaf-0.2.82}/docs/extraction-latency.md +0 -0
  36. {memleaf-0.2.80 → memleaf-0.2.82}/docs/gate-evidence-boundary.md +0 -0
  37. {memleaf-0.2.80 → memleaf-0.2.82}/docs/general-processing.md +0 -0
  38. {memleaf-0.2.80 → memleaf-0.2.82}/docs/hermes-provider-compatibility.md +0 -0
  39. {memleaf-0.2.80 → memleaf-0.2.82}/docs/host-acceptance-runbook.md +0 -0
  40. {memleaf-0.2.80 → memleaf-0.2.82}/docs/immutable-release-assets.md +0 -0
  41. {memleaf-0.2.80 → memleaf-0.2.82}/docs/incremental-commit-contract.md +0 -0
  42. {memleaf-0.2.80 → memleaf-0.2.82}/docs/incremental-execution.md +0 -0
  43. {memleaf-0.2.80 → memleaf-0.2.82}/docs/incremental-native-comparison.md +0 -0
  44. {memleaf-0.2.80 → memleaf-0.2.82}/docs/incremental-partial-recovery.md +0 -0
  45. {memleaf-0.2.80 → memleaf-0.2.82}/docs/incremental-planner-preview.md +0 -0
  46. {memleaf-0.2.80 → memleaf-0.2.82}/docs/incremental-runtime-contract.md +0 -0
  47. {memleaf-0.2.80 → memleaf-0.2.82}/docs/incremental-scope-registration.md +0 -0
  48. {memleaf-0.2.80 → memleaf-0.2.82}/docs/incremental-selected-retention.md +0 -0
  49. {memleaf-0.2.80 → memleaf-0.2.82}/docs/installed-artifact-verification.md +0 -0
  50. {memleaf-0.2.80 → memleaf-0.2.82}/docs/memory-state-contract.md +0 -0
  51. {memleaf-0.2.80 → memleaf-0.2.82}/docs/migration-preflight.md +0 -0
  52. {memleaf-0.2.80 → memleaf-0.2.82}/docs/processing-quality-acceptance.md +0 -0
  53. {memleaf-0.2.80 → memleaf-0.2.82}/docs/public-query-integrity.md +0 -0
  54. {memleaf-0.2.80 → memleaf-0.2.82}/docs/query-performance.md +0 -0
  55. {memleaf-0.2.80 → memleaf-0.2.82}/docs/remember-incremental-route.md +0 -0
  56. {memleaf-0.2.80 → memleaf-0.2.82}/docs/runtime-state-retention.md +0 -0
  57. {memleaf-0.2.80 → memleaf-0.2.82}/docs/semantic-extraction-protocol.md +0 -0
  58. {memleaf-0.2.80 → memleaf-0.2.82}/docs/source-work-contract.md +0 -0
  59. {memleaf-0.2.80 → memleaf-0.2.82}/docs/v0.2.26-processing-status.md +0 -0
  60. {memleaf-0.2.80 → memleaf-0.2.82}/examples/README.md +0 -0
  61. {memleaf-0.2.80 → memleaf-0.2.82}/examples/basic_usage.py +0 -0
  62. {memleaf-0.2.80 → memleaf-0.2.82}/examples/incremental_acceptance.json +0 -0
  63. {memleaf-0.2.80 → memleaf-0.2.82}/examples/mcp_stdio.ndjson +0 -0
  64. {memleaf-0.2.80 → memleaf-0.2.82}/examples/query_benchmark.py +0 -0
  65. {memleaf-0.2.80 → memleaf-0.2.82}/install.ps1 +0 -0
  66. {memleaf-0.2.80 → memleaf-0.2.82}/install.sh +0 -0
  67. {memleaf-0.2.80 → memleaf-0.2.82}/setup.cfg +0 -0
  68. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/__main__.py +0 -0
  69. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/acceptance.py +0 -0
  70. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/adapters/__init__.py +0 -0
  71. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/adapters/antigravity.py +0 -0
  72. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/adapters/base.py +0 -0
  73. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/adapters/codex.py +0 -0
  74. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/admission.py +0 -0
  75. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/batch_review.py +0 -0
  76. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/budget.py +0 -0
  77. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/capture.py +0 -0
  78. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/compaction.py +0 -0
  79. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/config.py +0 -0
  80. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/create_coordinator.py +0 -0
  81. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/credentials.py +0 -0
  82. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/evidence_budget.py +0 -0
  83. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/evidence_policy.py +0 -0
  84. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/evidence_structure.py +0 -0
  85. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/evidence_syntax.py +0 -0
  86. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/explicit_text_source.py +0 -0
  87. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/extraction_budget.py +0 -0
  88. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/extraction_capability.py +0 -0
  89. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/extraction_work_state.py +0 -0
  90. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/frontmatter.py +0 -0
  91. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/hermes_provider/__init__.py +0 -0
  92. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/hermes_provider/_mcp_client.py +0 -0
  93. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/hermes_provider/_provider.py +0 -0
  94. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/hermes_provider/_shared.py +0 -0
  95. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/hermes_provider/evidence_budget.py +0 -0
  96. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/hermes_provider/provider_compatibility.py +0 -0
  97. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/hermes_runtime.py +0 -0
  98. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/host_events.py +0 -0
  99. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/host_runtime.py +0 -0
  100. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/inbox.py +0 -0
  101. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/incremental_commit.py +0 -0
  102. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/incremental_dates.py +0 -0
  103. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/incremental_explicit.py +0 -0
  104. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/incremental_journal.py +0 -0
  105. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/incremental_native.py +0 -0
  106. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/incremental_partial.py +0 -0
  107. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/incremental_preview.py +0 -0
  108. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/incremental_prompts.py +0 -0
  109. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/incremental_protocol.py +0 -0
  110. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/incremental_recovery.py +0 -0
  111. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/incremental_runtime.py +0 -0
  112. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/incremental_scopes.py +0 -0
  113. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/incremental_selection.py +0 -0
  114. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/index.py +0 -0
  115. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/inspection.py +0 -0
  116. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/legacy_update_journal.py +0 -0
  117. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/llm/__init__.py +0 -0
  118. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/llm/claude_compatible.py +0 -0
  119. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/llm/gemini.py +0 -0
  120. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/llm/openai_compatible.py +0 -0
  121. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/llm/router.py +0 -0
  122. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/llm/thinking.py +0 -0
  123. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/locking.py +0 -0
  124. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/memory_commit.py +0 -0
  125. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/memory_planner.py +0 -0
  126. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/memory_retraction.py +0 -0
  127. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/memory_update.py +0 -0
  128. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/memory_writer.py +0 -0
  129. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/migration.py +0 -0
  130. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/model_capabilities.py +0 -0
  131. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/model_discovery.py +0 -0
  132. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/model_execution.py +0 -0
  133. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/models.py +0 -0
  134. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/native_index.py +0 -0
  135. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/native_registration.py +0 -0
  136. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/parallel_model.py +0 -0
  137. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/planning_context.py +0 -0
  138. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/process_common.py +0 -0
  139. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/process_jobs.py +0 -0
  140. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/process_journal.py +0 -0
  141. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/process_owner.py +0 -0
  142. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/processing.py +0 -0
  143. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/prompts.py +0 -0
  144. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/provenance.py +0 -0
  145. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/provider_compatibility.py +0 -0
  146. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/query_clock.py +0 -0
  147. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/query_progress.py +0 -0
  148. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/query_scan.py +0 -0
  149. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/receipt_codec.py +0 -0
  150. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/recording_policy.py +0 -0
  151. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/redaction.py +0 -0
  152. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/remember_route.py +0 -0
  153. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/retention.py +0 -0
  154. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/retrieval.py +0 -0
  155. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/retrieval_gate.py +0 -0
  156. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/runtime_retention.py +0 -0
  157. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/scope_maintenance.py +0 -0
  158. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/scope_state.py +0 -0
  159. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/semantic_maintenance.py +0 -0
  160. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/semantic_protocol.py +0 -0
  161. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/single_pass_memory_planner.py +0 -0
  162. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/single_pass_plan.py +0 -0
  163. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/source_policy.py +0 -0
  164. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/state_layout.py +0 -0
  165. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/subprocess_flags.py +0 -0
  166. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/summary_batch.py +0 -0
  167. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/target_reconciliation.py +0 -0
  168. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/turn_audit.py +0 -0
  169. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/turn_plan.py +0 -0
  170. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/update_coordinator.py +0 -0
  171. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/update_review.py +0 -0
  172. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/validation.py +0 -0
  173. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf/vault.py +0 -0
  174. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf.egg-info/dependency_links.txt +0 -0
  175. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf.egg-info/entry_points.txt +0 -0
  176. {memleaf-0.2.80 → memleaf-0.2.82}/src/memleaf.egg-info/top_level.txt +0 -0
@@ -4,6 +4,34 @@ All notable changes to memleaf are documented here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.2.82 — 2026-09-30
8
+
9
+ ### Hermes installation tool discovery
10
+
11
+ - Derive the Hermes MCP installation check from the installed server tool
12
+ declarations instead of a separately maintained tool-count constant. This
13
+ fixes the 0.2.81 installation rollback after adding `recover_failed_run`.
14
+ - Verify actual stdio tool discovery and Hermes count acceptance/rejection in
15
+ both installed distributions on every supported CI platform.
16
+
17
+ ## 0.2.81 — 2026-09-30
18
+
19
+ ### HTTP failure classification and controlled run recovery
20
+
21
+ - Preserve safe numeric HTTP status in model errors, request attempts and
22
+ processing diagnostics. Allow 408/500/502/503/504 to use the remaining original
23
+ transport allowance; known permanent errors stay closed.
24
+ - Add revision-bound failed automatic run preview/apply through the Python API,
25
+ `recover-run` CLI and `recover_failed_run` MCP tool. Historical unclassified
26
+ HTTP errors require explicit acknowledgement; ordinary recovery stays unchanged.
27
+ - Rebuild current comparison context for unchanged sources while retaining run,
28
+ commit and budget identities, prior attempts and recovery audit. Reject consumed,
29
+ changed or revoked sources, conflicting ownership, exhausted budgets and full
30
+ active receipt capacity. Repeated execution and interrupted writes remain bounded.
31
+ - Validation: 121 focused checks passed and 10 historical failed runs exercised
32
+ isolated recovery with an injected HTTP503, without network/model calls or
33
+ formal Vault changes. Real-model semantic quality remains unverified.
34
+
7
35
  ## 0.2.80 — 2026-09-29
8
36
 
9
37
  ### Explicit deadline annotations
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memleaf
3
- Version: 0.2.80
3
+ Version: 0.2.82
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.80。**
26
+ > **版本:0.2.82。**
27
27
  > 自动提炼和复核共用同一条“未来记忆价值”标准:模型综合未来复用、信息增量、再次读取时的直接可用性和忘记成本,只保留对未来理解、判断或行动有实质影响的最小核心;没有明确价值的信息不提炼,不按具体业务场景硬编码排除。
28
28
  > 提炼契约现在只有一份中文语义契约(旧版英文 B3 提示词已删除),首轮、主题选择、候选修复和 `semantic_maintenance` 使用同一套保留价值判断;同一件事只写一次。
29
29
  > Core 不再因为归属、日期、任务依据或跨项目而丢弃整条候选:归属缺失会归一到证据里唯一被点名的项目、否则落 `global`;日期只接受能在本候选证据里找到出处、且能锚定成 ISO 的写法(`明天`、`周五`、`月底` 都会被解析),无法解析时只丢掉日期字段,记忆照常写入;未获授权的改归属沿用目标原归属。
@@ -167,12 +167,12 @@ python -m pip install -U memleaf && python -m memleaf install
167
167
  4. 激活 `memory.provider=memleaf`;
168
168
  5. 通过 Hermes 官方 CLI 配置 memleaf MCP;
169
169
  6. 配置 MCP lazy/idle 生命周期;
170
- 7. 验证 MCP Server 能发现 13 个工具;
170
+ 7. 验证 MCP Server 能发现当前版本声明的全部工具;
171
171
  8. 写入本地 Agent 状态索引。
172
172
 
173
173
  完成后重启 Hermes。
174
174
 
175
- 如果没有检测到 Hermes、无法取得完整模型路由、Provider 激活失败或 MCP 的 13 工具验证失败,安装会明确返回失败,不会把未完成的接入报告为成功。
175
+ 如果没有检测到 Hermes、无法取得完整模型路由、Provider 激活失败或 MCP 工具发现验证失败,安装会明确返回失败,不会把未完成的接入报告为成功。
176
176
 
177
177
  仓库中的 `install.sh` 保留给源码开发、离线源码安装和故障排查;普通 PyPI 用户不需要执行它。
178
178
 
@@ -269,7 +269,7 @@ python -m memleaf.mcp_server --vault "$HOME/.memleaf"
269
269
 
270
270
  不传 `--vault` 时默认使用 `~/.memleaf`;也可以设置 `MEMLEAF_VAULT`。正常情况下不需要手工常驻,Hermes 或 Codex 会按需启动它。stdout 只输出 JSON-RPC,日志不会污染协议通道。
271
271
 
272
- 当前提供 13 个工具:
272
+ 当前提供以下工具:
273
273
 
274
274
  | 工具 | 用途 |
275
275
  | --- | --- |
@@ -277,9 +277,11 @@ python -m memleaf.mcp_server --vault "$HOME/.memleaf"
277
277
  | `context` | 兼容保留的有界轻量目录,不用于自动检索路径 |
278
278
  | `scope_catalog` | 返回 Scope、父级和别名,不返回具体记忆正文 |
279
279
  | `search` | 返回有界候选目录和 `found`/`no_match` 状态 |
280
+ | `list_todos` | 跨 Scope 按状态和日期筛选并分页列出当前待办 |
280
281
  | `read` | 使用当前 `retrieval_id` 分页读取选中的记忆正文 |
281
282
  | `process` | 处理完整 inbox 轮次并按准入规则提炼;`background=true` 时异步入队 |
282
283
  | `process_status` | 只读查询异步处理 job 状态 |
284
+ | `recover_failed_run` | 预览并显式恢复终态传输失败,保留原请求预算 |
283
285
  | `remember` | 用户明确要求时创建或更新记忆 |
284
286
  | `forget_memory` | 按精确 ID 删除一条记忆 |
285
287
  | `forget_about` | 忘记明确主题;有歧义时只返回候选 |
@@ -529,3 +531,5 @@ memleaf process --vault /path/to/existing/vault --source hermes --session-id SES
529
531
  边界在宿主采集、Core capture 和历史 inbox 的新处理处执行。
530
532
  已提交记忆不会自动删除。旧版本已冻结但未完成的计划不会被静默丢弃或重新规划,
531
533
  不含新来源边界标记的计划会停止恢复并保留现场,避免越过新的输入约定。
534
+
535
+ 历史 HTTP 失败的受控恢复及 CLI 用法见 [失败任务恢复](docs/failed-run-recovery.md)。
@@ -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.80.**
7
+ > **Version: 0.2.82.**
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
  > A single Chinese semantic contract is sent for extraction, topic selection, candidate repair, and `semantic_maintenance`; the older English B3 prompt has been deleted and B3 remains only as a compatibility read format. One topic is written once.
10
10
  > Core no longer discards a whole candidate over ownership, dates, task basis or cross-project wording. A missing owner normalizes to the single project named by that candidate's own evidence, otherwise `global`. A deadline is accepted only when it appears in the candidate's own evidence and can be anchored to ISO (`明天`, `周五`, `月底` all resolve); when it cannot, only the date field is dropped and the memory is still written. An unauthorized ownership change keeps the target's existing scope.
@@ -148,12 +148,12 @@ Both installation paths automatically:
148
148
  4. Activate `memory.provider=memleaf`.
149
149
  5. Configure the memleaf MCP entry through Hermes' official CLI.
150
150
  6. Configure MCP lazy/idle lifecycle settings.
151
- 7. Verify that the MCP server exposes all 13 tools.
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
- If Hermes cannot be detected, no complete model route can be configured, Provider activation fails, or the 13-tool MCP verification fails, the installer returns an explicit failure rather than reporting an incomplete integration as successful.
156
+ 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
157
 
158
158
  The repository `install.sh` remains for source development, offline source installation, and troubleshooting. Normal PyPI users do not need to run it.
159
159
 
@@ -250,7 +250,7 @@ python -m memleaf.mcp_server --vault "$HOME/.memleaf"
250
250
 
251
251
  Without `--vault`, the server uses `~/.memleaf`; `MEMLEAF_VAULT` can also specify the Vault. In normal use the server does not need to be kept running manually: Hermes or Codex starts it on demand. stdout contains only JSON-RPC messages so logs do not corrupt the protocol stream.
252
252
 
253
- The server currently exposes 13 tools:
253
+ The server exposes the following tools:
254
254
 
255
255
  | Tool | Purpose |
256
256
  | --- | --- |
@@ -262,6 +262,7 @@ The server currently exposes 13 tools:
262
262
  | `read` | Read a selected memory body in pages using the current `retrieval_id` |
263
263
  | `process` | Process complete inbox turns under the admission rules; `background=true` enqueues a job |
264
264
  | `process_status` | Read the status of a background processing job |
265
+ | `recover_failed_run` | Preview and explicitly recover terminal transport failures within the original request budget |
265
266
  | `remember` | Create or update memory after an explicit request |
266
267
  | `forget_memory` | Delete one memory by exact ID |
267
268
  | `forget_about` | Forget an unambiguous topic; return candidates when ambiguous |
@@ -516,3 +517,5 @@ The boundary is enforced by host capture, Core capture and new processing of his
516
517
  inbox records. Committed memories are not deleted automatically. Frozen operations without
517
518
  the conversation-only policy marker are not silently replayed or replanned; they remain
518
519
  available for inspection so the new input contract is not bypassed.
520
+
521
+ Controlled historical HTTP recovery and CLI usage: [failed-run recovery](docs/failed-run-recovery.md).
@@ -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.80。**
7
+ > **版本:0.2.82。**
8
8
  > 自动提炼和复核共用同一条“未来记忆价值”标准:模型综合未来复用、信息增量、再次读取时的直接可用性和忘记成本,只保留对未来理解、判断或行动有实质影响的最小核心;没有明确价值的信息不提炼,不按具体业务场景硬编码排除。
9
9
  > 提炼契约现在只有一份中文语义契约(旧版英文 B3 提示词已删除),首轮、主题选择、候选修复和 `semantic_maintenance` 使用同一套保留价值判断;同一件事只写一次。
10
10
  > Core 不再因为归属、日期、任务依据或跨项目而丢弃整条候选:归属缺失会归一到证据里唯一被点名的项目、否则落 `global`;日期只接受能在本候选证据里找到出处、且能锚定成 ISO 的写法(`明天`、`周五`、`月底` 都会被解析),无法解析时只丢掉日期字段,记忆照常写入;未获授权的改归属沿用目标原归属。
@@ -148,12 +148,12 @@ python -m pip install -U memleaf && python -m memleaf install
148
148
  4. 激活 `memory.provider=memleaf`;
149
149
  5. 通过 Hermes 官方 CLI 配置 memleaf MCP;
150
150
  6. 配置 MCP lazy/idle 生命周期;
151
- 7. 验证 MCP Server 能发现 13 个工具;
151
+ 7. 验证 MCP Server 能发现当前版本声明的全部工具;
152
152
  8. 写入本地 Agent 状态索引。
153
153
 
154
154
  完成后重启 Hermes。
155
155
 
156
- 如果没有检测到 Hermes、无法取得完整模型路由、Provider 激活失败或 MCP 的 13 工具验证失败,安装会明确返回失败,不会把未完成的接入报告为成功。
156
+ 如果没有检测到 Hermes、无法取得完整模型路由、Provider 激活失败或 MCP 工具发现验证失败,安装会明确返回失败,不会把未完成的接入报告为成功。
157
157
 
158
158
  仓库中的 `install.sh` 保留给源码开发、离线源码安装和故障排查;普通 PyPI 用户不需要执行它。
159
159
 
@@ -250,7 +250,7 @@ python -m memleaf.mcp_server --vault "$HOME/.memleaf"
250
250
 
251
251
  不传 `--vault` 时默认使用 `~/.memleaf`;也可以设置 `MEMLEAF_VAULT`。正常情况下不需要手工常驻,Hermes 或 Codex 会按需启动它。stdout 只输出 JSON-RPC,日志不会污染协议通道。
252
252
 
253
- 当前提供 13 个工具:
253
+ 当前提供以下工具:
254
254
 
255
255
  | 工具 | 用途 |
256
256
  | --- | --- |
@@ -258,9 +258,11 @@ python -m memleaf.mcp_server --vault "$HOME/.memleaf"
258
258
  | `context` | 兼容保留的有界轻量目录,不用于自动检索路径 |
259
259
  | `scope_catalog` | 返回 Scope、父级和别名,不返回具体记忆正文 |
260
260
  | `search` | 返回有界候选目录和 `found`/`no_match` 状态 |
261
+ | `list_todos` | 跨 Scope 按状态和日期筛选并分页列出当前待办 |
261
262
  | `read` | 使用当前 `retrieval_id` 分页读取选中的记忆正文 |
262
263
  | `process` | 处理完整 inbox 轮次并按准入规则提炼;`background=true` 时异步入队 |
263
264
  | `process_status` | 只读查询异步处理 job 状态 |
265
+ | `recover_failed_run` | 预览并显式恢复终态传输失败,保留原请求预算 |
264
266
  | `remember` | 用户明确要求时创建或更新记忆 |
265
267
  | `forget_memory` | 按精确 ID 删除一条记忆 |
266
268
  | `forget_about` | 忘记明确主题;有歧义时只返回候选 |
@@ -510,3 +512,5 @@ memleaf process --vault /path/to/existing/vault --source hermes --session-id SES
510
512
  边界在宿主采集、Core capture 和历史 inbox 的新处理处执行。
511
513
  已提交记忆不会自动删除。旧版本已冻结但未完成的计划不会被静默丢弃或重新规划,
512
514
  不含新来源边界标记的计划会停止恢复并保留现场,避免越过新的输入约定。
515
+
516
+ 历史 HTTP 失败的受控恢复及 CLI 用法见 [失败任务恢复](docs/failed-run-recovery.md)。
@@ -0,0 +1,56 @@
1
+ # HTTP 失败与历史任务恢复
2
+
3
+ HTTP 传输层保留经过校验的数字 `http_status`,不保存响应正文、URL、请求头或密钥。
4
+ 408、500、502、503、504 可在原预算仍有余额时进入 `retryable`;429 使用现有
5
+ 限流处理。400、401、403、404、501 等不因新的 HTTP 分类而获得重试。
6
+ 缺少状态码的历史 `model_http_error` 保持终态,普通 `process --recover` 不会重开它。
7
+
8
+ ## 按 run_id 预览及执行
9
+
10
+ ```sh
11
+ # 预览:只读,无模型调用。旧 HTTP 回执必须明确接受其状态码未知。
12
+ memleaf recover-run --vault /path/to/vault --run-id inc-run-... \
13
+ --allow-legacy-http --json
14
+
15
+ # 仅当 recoverable=true 时,使用此次预览返回的完整 expected_revision。
16
+ memleaf recover-run --vault /path/to/vault --run-id inc-run-... \
17
+ --allow-legacy-http --apply --expected-revision PREVIEW_REVISION --json
18
+ ```
19
+
20
+ `--allow-legacy-http` 不放行已知的永久 HTTP 错误,也不增加预算。执行使用当前
21
+ Vault 配置的固定 API 模型路由;预览不解析模型路由、不初始化 Vault、不创建锁文件。
22
+ MCP 对应工具为 `recover_failed_run`,参数为 `run_id`、`allow_legacy_http`、
23
+ `apply`(默认 false)及 `expected_revision`。
24
+
25
+ Python API:
26
+
27
+ ```python
28
+ preview = service.recover_failed_run(run_id, allow_legacy_http=True)
29
+ if preview["recoverable"]:
30
+ result = service.recover_failed_run(
31
+ run_id, dry_run=False, allow_legacy_http=True,
32
+ expected_revision=preview["expected_revision"],
33
+ )
34
+ ```
35
+
36
+ ## 恢复边界
37
+
38
+ - 只处理自动捕获任务的已知第一次传输失败,原请求预算为已用 1、尚有余额。
39
+ 显式 remember 的授权正文已清除时,不能借此重新取得保留授权。
40
+ - 原轮必须完整且可处理,来源摘要及原预算身份必须一致。来源被删除、修改、禁录,
41
+ 已有消费记录、自动提交、旧流程待提交计划或其他活动所有者时拒绝恢复。
42
+ - 原 run_id、budget_id、turn_budget_id、commit identity 和第一次尝试记录全部保留。
43
+ 执行在锁内绑定此次预览版本,并记录旧失败、旧来源窗口、旧快照及恢复时间。
44
+ 不修改原额度、不新建 run、不删除旧回执。
45
+ - 原请求正文已清除,因此执行根据当前记忆重新生成快照和请求。完整后续对话仅以
46
+ `use=context` 参与比较,不独立创建新事项,也不随本轮结算。已有日期、来源及
47
+ `stale_observation` 校验继续生效;不能证明安全的修改留下未解决项。
48
+ - 预览后状态、预算、来源或比较目标变化,需要重新预览。绑定后的调用前/提交前
49
+ 校验不允许再次悄悄更新快照。
50
+ - 第二次请求仍先预留再派发。崩溃后的未知调用保守计入预算;重复 apply 使用同一
51
+ 恢复绑定,完成后只返回旧回执。保存的回复/提交可在没有模型配置时恢复。
52
+ - compact 回执重开需要一个可用 full 槽位;容量不足时预览明确拒绝,不能删除
53
+ 回执来换取执行机会。
54
+
55
+ 真实模型可能输出 NO_MEMORY、NO_CHANGE、UPDATE、CREATE 或未解决项;入口可执行
56
+ 不等于模型语义正确,也不承诺所有历史内容都应保存。
@@ -152,7 +152,7 @@ Vault. Once changes begin, it snapshots:
152
152
  - Hermes `memleaf.json`; and
153
153
  - the installed `plugins/memleaf` Provider directory.
154
154
 
155
- If MCP persistence, MCP lifecycle configuration, the 13-tool test, Provider
155
+ If MCP persistence, MCP lifecycle configuration, the declared-tool discovery test, Provider
156
156
  copy/version validation, Provider activation, or native-source registration
157
157
  fails, those Hermes paths are restored to their pre-install state. Each
158
158
  snapshot is restored independently: a failure on one path does not prevent the
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memleaf"
7
- version = "0.2.80"
7
+ version = "0.2.82"
8
8
  description = "A local-first Markdown memory core for AI agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  """Local-first Markdown memory core for AI agents."""
2
2
 
3
- __version__ = "0.2.80"
3
+ __version__ = "0.2.82"
4
4
 
5
5
  from .config import DEFAULT_CONFIG, default_config, load_config, save_config
6
6
  from .frontmatter import FrontmatterError, dump_frontmatter, dump_yaml, load_yaml, parse_frontmatter
@@ -24,8 +24,14 @@ from .base import (
24
24
 
25
25
 
26
26
  _MCP_IDLE_TIMEOUT_SECONDS = 60
27
- MCP_EXPECTED_TOOL_COUNT = 13
28
- _MCP_EXPECTED_TOOL_COUNT = MCP_EXPECTED_TOOL_COUNT
27
+
28
+
29
+ def expected_mcp_tool_count() -> int:
30
+ """Read the installed server's declarations instead of a second tool inventory."""
31
+
32
+ from ..mcp_server import _TOOLS
33
+
34
+ return len(_TOOLS)
29
35
 
30
36
 
31
37
  def hermes_home_for_platform(
@@ -418,7 +424,7 @@ class HermesAdapter:
418
424
  self,
419
425
  detection: Detection | None = None,
420
426
  *,
421
- expected_tools: int = MCP_EXPECTED_TOOL_COUNT,
427
+ expected_tools: int | None = None,
422
428
  ) -> bool:
423
429
  """Run the official MCP test and require the expected tool count."""
424
430
 
@@ -426,7 +432,9 @@ class HermesAdapter:
426
432
  executable = detection.executable if detection.detected else None
427
433
  if detection.confidence != "high" or not executable:
428
434
  return False
429
- if isinstance(expected_tools, bool) or expected_tools < 0:
435
+ if expected_tools is None:
436
+ expected_tools = expected_mcp_tool_count()
437
+ if not isinstance(expected_tools, int) or isinstance(expected_tools, bool) or expected_tools < 0:
430
438
  return False
431
439
  try:
432
440
  result = run_argv(
@@ -100,6 +100,13 @@ def build_parser() -> argparse.ArgumentParser:
100
100
  help="allow remaining incremental transport recovery, not partial replan")
101
101
  process.add_argument("--dry-run", action="store_true", help="no source Vault writes; may call the configured model")
102
102
  process.add_argument("--json", action="store_true")
103
+ recovery = commands.add_parser("recover-run", help="preview or explicitly replan a failed automatic transport run")
104
+ recovery.add_argument("--vault", type=Path, default=None)
105
+ recovery.add_argument("--run-id", required=True)
106
+ recovery.add_argument("--apply", action="store_true", help="use the original remaining budget; requires preview revision")
107
+ recovery.add_argument("--expected-revision", default=None)
108
+ recovery.add_argument("--allow-legacy-http", action="store_true", help="explicitly retry one old HTTP failure whose status was not recorded")
109
+ recovery.add_argument("--json", action="store_true")
103
110
  maintenance = commands.add_parser("maintain-state", help="preview lossless terminal control-state compaction")
104
111
  maintenance.add_argument("--vault", type=Path, default=None)
105
112
  maintenance.add_argument("--apply", action="store_true", help="apply only to --expected-revision from preview")
@@ -274,6 +281,17 @@ def main(argv: Sequence[str] | None = None) -> int:
274
281
  from .service import Memleaf
275
282
  output = Memleaf(existing_root(args.vault)).process(
276
283
  source=args.source, session_id=args.session_id, scope=args.scope, pipeline=args.pipeline, recover=args.recover)
284
+ elif args.command == "recover-run":
285
+ from .inspection import existing_root
286
+ from .service import Memleaf
287
+ from .incremental_failed_recovery import FailedRecoveryError
288
+ service = Memleaf(Vault(existing_root(args.vault), create=False))
289
+ try:
290
+ output = service.recover_failed_run(args.run_id, dry_run=not args.apply,
291
+ expected_revision=args.expected_revision, allow_legacy_http=args.allow_legacy_http)
292
+ except FailedRecoveryError as error:
293
+ print(json.dumps({"run_id": args.run_id, "code": error.code, "status": "rejected"}))
294
+ return 1
277
295
  elif args.command == "maintain-state":
278
296
  from .inspection import existing_root
279
297
  from .service import Memleaf
@@ -318,15 +336,17 @@ def main(argv: Sequence[str] | None = None) -> int:
318
336
  print("{}")
319
337
  return 0
320
338
  if getattr(args, "json", False):
321
- print(json.dumps({"error": "operation failed" if args.command in {"audit", "process", "maintain-state", "migration-check", "migration-backup"}
339
+ print(json.dumps({"error": "operation failed" if args.command in {"audit", "process", "recover-run", "maintain-state", "migration-check", "migration-backup"}
322
340
  else "initialization failed", "stage": args.command}, ensure_ascii=False))
323
341
  else:
324
342
  action = getattr(args, "command", "init")
325
343
  print(f"memleaf {action} failed unexpectedly", file=sys.stderr)
326
344
  return 1
327
345
 
328
- if args.command in {"audit", "process", "maintain-state", "migration-check", "migration-backup"}:
346
+ if args.command in {"audit", "process", "recover-run", "maintain-state", "migration-check", "migration-backup"}:
329
347
  print(json.dumps(output, ensure_ascii=False, sort_keys=True, indent=None if args.json else 2))
348
+ if args.command == "recover-run":
349
+ return 2 if output.get("recoverable") is False or output.get("execution_status") in {"failed", "blocked", "cancelled"} else 0
330
350
  return 2 if args.command == "migration-check" and output["local_status"] != "clear" else 0
331
351
  if args.command == "host-event":
332
352
  print(json.dumps(output, ensure_ascii=False, separators=(",", ":")))
@@ -61,7 +61,8 @@ mcp_servers:
61
61
 
62
62
  Use `python -m memleaf install` to configure both entries. The installer writes
63
63
  the MCP entry through `hermes config set`, reads `config.yaml` back, and tests
64
- that all 13 tools are discoverable before it reports success. When two memleaf
64
+ that all tools declared by the installed MCP server are discoverable before it
65
+ reports success. When two memleaf
65
66
  virtual environments are present, use `--mcp-runtime current` to migrate to the
66
67
  runtime executing the installer or `--mcp-runtime existing` to retain an
67
68
  already configured executable after an exact version check. See the
@@ -1,5 +1,5 @@
1
1
  name: memleaf
2
- version: 0.2.80
2
+ version: 0.2.82
3
3
  description: "Hermes-native external MemoryProvider for local-first Markdown memory shared with the memleaf MCP server."
4
4
  hooks:
5
5
  - prefetch
@@ -28,7 +28,7 @@ from .incremental_run_state import (
28
28
  )
29
29
  from .index import turn_key
30
30
  from .inbox import captured_turn_selector
31
- from .llm.base import ModelError
31
+ from .llm.base import ModelError, HTTP_RETRYABLE_STATUSES
32
32
  from .locking import atomic_write_json
33
33
  from .models import utc_now
34
34
  from .process_common import _read_processed
@@ -295,12 +295,14 @@ def _drive(service, run_id, token, backend, calls):
295
295
  if ordinal > 1 and not run.get("partial_used"):
296
296
  request["system"] += RETRY_SYSTEM
297
297
  error_code = None
298
+ http_status = None
298
299
  response = None
299
300
  try:
300
301
  calls[0] += 1
301
302
  response = backend.complete(request["user"], system=request["system"], purpose="single_pass")
302
303
  except ModelError as error:
303
304
  error_code = error.code
305
+ http_status = error.http_status
304
306
  except Exception:
305
307
  error_code = "model_failed" # Never persist provider exception text.
306
308
  with service.vault.lock():
@@ -309,7 +311,10 @@ def _drive(service, run_id, token, backend, calls):
309
311
  return public_result(run, calls=calls[0])
310
312
  if error_code is not None:
311
313
  run["attempts"][-1]["outcome"] = error_code
312
- retryable = error_code in TRANSIENT and run["reserved_requests"] < 2
314
+ if http_status is not None:
315
+ run["attempts"][-1]["http_status"] = http_status
316
+ retryable = (error_code in TRANSIENT or
317
+ (error_code == "model_http_error" and http_status in HTTP_RETRYABLE_STATUSES)) and run["reserved_requests"] < 2
313
318
  _finish(service, processed, run, "retryable" if retryable else "failed", error_code)
314
319
  return public_result(run, calls=calls[0])
315
320
  if not isinstance(response, str) or not response.strip() or len(response.encode("utf-8")) > MAX_BYTES:
@@ -0,0 +1,184 @@
1
+ """Explicit, revision-bound recovery of failed automatic transport runs.
2
+
3
+ Preview never opens a Vault lock, calls a model or reconstructs persisted
4
+ requests. Apply rebuilds a current request for unchanged source under the same
5
+ run and budget, retaining the original failed attempt and a bounded audit.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import re
11
+ from typing import Any
12
+
13
+ from .extraction_work_state import _read_budget_state_unlocked, ExtractionWorkStateError
14
+ from .incremental_commit import _window
15
+ from .incremental_execution import (TRANSIENT, RETRY_SYSTEM, _guard_legacy, _protocol_digest,
16
+ resume_incremental_run)
17
+ from .incremental_journal import digest, load_work
18
+ from .incremental_preview import _prepare_incremental_unlocked
19
+ from .incremental_prompts import INCREMENTAL_SYSTEM
20
+ from .incremental_protocol import MAX_BYTES
21
+ from .incremental_run_state import TERMINAL, MAX_RUNS, COMPACT_VERSION, load_run, owner_live, public_result, save_run
22
+ from .llm.base import HTTP_RETRYABLE_STATUSES
23
+ from .models import utc_now
24
+ from .process_common import _read_processed
25
+ from .recording_policy import recording_allowed
26
+ from .turn_plan import input_digest, turn_identity_key
27
+ from .receipt_codec import is_compact, ledger_usage
28
+
29
+
30
+ class FailedRecoveryError(ValueError):
31
+ """Bounded public rejection, without private provider/source text."""
32
+
33
+ def __init__(self, code: str):
34
+ super().__init__(code)
35
+ self.code = code
36
+
37
+
38
+ def _reject(code: str):
39
+ raise FailedRecoveryError(code)
40
+
41
+
42
+ def _plan(service: Any, run_id: str, *, allow_legacy_http: bool):
43
+ processed = _read_processed(service.vault.processed_state_path)
44
+ run = load_run(processed, run_id)
45
+ if run is None:
46
+ _reject('incremental_run_not_found')
47
+ if (is_compact(processed['incremental_runs'][run_id], COMPACT_VERSION)
48
+ and ledger_usage(processed['incremental_runs'], compact_version=COMPACT_VERSION)['full'] >= MAX_RUNS):
49
+ _reject('active_run_capacity_unavailable')
50
+ if turn_identity_key(run['source'], run['session_id'], run['turn_key']) in processed.get('pending_turn_plans', {}):
51
+ _reject('legacy_pending_plan')
52
+ if run['status'] != 'failed' or 'terminal_recovery' in run:
53
+ _reject('terminal_recovery_not_available')
54
+ if run.get('request_kind', 'automatic') != 'automatic' or run['arguments'].get('selection'):
55
+ _reject('explicit_retention_requires_new_authorization')
56
+ if owner_live(processed):
57
+ _reject('incremental_model_busy')
58
+ _guard_legacy(service, processed)
59
+ attempts = run['attempts']
60
+ # Only a known failed first transport dispatch. Unknown reservations,
61
+ # responses, partial commits and exhausted attempts cannot be reopened.
62
+ if (len(attempts) != 1 or attempts[0]['ordinal'] != 1
63
+ or attempts[0]['outcome'] != run.get('code')
64
+ or run['reserved_requests'] != 1 or run.get('partial_used')):
65
+ _reject('terminal_recovery_attempt_not_eligible')
66
+ code = run.get('code')
67
+ status = attempts[0].get('http_status')
68
+ legacy = code == 'model_http_error' and status is None
69
+ if code == 'model_http_error':
70
+ if legacy and not allow_legacy_http:
71
+ _reject('legacy_http_status_unknown')
72
+ if not legacy and status not in HTTP_RETRYABLE_STATUSES:
73
+ _reject('http_error_not_retryable')
74
+ elif code not in TRANSIENT:
75
+ _reject('failure_not_retryable')
76
+ if not recording_allowed(processed, run['source'], run['session_id'], run['turn_key']):
77
+ _reject('source_recording_revoked')
78
+ if run['commit_work_id'] in processed.get('incremental_commits', {}):
79
+ _reject('source_already_owned')
80
+ turn, window = _window(service, run['source'], run['session_id'], run['turn_key'])
81
+ if not turn.processable or input_digest(turn) != run['source_digest']:
82
+ _reject('source_changed')
83
+ from .extraction_work_state import extraction_work_id
84
+ if extraction_work_id(turn, request_kind='automatic', intent_id='automatic') != run['budget_id']:
85
+ _reject('source_budget_identity_changed')
86
+ session = processed.get('sessions', {}).get(f"{run['source']}/{run['session_id']}", {})
87
+ if any(isinstance(entry, dict) and entry.get('turn_key') == run['turn_key']
88
+ for entry in session.get('processed_turns', [])):
89
+ _reject('source_already_processed')
90
+ for key in processed.get('incremental_commits', {}):
91
+ work = load_work(processed, key)
92
+ if (work['source'] == run['source'] and work['session_id'] == run['session_id']
93
+ and work['turn_key'] == run['turn_key']
94
+ and work.get('request_kind', 'automatic') == 'automatic'):
95
+ _reject('source_already_owned')
96
+ for key in processed.get('incremental_runs', {}):
97
+ other = load_run(processed, key)
98
+ if (key != run_id and other['source'] == run['source']
99
+ and other['session_id'] == run['session_id'] and other['turn_key'] == run['turn_key']
100
+ and other['status'] not in TERMINAL):
101
+ _reject('source_already_owned')
102
+ try:
103
+ budget = _read_budget_state_unlocked(service.vault)
104
+ except ExtractionWorkStateError:
105
+ _reject('remaining_request_budget_unavailable')
106
+ work_budget = budget['works'].get(run['budget_id'], {})
107
+ row = work_budget.get('turns', {}).get(run['turn_budget_id'])
108
+ if (not row or work_budget.get('retired') or row['completed'] or run.get('budget_finalized')
109
+ or row['requests'] != 1 or row['request_limit_at_creation'] <= row['requests']):
110
+ _reject('remaining_request_budget_unavailable')
111
+ snapshot = _prepare_incremental_unlocked(service, **run['arguments'])
112
+ request = {'system': INCREMENTAL_SYSTEM,
113
+ 'user': json.dumps(snapshot.model_input(), ensure_ascii=False, separators=(',', ':'))}
114
+ if sum(len(s.encode('utf-8')) for s in request.values()) + len(RETRY_SYSTEM.encode('utf-8')) > MAX_BYTES:
115
+ _reject('blocked_context')
116
+ # Preview has no advisory lock. Confirm the independently read planning
117
+ # state/budget stayed stable; apply repeats this inside the existing lock.
118
+ if (processed != _read_processed(service.vault.processed_state_path)
119
+ or budget != _read_budget_state_unlocked(service.vault)):
120
+ _reject('recovery_preview_changed')
121
+ revision = digest({'processed': processed, 'budget': budget, 'source_window': window,
122
+ 'snapshot': snapshot.snapshot_id, 'protocol': _protocol_digest(),
123
+ 'allow_legacy_http': allow_legacy_http, 'run_id': run_id})
124
+ result = {'run_id': run_id, 'recoverable': True, 'read_only': True, 'model_calls': 0,
125
+ 'expected_revision': revision, 'remaining_requests': 1,
126
+ 'http_status': status, 'legacy_http_status_unknown': legacy,
127
+ 'source_unchanged': True, 'source_window_changed': window != run['source_window'],
128
+ 'snapshot_changed': snapshot.snapshot_id != run['snapshot_id'],
129
+ 'new_events': sum(e['use'] == 'new' for e in snapshot.state()['evidence']),
130
+ 'context_events': sum(e['use'] == 'context' for e in snapshot.state()['evidence'])}
131
+ return processed, run, snapshot, window, request, result
132
+
133
+
134
+ def recover_failed_run(service: Any, run_id: str, *, dry_run: bool = True,
135
+ expected_revision: str | None = None, allow_legacy_http: bool = False,
136
+ model: Any = None, router: Any = None) -> dict[str, Any]:
137
+ if type(dry_run) is not bool or type(allow_legacy_http) is not bool:
138
+ _reject('invalid_recovery_options')
139
+ if dry_run:
140
+ try:
141
+ return _plan(service, run_id, allow_legacy_http=allow_legacy_http)[-1]
142
+ except (ValueError, OSError, ExtractionWorkStateError) as error:
143
+ # Known local validators emit reason codes, never source bodies.
144
+ code = error.code if isinstance(error, FailedRecoveryError) else 'recovery_preflight_failed'
145
+ return {'run_id': run_id, 'recoverable': False, 'read_only': True,
146
+ 'model_calls': 0, 'code': code}
147
+ if not isinstance(expected_revision, str) or re.fullmatch(r'[0-9a-f]{64}', expected_revision) is None:
148
+ _reject('expected_recovery_revision_required')
149
+ from .incremental_runtime import _resolve
150
+ # A repeated apply is the same original run, not another authorization.
151
+ with service.vault.lock():
152
+ stored = load_run(_read_processed(service.vault.processed_state_path), run_id)
153
+ audit = stored.get('terminal_recovery') if stored else None
154
+ if audit is not None:
155
+ if audit['expected_revision'] != expected_revision or audit['allow_legacy_http'] != allow_legacy_http:
156
+ _reject('recovery_already_bound')
157
+ if stored['status'] in TERMINAL:
158
+ return public_result(stored)
159
+ # Active/resumable same-intent work uses the existing owner guard.
160
+ already_bound = True
161
+ else:
162
+ already_bound = False
163
+ if not already_bound:
164
+ processed, run, snapshot, window, request, preview = _plan(
165
+ service, run_id, allow_legacy_http=allow_legacy_http)
166
+ if preview['expected_revision'] != expected_revision:
167
+ _reject('stale_recovery_preview')
168
+ backend = _resolve(service, model, router)
169
+ audit = {'version': 1, 'expected_revision': expected_revision,
170
+ 'allow_legacy_http': allow_legacy_http, 'previous_code': run['code'],
171
+ 'previous_snapshot_id': run['snapshot_id'], 'previous_source_window': run['source_window'],
172
+ 'requested_at': utc_now()}
173
+ run.update(status='ready', code=None, snapshot_id=snapshot.snapshot_id, source_window=window,
174
+ request=request, request_digest=digest(request), protocol_digest=_protocol_digest(),
175
+ source_keys=[e['event_key'] for e in snapshot.state()['evidence']],
176
+ target_ids=[t['memory']['memory_id'] for t in snapshot.state()['targets'].values()],
177
+ terminal_recovery=audit)
178
+ save_run(service, processed, run)
179
+ if already_bound:
180
+ # Resolve only if a dispatch is still needed. Saved responses/commits
181
+ # remain recoverable with zero model calls and no API configuration.
182
+ backend = (_resolve(service, model, router) if stored['status'] in {'ready', 'retryable', 'dispatching'}
183
+ else None)
184
+ return resume_incremental_run(service, run_id, backend=backend)