memleaf 0.2.44__tar.gz → 0.2.46__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 (115) hide show
  1. {memleaf-0.2.44 → memleaf-0.2.46}/CHANGELOG.md +16 -0
  2. {memleaf-0.2.44/src/memleaf.egg-info → memleaf-0.2.46}/PKG-INFO +2 -2
  3. {memleaf-0.2.44 → memleaf-0.2.46}/README.en.md +1 -1
  4. {memleaf-0.2.44 → memleaf-0.2.46}/README.md +1 -1
  5. {memleaf-0.2.44 → memleaf-0.2.46}/pyproject.toml +1 -1
  6. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/__init__.py +1 -1
  7. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/cli.py +29 -3
  8. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/config.py +55 -0
  9. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/extraction_budget.py +14 -3
  10. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/extraction_capability.py +9 -9
  11. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/hermes_provider/plugin.yaml +1 -1
  12. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/installer.py +34 -12
  13. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/llm/base.py +5 -2
  14. memleaf-0.2.46/src/memleaf/llm/openai_compatible.py +312 -0
  15. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/llm/router.py +18 -11
  16. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/llm/thinking.py +19 -8
  17. memleaf-0.2.46/src/memleaf/model_capabilities.py +199 -0
  18. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/model_discovery.py +31 -16
  19. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/model_execution.py +60 -1
  20. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/process_common.py +2 -2
  21. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/single_pass_memory_planner.py +10 -1
  22. memleaf-0.2.46/src/memleaf/single_pass_plan.py +1063 -0
  23. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/validation.py +115 -0
  24. {memleaf-0.2.44 → memleaf-0.2.46/src/memleaf.egg-info}/PKG-INFO +2 -2
  25. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf.egg-info/SOURCES.txt +1 -0
  26. memleaf-0.2.44/src/memleaf/llm/openai_compatible.py +0 -165
  27. memleaf-0.2.44/src/memleaf/single_pass_plan.py +0 -536
  28. {memleaf-0.2.44 → memleaf-0.2.46}/LICENSE +0 -0
  29. {memleaf-0.2.44 → memleaf-0.2.46}/MANIFEST.in +0 -0
  30. {memleaf-0.2.44 → memleaf-0.2.46}/docs/capture-budget-design.md +0 -0
  31. {memleaf-0.2.44 → memleaf-0.2.46}/docs/config-migrations.md +0 -0
  32. {memleaf-0.2.44 → memleaf-0.2.46}/docs/core-refactor.md +0 -0
  33. {memleaf-0.2.44 → memleaf-0.2.46}/docs/evidence-retention.md +0 -0
  34. {memleaf-0.2.44 → memleaf-0.2.46}/docs/extraction-latency.md +0 -0
  35. {memleaf-0.2.44 → memleaf-0.2.46}/docs/gate-evidence-boundary.md +0 -0
  36. {memleaf-0.2.44 → memleaf-0.2.46}/docs/general-processing.md +0 -0
  37. {memleaf-0.2.44 → memleaf-0.2.46}/docs/hermes-mcp-runtime.md +0 -0
  38. {memleaf-0.2.44 → memleaf-0.2.46}/docs/processing-quality-acceptance.md +0 -0
  39. {memleaf-0.2.44 → memleaf-0.2.46}/docs/v0.2.26-processing-status.md +0 -0
  40. {memleaf-0.2.44 → memleaf-0.2.46}/examples/README.md +0 -0
  41. {memleaf-0.2.44 → memleaf-0.2.46}/examples/basic_usage.py +0 -0
  42. {memleaf-0.2.44 → memleaf-0.2.46}/examples/mcp_stdio.ndjson +0 -0
  43. {memleaf-0.2.44 → memleaf-0.2.46}/install.ps1 +0 -0
  44. {memleaf-0.2.44 → memleaf-0.2.46}/install.sh +0 -0
  45. {memleaf-0.2.44 → memleaf-0.2.46}/setup.cfg +0 -0
  46. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/__main__.py +0 -0
  47. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/adapters/__init__.py +0 -0
  48. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/adapters/antigravity.py +0 -0
  49. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/adapters/base.py +0 -0
  50. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/adapters/codex.py +0 -0
  51. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/adapters/hermes.py +0 -0
  52. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/admission.py +0 -0
  53. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/batch_review.py +0 -0
  54. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/budget.py +0 -0
  55. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/capture.py +0 -0
  56. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/compaction.py +0 -0
  57. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/create_coordinator.py +0 -0
  58. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/credentials.py +0 -0
  59. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/evidence_budget.py +0 -0
  60. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/evidence_policy.py +0 -0
  61. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/evidence_structure.py +0 -0
  62. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/evidence_syntax.py +0 -0
  63. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/extraction_work_state.py +0 -0
  64. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/frontmatter.py +0 -0
  65. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/hermes_provider/README.md +0 -0
  66. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/hermes_provider/__init__.py +0 -0
  67. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/hermes_provider/_mcp_client.py +0 -0
  68. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/hermes_provider/_provider.py +0 -0
  69. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/hermes_provider/_shared.py +0 -0
  70. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/hermes_provider/evidence_budget.py +0 -0
  71. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/hermes_runtime.py +0 -0
  72. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/host_events.py +0 -0
  73. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/host_runtime.py +0 -0
  74. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/inbox.py +0 -0
  75. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/index.py +0 -0
  76. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/inspection.py +0 -0
  77. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/llm/__init__.py +0 -0
  78. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/llm/claude_compatible.py +0 -0
  79. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/llm/gemini.py +0 -0
  80. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/locking.py +0 -0
  81. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/mcp_server.py +0 -0
  82. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/memory_commit.py +0 -0
  83. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/memory_planner.py +0 -0
  84. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/memory_writer.py +0 -0
  85. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/models.py +0 -0
  86. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/native_index.py +0 -0
  87. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/native_registration.py +0 -0
  88. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/parallel_model.py +0 -0
  89. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/planning_context.py +0 -0
  90. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/process_jobs.py +0 -0
  91. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/process_journal.py +0 -0
  92. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/process_owner.py +0 -0
  93. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/processing.py +0 -0
  94. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/prompts.py +0 -0
  95. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/provenance.py +0 -0
  96. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/recording_policy.py +0 -0
  97. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/redaction.py +0 -0
  98. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/retention.py +0 -0
  99. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/retrieval.py +0 -0
  100. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/retrieval_gate.py +0 -0
  101. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/scope_maintenance.py +0 -0
  102. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/scope_state.py +0 -0
  103. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/service.py +0 -0
  104. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/source_policy.py +0 -0
  105. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/state_layout.py +0 -0
  106. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/summary_batch.py +0 -0
  107. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/target_reconciliation.py +0 -0
  108. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/turn_audit.py +0 -0
  109. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/turn_plan.py +0 -0
  110. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/update_coordinator.py +0 -0
  111. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/update_review.py +0 -0
  112. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf/vault.py +0 -0
  113. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf.egg-info/dependency_links.txt +0 -0
  114. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf.egg-info/entry_points.txt +0 -0
  115. {memleaf-0.2.44 → memleaf-0.2.46}/src/memleaf.egg-info/top_level.txt +0 -0
@@ -2,6 +2,22 @@
2
2
 
3
3
  All notable changes to memleaf are documented here.
4
4
 
5
+ ## 0.2.46 — 2026-09-12
6
+
7
+ - Restore the host protocol literals that v0.2.45 dropped from the API-route protocol table. `anthropic_messages`, `chat_completions`, `chat`, `google` and `generate_content` resolve again. v0.2.45 replaced the previous tolerant mapping with an exact table that omitted every canonical Hermes `api_mode` value, so a Hermes custom provider that used to resolve to a protocol began resolving to none and was reported as `missing provider, base URL, protocol, or model`. Substring and model-name matching stay removed: only exact host-contract literals are accepted, and an unknown protocol still fails closed.
8
+ - Stop `memleaf install --host hermes` from aborting when model-route discovery finds nothing. Installation now continues and reports `processing_status: model_route_required` with an explicit user action, matching the Codex installer policy. Previously the install returned `stage: model_route` before touching the provider, which left the core package upgraded while the Hermes provider stayed on its previous version. The provider and MCP surface do not need a model route for capture or retrieval, so a missing route is a deferred configuration step rather than an installation failure.
9
+ - Add `install --no-model-discovery` so an upgrade can skip host model discovery while still preserving an existing memleaf route, matching the `init` flag that already existed. Discovery remains enabled by default, so an installation that already succeeds is unaffected.
10
+ - Verification for this release used synthetic Hermes-shaped routes, a temporary Vault and mocked host calls only: 18 focused tests cover the restored protocol literals, rejection of still-unknown protocols, discovery of a Hermes-shaped custom provider, the degraded-install decision, the new CLI flag, and an end-to-end assertion that `install_hermes` walks past a failed model route. No real provider call was made and no production Vault, Hermes configuration or session was read or written. A real Windows Hermes install and a real macOS Hermes install remain unverified by this release, and the manual model-route configuration step is still required on a host whose credentials the host CLI only reports masked.
11
+
12
+ ## 0.2.45 — 2026-09-12
13
+
14
+ - Repair the `b3-single-pass-v1` automatic extraction contract so model output is constrained by the same CREATE / UPDATE / NO_CHANGE / DEFERRED fields, evidence shapes, no-memory/defer reasons, target rules and lookup-completeness checks that Core validates. Pure ASCII decision-case differences are normalized locally; unknown decisions and missing semantic fields still fail closed.
15
+ - Replace the generic second full-prompt retry with one B3-specific structural repair. The repair receives only the previous untrusted B3 object, the compact protocol and allowlisted structural diagnostics; Core rejects candidate count/order changes, evidence/target/scope/memory drift, or any edit outside the explicitly repairable protocol redundancy. Automatic extraction remains limited to at most two actual model requests and never fabricates a terminal disposition.
16
+ - Separate credential/profile name (`provider`), wire protocol (`protocol`) and capability identity (`provider_family`). DeepSeek capabilities are resolved only from explicit configuration, exact legacy provider IDs, or exact official hostnames; custom aliases, model-name substrings and lookalike domains no longer grant provider-specific JSON/thinking behavior or override an explicit protocol. Unknown OpenAI-compatible services fail closed for single-pass capability.
17
+ - Make DeepSeek single-pass primary and repair requests send explicit `thinking: {type: disabled}` and `response_format: {type: json_object}` from the resolved capability profile, with independently configurable bounded output ceilings. Finish-reason truncation is rejected before commit, repair output is never truncated to fit a budget, and transport success, parser rejection and commit acceptance remain separate metrics.
18
+ - Refine thinking telemetry to distinguish requested, applied and observed state, preserve missing usage as unknown, and report observed reasoning when provider usage or response content proves it. The HTTP transport base no longer grants `single_pass_safe` by inheritance; B3 protocol compatibility and bounded one-request transport semantics are evaluated separately.
19
+ - Validation for this release used the exact v0.2.44 source artifact at `c13c6dedacfdf6206fd8ebdaa04c4d84fdc0b919`: 88 focused tests passed, including temporary-Vault atomicity/idempotency/revision-conflict boundaries, provider request serialization, B3 repair fidelity, request-count limits and the advisory ten-second latency rule. No real provider call or production-session replay was used, so real-model first-pass success rate, repair success rate, reasoning reduction and latency improvement remain unmeasured.
20
+
5
21
  ## 0.2.44 — 2026-09-12
6
22
 
7
23
  - Correct the extraction latency policy: ten seconds is an advisory performance target, not a cancellation or commit deadline. Remove the fixed six-second primary cap and eight-second shared window; single-pass HTTP requests now honor `llm.request_timeout`, including the one bounded format repair.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memleaf
3
- Version: 0.2.44
3
+ Version: 0.2.46
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.44。**
26
+ > **版本:0.2.46。**
27
27
  > 记忆只从用户与 Agent 的可见对话提炼。Agent 已在回复中整理的事实、项目进展和明确待办可作为来源;邮件、附件、网页和终端等工具原文不进入记忆提炼。模型负责保留原话中的不确定性,现有记忆仅用于比较、去重和更新。Markdown 仍是唯一事实源。
28
28
  > **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
29
29
 
@@ -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.44.**
7
+ > **Version: 0.2.46.**
8
8
  > Automatic extraction now uses only the current turn's visible user input and final assistant reply. Raw tool output, attachments, web/file/terminal payloads and legacy tool-evidence bodies are not new source evidence; existing or retrieved memory remains comparison context rather than source authority. Background processing is persisted as a local job and can be checked through the read-only `process_status` MCP tool; failed work remains retryable and fail closed. The release also tightens source/date grounding, target reconciliation, duplicate/no-op handling and semantic review before writes. Markdown remains the sole source of truth with no SQLite runtime dependency. Acceptance covers deterministic regression suites and synthetic inputs; it does not claim real-mail or customer-business acceptance.
9
9
  > **The current release supports Hermes and Codex.** Antigravity is not detected, installed, or configured.
10
10
 
@@ -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.44。**
7
+ > **版本:0.2.46。**
8
8
  > 记忆只从用户与 Agent 的可见对话提炼。Agent 已在回复中整理的事实、项目进展和明确待办可作为来源;邮件、附件、网页和终端等工具原文不进入记忆提炼。模型负责保留原话中的不确定性,现有记忆仅用于比较、去重和更新。Markdown 仍是唯一事实源。
9
9
  > **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
10
10
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memleaf"
7
- version = "0.2.44"
7
+ version = "0.2.46"
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.44"
3
+ __version__ = "0.2.46"
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
@@ -22,6 +22,7 @@ from .adapters.base import (
22
22
  from .adapters.hermes import HermesAdapter
23
23
  from .credentials import credential_text
24
24
  from .config import load_config
25
+ from .model_capabilities import resolve_provider_capabilities
25
26
  from .model_discovery import ModelCandidate, discover_models, manual_candidate, write_model_config
26
27
  from .vault import Vault
27
28
 
@@ -76,6 +77,14 @@ def build_parser() -> argparse.ArgumentParser:
76
77
  "this installation (current), or retain a version-matched configured runtime (existing)"
77
78
  ),
78
79
  )
80
+ install.add_argument(
81
+ "--no-model-discovery",
82
+ action="store_true",
83
+ help=(
84
+ "Hermes only: skip host model discovery; an existing memleaf route is "
85
+ "still preserved and the install continues either way"
86
+ ),
87
+ )
79
88
  install.add_argument("--json", action="store_true", help="emit one JSON result")
80
89
  audit = commands.add_parser("audit", help="inspect an existing Vault without changing it")
81
90
  audit.add_argument("--vault", type=Path, default=None)
@@ -231,6 +240,8 @@ def main(argv: Sequence[str] | None = None) -> int:
231
240
  install_kwargs = {"vault_path": args.vault}
232
241
  if args.mcp_runtime != "auto":
233
242
  install_kwargs["mcp_runtime"] = args.mcp_runtime
243
+ if args.no_model_discovery:
244
+ install_kwargs["skip_model_discovery"] = True
234
245
  output = install_hermes(**install_kwargs)
235
246
  elif args.command in {"audit", "process"}:
236
247
  from .inspection import audit_vault, existing_root, preview_process
@@ -301,8 +312,12 @@ def main(argv: Sequence[str] | None = None) -> int:
301
312
  print("Restart Hermes to use memleaf.")
302
313
  elif output.get("user_action_required"):
303
314
  print(f"Codex action required: {output.get('user_action')}")
304
- if output.get("model", {}).get("status") == "not_configured":
305
- print("Configure a memleaf model route before using automatic processing.")
315
+ if output.get("processing_status") == "model_route_required":
316
+ print(
317
+ "Automatic memory extraction needs a memleaf model route before it can run. "
318
+ "Set llm.provider/family/protocol/base_url/model/key in the Vault config.yaml, "
319
+ "or rerun `memleaf init` from an interactive terminal."
320
+ )
306
321
  else:
307
322
  _print_install_failure(output, host=args.host)
308
323
  return 0 if output.get("status") in {"configured", "already_configured"} else 2
@@ -531,6 +546,11 @@ def _existing_memleaf_route(path: Path) -> ModelCandidate | None:
531
546
  if key is None:
532
547
  return None
533
548
  try:
549
+ capabilities = resolve_provider_capabilities(
550
+ provider=llm.get("provider"),
551
+ provider_family=llm.get("provider_family"),
552
+ base_url=llm.get("base_url"),
553
+ )
534
554
  return ModelCandidate(
535
555
  source="memleaf",
536
556
  provider=str(llm.get("provider", "")),
@@ -540,6 +560,8 @@ def _existing_memleaf_route(path: Path) -> ModelCandidate | None:
540
560
  api_key=key,
541
561
  context_window=int(llm.get("context_window", 200000)),
542
562
  source_detail="existing memleaf route",
563
+ provider_family=capabilities.provider_family,
564
+ provider_family_source=capabilities.source,
543
565
  )
544
566
  except (TypeError, ValueError):
545
567
  return None
@@ -547,13 +569,17 @@ def _existing_memleaf_route(path: Path) -> ModelCandidate | None:
547
569
 
548
570
  def _prompt_for_model() -> ModelCandidate:
549
571
  print("memleaf: no complete callable chat model was found; configure one for processing.", file=sys.stderr)
550
- provider = input("Provider (openai/claude/gemini): ").strip()
572
+ provider = input("Provider / credential profile name: ").strip()
573
+ provider_family = input(
574
+ "Provider family (openai/deepseek/anthropic/gemini/generic, blank=auto): "
575
+ ).strip()
551
576
  protocol = input("Protocol (openai/claude/gemini): ").strip()
552
577
  base_url = input("Base URL: ").strip()
553
578
  model = input("Model: ").strip()
554
579
  api_key = getpass.getpass("API key (input hidden): ")
555
580
  return manual_candidate(
556
581
  provider=provider,
582
+ provider_family=provider_family,
557
583
  protocol=protocol,
558
584
  base_url=base_url,
559
585
  model=model,
@@ -9,6 +9,9 @@ from typing import Any, Mapping
9
9
 
10
10
  from .frontmatter import FrontmatterError, dump_yaml, load_yaml
11
11
  from .locking import atomic_write_text
12
+ from .model_capabilities import (
13
+ legacy_protocol_for_provider, normalize_protocol, normalize_provider_family,
14
+ )
12
15
  from .native_index import NativeConfigError, validate_native_sources
13
16
  from .scope_state import ScopeError, validate_scope_registry
14
17
 
@@ -19,6 +22,10 @@ MAX_REQUEST_TIMEOUT = 240
19
22
  DEFAULT_MODEL_CONCURRENCY = 3
20
23
  MIN_MODEL_CONCURRENCY = 1
21
24
  MAX_MODEL_CONCURRENCY = 8
25
+ DEFAULT_SINGLE_PASS_PRIMARY_MAX_TOKENS = 8192
26
+ DEFAULT_SINGLE_PASS_REPAIR_MAX_TOKENS = 8192
27
+ MIN_SINGLE_PASS_MAX_TOKENS = 2048
28
+ MAX_SINGLE_PASS_MAX_TOKENS = 32768
22
29
  THINKING_PURPOSES = ("gate", "summarize", "compact", "single_pass")
23
30
  THINKING_MODES = frozenset({"default", "disabled", "low", "high", "max"})
24
31
  DEFAULT_THINKING = {purpose: "low" for purpose in THINKING_PURPOSES}
@@ -40,6 +47,16 @@ def _normalize_request_timeout(value: Any) -> int | float:
40
47
  return int(parsed) if parsed.is_integer() else parsed
41
48
 
42
49
 
50
+ def _normalize_single_pass_token_limit(value: Any, *, field: str, default: int) -> int:
51
+ if value is None or value == "":
52
+ return default
53
+ if isinstance(value, bool) or not isinstance(value, int):
54
+ raise ValueError(f"invalid memleaf llm.{field}")
55
+ if not MIN_SINGLE_PASS_MAX_TOKENS <= value <= MAX_SINGLE_PASS_MAX_TOKENS:
56
+ raise ValueError(f"invalid memleaf llm.{field}")
57
+ return value
58
+
59
+
43
60
  def _normalize_model_concurrency(value: Any) -> int:
44
61
  if isinstance(value, bool) or not isinstance(value, int):
45
62
  raise ValueError("invalid memleaf process.model_concurrency")
@@ -89,6 +106,7 @@ DEFAULT_CONFIG: dict[str, Any] = {
89
106
  "llm": {
90
107
  "mode": "auto",
91
108
  "provider": "",
109
+ "provider_family": "",
92
110
  "protocol": "openai",
93
111
  "base_url": "",
94
112
  "api_key": "",
@@ -96,6 +114,8 @@ DEFAULT_CONFIG: dict[str, Any] = {
96
114
  "model": "",
97
115
  "context_window": 200000,
98
116
  "request_timeout": DEFAULT_REQUEST_TIMEOUT,
117
+ "single_pass_primary_max_tokens": DEFAULT_SINGLE_PASS_PRIMARY_MAX_TOKENS,
118
+ "single_pass_repair_max_tokens": DEFAULT_SINGLE_PASS_REPAIR_MAX_TOKENS,
99
119
  "diagnostic_logging": False,
100
120
  "thinking": dict(DEFAULT_THINKING),
101
121
  },
@@ -148,6 +168,19 @@ def _normalize_legacy_config(value: Mapping[str, Any]) -> dict[str, Any]:
148
168
  # the current default restore normal bounded evidence capture.
149
169
  current["tool_evidence_mode"] = "off"
150
170
  normalized["capture"] = current
171
+ llm = normalized.get("llm")
172
+ if llm is not None and not isinstance(llm, Mapping):
173
+ raise ValueError("invalid memleaf llm settings")
174
+ if isinstance(llm, Mapping):
175
+ current_llm = dict(llm)
176
+ # Old configs often omitted protocol and relied on the provider name.
177
+ # Preserve only exact built-in identifiers; custom aliases never
178
+ # override an explicitly configured protocol.
179
+ if not isinstance(current_llm.get("protocol"), str) or not current_llm.get("protocol", "").strip():
180
+ legacy_protocol = legacy_protocol_for_provider(current_llm.get("provider"))
181
+ if legacy_protocol is not None:
182
+ current_llm["protocol"] = legacy_protocol
183
+ normalized["llm"] = current_llm
151
184
  return normalized
152
185
 
153
186
 
@@ -210,7 +243,17 @@ def load_config(path: Path | str, *, vault: Path | str | None = None) -> dict[st
210
243
  if not isinstance(llm, Mapping):
211
244
  raise ValueError("invalid memleaf llm settings")
212
245
  llm = dict(llm)
246
+ llm["provider_family"] = normalize_provider_family(llm.get("provider_family"))
247
+ llm["protocol"] = normalize_protocol(llm.get("protocol"), provider=llm.get("provider"))
213
248
  llm["request_timeout"] = _normalize_request_timeout(llm.get("request_timeout", DEFAULT_REQUEST_TIMEOUT))
249
+ llm["single_pass_primary_max_tokens"] = _normalize_single_pass_token_limit(
250
+ llm.get("single_pass_primary_max_tokens"),
251
+ field="single_pass_primary_max_tokens", default=DEFAULT_SINGLE_PASS_PRIMARY_MAX_TOKENS,
252
+ )
253
+ llm["single_pass_repair_max_tokens"] = _normalize_single_pass_token_limit(
254
+ llm.get("single_pass_repair_max_tokens"),
255
+ field="single_pass_repair_max_tokens", default=DEFAULT_SINGLE_PASS_REPAIR_MAX_TOKENS,
256
+ )
214
257
  llm["thinking"] = _normalize_thinking_settings(llm.get("thinking"))
215
258
  if type(llm.get("diagnostic_logging", False)) is not bool:
216
259
  raise ValueError("invalid memleaf llm.diagnostic_logging")
@@ -252,9 +295,21 @@ def save_config(path: Path | str, config: Mapping[str, Any]) -> None:
252
295
  if not isinstance(llm, Mapping):
253
296
  raise ValueError("invalid memleaf llm settings")
254
297
  normalized_llm = dict(llm)
298
+ normalized_llm["provider_family"] = normalize_provider_family(normalized_llm.get("provider_family"))
299
+ normalized_llm["protocol"] = normalize_protocol(
300
+ normalized_llm.get("protocol"), provider=normalized_llm.get("provider")
301
+ )
255
302
  normalized_llm["request_timeout"] = _normalize_request_timeout(
256
303
  normalized_llm.get("request_timeout", DEFAULT_REQUEST_TIMEOUT)
257
304
  )
305
+ normalized_llm["single_pass_primary_max_tokens"] = _normalize_single_pass_token_limit(
306
+ normalized_llm.get("single_pass_primary_max_tokens"),
307
+ field="single_pass_primary_max_tokens", default=DEFAULT_SINGLE_PASS_PRIMARY_MAX_TOKENS,
308
+ )
309
+ normalized_llm["single_pass_repair_max_tokens"] = _normalize_single_pass_token_limit(
310
+ normalized_llm.get("single_pass_repair_max_tokens"),
311
+ field="single_pass_repair_max_tokens", default=DEFAULT_SINGLE_PASS_REPAIR_MAX_TOKENS,
312
+ )
258
313
  normalized_llm["thinking"] = _normalize_thinking_settings(normalized_llm.get("thinking"))
259
314
  diagnostic_logging = normalized_llm.get("diagnostic_logging", False)
260
315
  if type(diagnostic_logging) is not bool:
@@ -26,8 +26,6 @@ class SinglePassBudgetBackend:
26
26
  its repair overrides the transport's configured ``llm.request_timeout``.
27
27
  """
28
28
 
29
- single_pass_safe = True
30
-
31
29
  def __init__(
32
30
  self,
33
31
  backend: Any,
@@ -50,6 +48,18 @@ class SinglePassBudgetBackend:
50
48
  def model(self) -> str:
51
49
  return str(getattr(self._backend, "model", "unknown"))
52
50
 
51
+ @property
52
+ def single_pass_safe(self) -> bool:
53
+ """Preserve the underlying adapter/route request-boundary guarantee."""
54
+
55
+ return getattr(self._backend, "single_pass_safe", False) is True
56
+
57
+ @property
58
+ def single_pass_protocol(self) -> bool:
59
+ """Preserve protocol identity while this wrapper enforces request count."""
60
+
61
+ return getattr(self._backend, "single_pass_protocol", False) is True
62
+
53
63
  @property
54
64
  def parallel_safe(self) -> bool:
55
65
  return getattr(self._backend, "parallel_safe", False) is True
@@ -144,6 +154,7 @@ class ExtractionTiming:
144
154
  "failed_turn_count": int(failed),
145
155
  "over_target_turn_count": int(seconds > TARGET_TOTAL_SECONDS),
146
156
  "successful_within_target_count": int(not failed and seconds <= TARGET_TOTAL_SECONDS),
157
+ "commit_accepted_count": int(not failed and commit_started is not None),
147
158
  "total_duration_ms": int(seconds * 1000),
148
159
  "max_turn_duration_ms": int(seconds * 1000),
149
160
  "planning_duration_ms": int(max(0.0, planning_end - self._started) * 1000),
@@ -156,7 +167,7 @@ def aggregate_extraction_metrics(values: Iterable[Mapping[str, Any]]) -> dict[st
156
167
 
157
168
  summed = (
158
169
  "turn_count", "failed_turn_count", "over_target_turn_count",
159
- "successful_within_target_count", "total_duration_ms",
170
+ "successful_within_target_count", "commit_accepted_count", "total_duration_ms",
160
171
  "planning_duration_ms", "commit_duration_ms",
161
172
  )
162
173
  result = {key: 0 for key in summed}
@@ -32,20 +32,20 @@ def _direct_protocol_capable(backend: Any) -> bool:
32
32
 
33
33
  if backend is None:
34
34
  return False
35
- if getattr(backend, "single_pass_safe", False) is True:
36
- return True
37
- declared = getattr(backend, "single_pass_protocol", None)
38
- if isinstance(declared, bool):
39
- return declared
35
+ # Raw Python callbacks are adapted by ModelExecutor into CallableBackend.
36
+ # Preserve their established prompt-level B3 compatibility without
37
+ # claiming any transport boundary inside caller-owned code. An explicit
38
+ # callback override can still opt out.
40
39
  if isinstance(backend, CallableBackend):
41
40
  override = _callable_protocol_override(backend)
42
41
  if override is not None:
43
42
  return override
44
- # Raw Python callbacks are adapted by ModelExecutor into
45
- # CallableBackend. They can consume memleaf's B3 prompt, but are not
46
- # transport-timeout safe because caller-owned code may ignore
47
- # timeout/cancellation entirely.
48
43
  return True
44
+ # Protocol support is independent of request-boundary safety. A backend
45
+ # must not gain B3 merely because complete() happens to be bounded.
46
+ declared = getattr(backend, "single_pass_protocol", None)
47
+ if isinstance(declared, bool):
48
+ return declared
49
49
  return False
50
50
 
51
51
 
@@ -1,5 +1,5 @@
1
1
  name: memleaf
2
- version: 0.2.44
2
+ version: 0.2.46
3
3
  description: "Hermes-native external MemoryProvider for local-first Markdown memory shared with the memleaf MCP server."
4
4
  hooks:
5
5
  - prefetch
@@ -15,7 +15,7 @@ import subprocess
15
15
  import sys
16
16
  import sysconfig
17
17
  import tempfile
18
- from typing import Any
18
+ from typing import Any, Mapping
19
19
 
20
20
  from . import __version__
21
21
  from .adapters.base import (
@@ -754,10 +754,33 @@ def _failure_result(
754
754
  return result
755
755
 
756
756
 
757
+ def _model_route_outcome(model: Any) -> tuple[bool, str, str | None]:
758
+ """Decide how one host installation reports its model route.
759
+
760
+ A missing model route must never block installing the host integration.
761
+ The provider/MCP surface still serves capture and retrieval without one,
762
+ so installation continues and reports ``model_route_required`` until a
763
+ route is configured. This is the documented Codex policy, and applying it
764
+ to Hermes keeps the two hosts consistent instead of aborting the install
765
+ and leaving core and provider versions out of step.
766
+ """
767
+
768
+ if isinstance(model, Mapping) and model.get("status") in {"configured", "already_configured"}:
769
+ return True, "ready", None
770
+ return (
771
+ False,
772
+ "model_route_required",
773
+ "Configure an independent memleaf Model Route for this Vault before relying on "
774
+ "automatic memory extraction. Host model/provider settings are intentionally "
775
+ "not used or modified.",
776
+ )
777
+
778
+
757
779
  def install_hermes(
758
780
  *,
759
781
  vault_path: Path | None = None,
760
782
  mcp_runtime: str = "auto",
783
+ skip_model_discovery: bool = False,
761
784
  ) -> dict[str, Any]:
762
785
  """Install memleaf for Hermes with runtime preflight and host rollback.
763
786
 
@@ -870,18 +893,14 @@ def install_hermes(
870
893
  home=home,
871
894
  dry_run=False,
872
895
  non_interactive=not sys.stdin.isatty(),
873
- skip_discovery=False,
896
+ skip_discovery=skip_model_discovery,
874
897
  )
875
- if model.get("status") == "failure":
876
- return _failure_result(
877
- stage="model_route",
878
- reason="model route is not configured",
879
- core_version=core_version,
880
- vault=vault.root,
881
- vault_source=vault_source,
882
- model=model,
883
- mcp_runtime=runtime_details,
884
- )
898
+ # A missing route degrades the installation instead of aborting it: the
899
+ # Hermes provider and MCP surface do not need a model route, and stopping
900
+ # here used to leave core and provider versions out of step.
901
+ model_ready, processing_status, model_action = _model_route_outcome(model)
902
+ # ``model_ready`` is reported through ``processing_status`` below; the host
903
+ # integration itself is configured either way.
885
904
 
886
905
  provider_target = hermes_home / "plugins" / "memleaf"
887
906
  provider_config = hermes_home / "memleaf.json"
@@ -1079,6 +1098,9 @@ def install_hermes(
1079
1098
  "model": model,
1080
1099
  "native_sources": native_registration,
1081
1100
  "capture": capture_policy_status(vault.config()),
1101
+ "processing_status": processing_status,
1102
+ "user_action_required": bool(model_action),
1103
+ "user_action": model_action,
1082
1104
  }
1083
1105
 
1084
1106
 
@@ -146,6 +146,7 @@ class ModelBackend(Protocol):
146
146
  parallel_safe: bool
147
147
  structured_batch_safe: bool
148
148
  single_pass_safe: bool
149
+ single_pass_protocol: bool
149
150
 
150
151
  def complete(
151
152
  self,
@@ -167,7 +168,6 @@ class CallableBackend:
167
168
  parallel_safe = False
168
169
  structured_batch_safe = False
169
170
  single_pass_safe = False
170
-
171
171
  def __init__(self, callback: Callable[..., str], *, model: str = "host"):
172
172
  if not callable(callback):
173
173
  raise TypeError("model callback must be callable")
@@ -231,7 +231,10 @@ class HTTPModelBackend:
231
231
  provider = "api"
232
232
  parallel_safe = False
233
233
  structured_batch_safe = True
234
- single_pass_safe = True
234
+ # Protocol compatibility is provider-specific. Generic HTTP transports
235
+ # must not authorize B3 merely because one complete() maps to one request.
236
+ single_pass_safe = False
237
+ single_pass_protocol = False
235
238
 
236
239
  def __init__(
237
240
  self,