memleaf 0.2.49__tar.gz → 0.2.51__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 (114) hide show
  1. {memleaf-0.2.49 → memleaf-0.2.51}/CHANGELOG.md +16 -0
  2. {memleaf-0.2.49/src/memleaf.egg-info → memleaf-0.2.51}/PKG-INFO +2 -2
  3. {memleaf-0.2.49 → memleaf-0.2.51}/README.en.md +1 -1
  4. {memleaf-0.2.49 → memleaf-0.2.51}/README.md +1 -1
  5. {memleaf-0.2.49 → memleaf-0.2.51}/pyproject.toml +1 -1
  6. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/__init__.py +1 -1
  7. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/adapters/base.py +2 -0
  8. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/hermes_provider/plugin.yaml +1 -1
  9. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/installer.py +2 -0
  10. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/planning_context.py +20 -15
  11. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/process_common.py +23 -6
  12. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/process_jobs.py +5 -1
  13. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/single_pass_plan.py +31 -6
  14. memleaf-0.2.51/src/memleaf/subprocess_flags.py +43 -0
  15. {memleaf-0.2.49 → memleaf-0.2.51/src/memleaf.egg-info}/PKG-INFO +2 -2
  16. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf.egg-info/SOURCES.txt +1 -0
  17. {memleaf-0.2.49 → memleaf-0.2.51}/LICENSE +0 -0
  18. {memleaf-0.2.49 → memleaf-0.2.51}/MANIFEST.in +0 -0
  19. {memleaf-0.2.49 → memleaf-0.2.51}/docs/capture-budget-design.md +0 -0
  20. {memleaf-0.2.49 → memleaf-0.2.51}/docs/config-migrations.md +0 -0
  21. {memleaf-0.2.49 → memleaf-0.2.51}/docs/core-refactor.md +0 -0
  22. {memleaf-0.2.49 → memleaf-0.2.51}/docs/evidence-retention.md +0 -0
  23. {memleaf-0.2.49 → memleaf-0.2.51}/docs/extraction-latency.md +0 -0
  24. {memleaf-0.2.49 → memleaf-0.2.51}/docs/gate-evidence-boundary.md +0 -0
  25. {memleaf-0.2.49 → memleaf-0.2.51}/docs/general-processing.md +0 -0
  26. {memleaf-0.2.49 → memleaf-0.2.51}/docs/hermes-mcp-runtime.md +0 -0
  27. {memleaf-0.2.49 → memleaf-0.2.51}/docs/processing-quality-acceptance.md +0 -0
  28. {memleaf-0.2.49 → memleaf-0.2.51}/docs/v0.2.26-processing-status.md +0 -0
  29. {memleaf-0.2.49 → memleaf-0.2.51}/examples/README.md +0 -0
  30. {memleaf-0.2.49 → memleaf-0.2.51}/examples/basic_usage.py +0 -0
  31. {memleaf-0.2.49 → memleaf-0.2.51}/examples/mcp_stdio.ndjson +0 -0
  32. {memleaf-0.2.49 → memleaf-0.2.51}/install.ps1 +0 -0
  33. {memleaf-0.2.49 → memleaf-0.2.51}/install.sh +0 -0
  34. {memleaf-0.2.49 → memleaf-0.2.51}/setup.cfg +0 -0
  35. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/__main__.py +0 -0
  36. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/adapters/__init__.py +0 -0
  37. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/adapters/antigravity.py +0 -0
  38. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/adapters/codex.py +0 -0
  39. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/adapters/hermes.py +0 -0
  40. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/admission.py +0 -0
  41. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/batch_review.py +0 -0
  42. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/budget.py +0 -0
  43. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/capture.py +0 -0
  44. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/cli.py +0 -0
  45. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/compaction.py +0 -0
  46. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/config.py +0 -0
  47. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/create_coordinator.py +0 -0
  48. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/credentials.py +0 -0
  49. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/evidence_budget.py +0 -0
  50. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/evidence_policy.py +0 -0
  51. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/evidence_structure.py +0 -0
  52. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/evidence_syntax.py +0 -0
  53. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/extraction_budget.py +0 -0
  54. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/extraction_capability.py +0 -0
  55. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/extraction_work_state.py +0 -0
  56. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/frontmatter.py +0 -0
  57. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/hermes_provider/README.md +0 -0
  58. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/hermes_provider/__init__.py +0 -0
  59. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/hermes_provider/_mcp_client.py +0 -0
  60. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/hermes_provider/_provider.py +0 -0
  61. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/hermes_provider/_shared.py +0 -0
  62. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/hermes_provider/evidence_budget.py +0 -0
  63. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/hermes_runtime.py +0 -0
  64. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/host_events.py +0 -0
  65. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/host_runtime.py +0 -0
  66. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/inbox.py +0 -0
  67. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/index.py +0 -0
  68. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/inspection.py +0 -0
  69. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/llm/__init__.py +0 -0
  70. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/llm/base.py +0 -0
  71. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/llm/claude_compatible.py +0 -0
  72. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/llm/gemini.py +0 -0
  73. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/llm/openai_compatible.py +0 -0
  74. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/llm/router.py +0 -0
  75. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/llm/thinking.py +0 -0
  76. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/locking.py +0 -0
  77. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/mcp_server.py +0 -0
  78. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/memory_commit.py +0 -0
  79. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/memory_planner.py +0 -0
  80. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/memory_writer.py +0 -0
  81. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/model_capabilities.py +0 -0
  82. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/model_discovery.py +0 -0
  83. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/model_execution.py +0 -0
  84. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/models.py +0 -0
  85. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/native_index.py +0 -0
  86. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/native_registration.py +0 -0
  87. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/parallel_model.py +0 -0
  88. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/process_journal.py +0 -0
  89. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/process_owner.py +0 -0
  90. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/processing.py +0 -0
  91. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/prompts.py +0 -0
  92. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/provenance.py +0 -0
  93. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/recording_policy.py +0 -0
  94. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/redaction.py +0 -0
  95. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/retention.py +0 -0
  96. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/retrieval.py +0 -0
  97. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/retrieval_gate.py +0 -0
  98. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/scope_maintenance.py +0 -0
  99. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/scope_state.py +0 -0
  100. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/service.py +0 -0
  101. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/single_pass_memory_planner.py +0 -0
  102. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/source_policy.py +0 -0
  103. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/state_layout.py +0 -0
  104. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/summary_batch.py +0 -0
  105. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/target_reconciliation.py +0 -0
  106. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/turn_audit.py +0 -0
  107. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/turn_plan.py +0 -0
  108. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/update_coordinator.py +0 -0
  109. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/update_review.py +0 -0
  110. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/validation.py +0 -0
  111. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf/vault.py +0 -0
  112. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf.egg-info/dependency_links.txt +0 -0
  113. {memleaf-0.2.49 → memleaf-0.2.51}/src/memleaf.egg-info/entry_points.txt +0 -0
  114. {memleaf-0.2.49 → memleaf-0.2.51}/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.51 — 2026-09-13
6
+
7
+ - Publish the scope vocabulary in the B3 single-pass contract. The compact contract described `scopes` only as `nonempty string[]` and never named `project:<name>`, so a planner handed an empty `scope_registry` had exactly one value it could be sure was legal. A first message that plainly establishes a project -- "最近在弄个记账的小玩意儿,就扔家里那台 N100 上跑" -- was filed under `global`, producing the internally inconsistent `type=project` beside `scopes=["global"]`. The legacy summarize contract always carried this paragraph; B3 lost it, the same way it had lost the todo-field and date rules repaired in 0.2.48 and 0.2.50.
8
+ - State the rule as an ownership decision rather than a permission. The first wording ("use a project scope only when the evidence establishes that project") was read as being about *topics*, and the model kept choosing `global`. The shipped wording says that a candidate concerning one distinct project the user is working on -- a `type=project` memory, or the work, decisions, state or deadlines belonging to that project -- is scoped to that project as `project:<name>`, named as the evidence names it, and that `global` is for a fact no single project owns: a standing personal preference, a machine-wide or tool-wide rule, or an environment fact. Nothing had to be built for this: new scope names are already accepted unregistered, `project:记账` is a legal key, and the commit path registers newly observed scopes on write.
9
+ - Verification for this release used the real model and the real prompt builder. 102 tests pass with no model call, pinning the vocabulary, the directive phrasing and the `see SCOPES` reference from the compact contract. Against the live route, eight sentences were sent with an EMPTY memory library so the model had to CREATE and only the chosen scope was under test: three project-shaped sentences produced `project:记账`, `project:爬虫` and `project:NAS`, three global-shaped sentences produced `global`, a bare product mention ("我看了下 FastAPI 的文档") stayed `global` rather than claiming ownership, and someone else's project produced no memory at all. Two full passes agreed on all eight cases. Whether a longer or noisier turn keeps this discrimination is not measured, and the 23 memories already written as `global` are not re-scoped by this release.
10
+
11
+ ## 0.2.50 — 2026-09-13
12
+
13
+ - Remove the six-item cap on the related-memory catalog. It contradicted the retrieval design outright. Retrieval injects the scope list and resolves the scope the planner selects; a Vault whose memories all carry one scope -- `global`, which is the common personal case and the only scope in an empty registry -- correctly resolves that scope to the whole library, and six rows then hid most of it. Measured on a real Vault of 18 memories, every one `global`: retrieval returned 19 rows, the cap kept 6, discarded 13, and the discarded rows marked the lookup unprovable, so every turn died with `incomplete B3 lookup cannot authorize a terminal decision`. Size, not count, is what the prompt budget is about, and the character ceiling already expresses it.
14
+ - Derive that ceiling from the prompt budget instead of a round number, and downgrade it to a safety net. `_RELATED_MAX_CHARS` was 6000, which on the same Vault sat just below the whole library and so bound continuously; it is now 40000, chosen against `MAX_PROMPT_BYTES` (192 KiB) at the measured ~1.6 UTF-8 bytes per catalog row, which leaves well over 100 KiB for the contract, the current evidence and the native catalog and admits roughly 180 memories. It no longer expresses a working constraint, only the guarantee that the hard prompt limit cannot be reached.
15
+ - Stop treating a trimmed body as a withheld record. A body shortened to fit the catalog budget is still projected -- its id, title, type and scopes are all present, so it can still be targeted and still rules out a duplicate CREATE -- yet it was recorded as withheld and made `lookup_complete` false. Only a record actually dropped from the projection can carry that weight now, whether by the ceiling or because it could not be projected at all. The 1600-character per-body bound is unchanged: it guards against one pathological memory consuming the whole catalog, and at the measured distribution (bodies average 146 characters, p90 194) it does not bind in normal use.
16
+ - Defer the candidate that needs the missing proof instead of discarding the turn. Only a CREATE claims novelty, and only a complete lookup can prove it, so a CREATE under an incomplete lookup is now deferred with the protocol's own `lookup_incomplete` reason -- the vocabulary already declared it -- and recorded as `b3_candidate_deferred_count`. UPDATE and NO_CHANGE name a target that is in the supplied catalog by construction, so they remain provable and still commit. The turn-level failure that refused every terminal decision is gone.
17
+ - Keep the extraction worker, the installer's host CLI probes and the adapter runner from opening a console window. All three ran on Windows without `CREATE_NO_WINDOW`; the worker asked for `DETACHED_PROCESS`, which does not help, because a console program still needs a console and Windows creates a visible one. Whenever the host had no console of its own -- the Hermes desktop app, for instance -- every captured turn put a stray terminal on the user's desktop. Measured on Windows 11 with Windows Terminal as the default host: `CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS` opened one new console window titled with the interpreter path, `CREATE_NO_WINDOW` opened none. The worker keeps its own process group so a Ctrl+C aimed at the parent's console does not take it down.
18
+
19
+ Verification for this release used the reported production Vault. 100 tests pass with no model call, covering the item-count regression directly (an 18-memory and a 60-memory library must both stay complete), the trimmed-versus-withheld distinction, the still-fail-closed cases for a genuinely withheld local record and an unprojectable one, the incomplete-lookup deferral for CREATE, UPDATE and NO_CHANGE, the reconciler not treating a deferred member as a collision, and all three spawn sites. The real replay retried the four turns that had failed: the lookup reported `lookup_complete: true` with all 18 rows projected, two preview runs returned `execution_status: ok` with `coverage_status: complete`, the committed run processed all four previously failing turns and wrote six memories, and the Vault then reported `failed_sessions: 0`, `failed_turns: 0`, `retryable: false` with a clean audit. Compaction is still not wired to any automatic caller, so an active-memory ceiling remains absent in normal operation; that and the question of whether retrieval should select by relevance when several scopes exist are left open rather than assumed.
20
+
5
21
  ## 0.2.49 — 2026-09-13
6
22
 
7
23
  - Restore candidate-local deferral to the B3 single-pass path. v0.2.26 established that an invalid proposal receives bounded model correction and then explicit candidate-local deferral while "valid siblings proceed", and v0.2.34 required that "candidate-local deferral and idempotent partial retries" be preserved so unresolved ownership, target, evidence or timing never fabricates a write. The B3 planner had lost that property: the first proposal Core rejected raised out of the parser and discarded every other verdict in the turn, so one model slip cost the whole turn even when six other candidates were sound. A proposal that Core can attribute to a single candidate is now deferred with a B3 defer reason and the rest of the turn still commits. Nothing is written for a deferred candidate, no extra model call is made, and the turn stays retryable with its already-settled units intact.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memleaf
3
- Version: 0.2.49
3
+ Version: 0.2.51
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.49。**
26
+ > **版本:0.2.51。**
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.49.**
7
+ > **Version: 0.2.51.**
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.49。**
7
+ > **版本:0.2.51。**
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.49"
7
+ version = "0.2.51"
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.49"
3
+ __version__ = "0.2.51"
4
4
 
5
5
  from .config import DEFAULT_CONFIG, default_config, load_config, save_config
6
6
  from .frontmatter import FrontmatterError, dump_frontmatter, dump_yaml, load_yaml, parse_frontmatter
@@ -24,6 +24,7 @@ from pathlib import Path
24
24
  from typing import Any, Callable, Mapping, Sequence
25
25
 
26
26
  from ..locking import VaultLock, atomic_write_json, read_json
27
+ from ..subprocess_flags import hidden_popen_kwargs
27
28
 
28
29
 
29
30
  @dataclass(frozen=True)
@@ -453,6 +454,7 @@ def _subprocess_runner(
453
454
  "errors": "strict",
454
455
  "env": dict(env),
455
456
  }
457
+ options.update(hidden_popen_kwargs())
456
458
  if input_text is not None:
457
459
  options["input"] = input_text
458
460
  return subprocess.run(list(argv), **options)
@@ -1,5 +1,5 @@
1
1
  name: memleaf
2
- version: 0.2.49
2
+ version: 0.2.51
3
3
  description: "Hermes-native external MemoryProvider for local-first Markdown memory shared with the memleaf MCP server."
4
4
  hooks:
5
5
  - prefetch
@@ -42,6 +42,7 @@ from .hermes_runtime import (
42
42
  )
43
43
  from .locking import atomic_write_json
44
44
  from .native_registration import ensure_hermes_native_sources
45
+ from .subprocess_flags import hidden_popen_kwargs
45
46
  from .vault import Vault
46
47
 
47
48
 
@@ -357,6 +358,7 @@ def _run(
357
358
  "errors": "strict",
358
359
  "check": False,
359
360
  }
361
+ options.update(hidden_popen_kwargs())
360
362
  if timeout is not None:
361
363
  options["timeout"] = timeout
362
364
  return subprocess.run(command, **options)
@@ -11,7 +11,7 @@ from .native_index import NativeIndexer
11
11
  from .retrieval import candidate_matches_query, filter_by_scope, normalize_term
12
12
  from .scope_state import project_scopes_for_domains
13
13
  from .scope_maintenance import ScopeMaintenanceError, scope_registry_projection
14
- from .process_common import ProcessingError, _RELATED_MAX_BODY_CHARS, _RELATED_MAX_CHARS, _RELATED_MAX_ITEMS, _SCOPE_CORRECTION_MARKER_RE, _SCOPE_DIRECTORY_MAX_CHARS, _SCOPE_DIRECTORY_MAX_ITEMS, _SCOPE_DIRECTORY_MAX_TITLE_CHARS, _TARGET_NOT_RELATED, _TARGET_SAME_USE, _TARGET_UNKNOWN, _invoke_native, _merge_related, _native_result, _safe_scope_background, _session_key
14
+ from .process_common import ProcessingError, _RELATED_MAX_BODY_CHARS, _RELATED_MAX_CHARS, _SCOPE_CORRECTION_MARKER_RE, _SCOPE_DIRECTORY_MAX_CHARS, _SCOPE_DIRECTORY_MAX_ITEMS, _SCOPE_DIRECTORY_MAX_TITLE_CHARS, _TARGET_NOT_RELATED, _TARGET_SAME_USE, _TARGET_UNKNOWN, _invoke_native, _merge_related, _native_result, _safe_scope_background, _session_key
15
15
 
16
16
 
17
17
  class PlanningContext:
@@ -317,31 +317,35 @@ class PlanningContext:
317
317
  complete = True
318
318
 
319
319
  def _drop(item: Mapping[str, Any]) -> None:
320
- """Record one related record that could not be projected verbatim.
320
+ """Record one related record that was withheld from the projection.
321
321
 
322
322
  ``complete`` answers one question only: can Core still prove that no
323
323
  **local** record was withheld, so that an absent UPDATE target and a
324
- create-safe lookup remain provable? Native sources are read-only
325
- host files that are never an UPDATE or NO_CHANGE target and are only
326
- ever referenced through ``shadow_native_ids``, so clipping one must
327
- not make the whole lookup unprovable. Treating a long host memory
328
- file as a failed proof would block every automatic extraction with no
329
- way for the user to recover except shrinking a file they own.
324
+ create-safe lookup remain provable?
325
+
326
+ Only a withheld record counts. A body trimmed to fit is still
327
+ projected -- its identity, type and scopes are all present, so it can
328
+ still be targeted and still rules out a duplicate CREATE -- and
329
+ marking that as an unprovable lookup would make ordinary Vault growth
330
+ fail turns.
331
+
332
+ Native sources are read-only host files that are never an UPDATE or
333
+ NO_CHANGE target and are only ever referenced through
334
+ ``shadow_native_ids``, so clipping one must not make the whole lookup
335
+ unprovable either. Treating a long host memory file as a failed
336
+ proof would block every automatic extraction with no way for the user
337
+ to recover except shrinking a file they own.
330
338
  """
331
339
 
332
340
  nonlocal complete
333
341
  if item.get("native") is not True:
334
342
  complete = False
335
343
 
336
- for index, value in enumerate(values):
337
- if len(selected) >= _RELATED_MAX_ITEMS:
338
- for item in values[index:]:
339
- _drop(item)
340
- break
344
+ for value in values:
341
345
  body = value.get("body")
342
346
  if isinstance(body, str) and len(body) > _RELATED_MAX_BODY_CHARS:
347
+ # Trimmed, not withheld: the record keeps its identity.
343
348
  value["body"] = body[: _RELATED_MAX_BODY_CHARS - 1].rstrip() + "…"
344
- _drop(value)
345
349
  size = cls._related_payload_size(value)
346
350
  if size < 0:
347
351
  _drop(value)
@@ -364,9 +368,10 @@ class PlanningContext:
364
368
  if size < 0 or used + size + (1 if selected else 0) > _RELATED_MAX_CHARS:
365
369
  _drop(value)
366
370
  continue
371
+ # A priority target kept in reduced form is still projected, so
372
+ # its identity is not withheld and the proof survives.
367
373
  value = minimal
368
374
  additional = size + (1 if selected else 0)
369
- _drop(value)
370
375
  selected.append(value)
371
376
  used += additional
372
377
  return selected, complete
@@ -259,15 +259,32 @@ _DIAGNOSTIC_SUMMARY_ALLOWED = frozenset(
259
259
  )
260
260
 
261
261
 
262
- _RELATED_MAX_ITEMS = 6
263
-
264
-
262
+ # The comparison catalog is the only part of the B3 prompt that grows with the
263
+ # Vault, so it needs a ceiling -- but the ceiling must be a safety net for the
264
+ # prompt's own hard limit, never a constraint that decides whether a turn may
265
+ # write.
266
+ #
267
+ # It used to be six items, which contradicted the retrieval design outright: a
268
+ # Vault whose memories all carry one scope (``global``, the common personal
269
+ # case, and the only scope in an empty registry) correctly resolves that scope
270
+ # to the whole library, and six rows then hid most of it. Withholding a local
271
+ # record is also what marks the lookup unprovable, so a small item cap turned
272
+ # ordinary growth into failing turns.
273
+ #
274
+ # The ceiling is now derived from the prompt budget instead of chosen as a round
275
+ # number. ``MAX_PROMPT_BYTES`` is 192 KiB; a catalog row measures about 1.6
276
+ # UTF-8 bytes per character in practice, so 40000 characters is roughly 64 KiB,
277
+ # leaving well over 100 KiB for the contract, the current evidence and the
278
+ # native catalog. That admits roughly 180 memories at the observed row size --
279
+ # far beyond the point where compaction is meant to bring the active set down.
280
+ _RELATED_MAX_CHARS = 40000
281
+
282
+
283
+ # A single pathological body must not consume the whole catalog budget. This
284
+ # only ever fires on a memory far longer than an atomic fact.
265
285
  _RELATED_MAX_BODY_CHARS = 1600
266
286
 
267
287
 
268
- _RELATED_MAX_CHARS = 6000
269
-
270
-
271
288
  _SCOPE_DIRECTORY_MAX_ITEMS = 8
272
289
 
273
290
 
@@ -23,6 +23,7 @@ from typing import Any, Mapping
23
23
  from .locking import atomic_write_json, read_json
24
24
  from .extraction_budget import aggregate_extraction_metrics
25
25
  from .service import Memleaf
26
+ from .subprocess_flags import hidden_popen_kwargs
26
27
  from .vault import Vault, safe_component
27
28
 
28
29
 
@@ -554,7 +555,10 @@ def _launch(vault: Vault, job_id: str) -> subprocess.Popen[Any]:
554
555
  "close_fds": True,
555
556
  }
556
557
  if os.name == "nt":
557
- kwargs["creationflags"] = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0) | getattr(subprocess, "DETACHED_PROCESS", 0)
558
+ # A windowless child, not a detached one: see subprocess_flags. The
559
+ # detached flag a previous release used opened a stray console window
560
+ # on the user's desktop for every captured turn.
561
+ kwargs.update(hidden_popen_kwargs())
558
562
  else:
559
563
  kwargs["start_new_session"] = True
560
564
  return subprocess.Popen(command, **kwargs)
@@ -104,7 +104,7 @@ def _enum_text(values: Iterable[str]) -> str:
104
104
  B3_COMPACT_CONTRACT = f"""B3 STRICT OUTPUT CONTRACT
105
105
  Root exactly: {{protocol_version,items,no_memory}}; protocol_version={PROTOCOL_VERSION}. No extra fields at any level.
106
106
  Decision exactly one of: {_enum_text(_DECISIONS)}. candidate_id: nonempty string, case-insensitively unique.
107
- CREATE exactly requires candidate_id,decision,evidence,type,scopes,memory; optional scope_source. type={_enum_text(MEMORY_TYPES)}; scopes=nonempty string[]. memory requires title+body.
107
+ CREATE exactly requires candidate_id,decision,evidence,type,scopes,memory; optional scope_source. type={_enum_text(MEMORY_TYPES)}; scopes=nonempty string[]; see SCOPES. memory requires title+body.
108
108
  UPDATE exactly requires candidate_id,decision,evidence,target_memory_id,memory; optional scopes and scope_source; scope_source requires scopes. memory requires body; title optional.
109
109
  NO_CHANGE exactly requires candidate_id,decision,evidence,target_memory_id.
110
110
  DEFERRED exactly requires candidate_id,decision,evidence,reason; reason={_enum_text(_DEFER_REASONS)}.
@@ -125,6 +125,12 @@ Only current_evidence may establish new facts or changes. local_memory_catalog a
125
125
  TASK
126
126
  Extract every independently useful long-term memory. Keep independently retrievable/updateable topics separate and preserve entity, condition, polarity, uncertainty, ownership, state and meaning-critical numbers/codes. CREATE only when no supplied local memory represents the durable information; UPDATE only when current evidence proves a change to one supplied local memory; NO_CHANGE only when it adds no semantic change; DEFERRED for a durable candidate that cannot safely reach a terminal decision. Project ownership and platform/system names are separate judgments. Do not turn every negation into no_memory and do not use NO_CHANGE to hide ambiguity.
127
127
 
128
+ SCOPES
129
+ Legal values: global | domain:<name> | portfolio:<name> | project:<name> | unscoped. scopes is a nonempty array; at most one project:<name> per memory; unscoped must be the only value, and Core then records insufficient_context.
130
+ When a candidate concerns one distinct project the user is working on -- a type=project memory, or the work, decisions, state or deadlines that belong to that project -- scope it to that project as project:<name>, using the name the evidence itself uses. The project does not need to be registered first; scope_registry only lists what already exists.
131
+ Ground the name in this candidate's own cited evidence, or in a project scope explicitly supplied for this turn. A product, platform, system, vendor, notification source, comparison or implementation context is not ownership by name alone.
132
+ Use global only for a fact that no single project owns: a standing personal preference, a machine-wide or tool-wide rule, or an environment fact. Never invent, translate or borrow a project name.
133
+
128
134
  DATES
129
135
  An evidence unit may carry an ISO-8601 UTC timestamp. Use it ONLY to resolve a relative, partial or yearless date that the unit's own text expresses; never borrow another unit's timestamp and never guess a missing year. The timestamp is an anchor, not content: never write its own date into a memory, and add no date the evidence text does not state. A date literal in a memory must appear in that memory's cited evidence, either verbatim or as the same month and day. Write due_date as YYYY-MM-DD. A todo declares status, and due_date only when its own evidence establishes a deadline: otherwise omit due_date, or set it to null when the deadline itself is still unresolved. Any memory carrying a date that no admitted evidence grounds is rejected and costs the whole turn, so defer instead of approximating.
130
136
 
@@ -490,6 +496,7 @@ def parse_single_pass_output(
490
496
  used_targets: set[str] = set()
491
497
  binding_rows: list[dict[str, Any]] = []
492
498
  prepared: list[tuple[dict[str, Any], Mapping[str, Any] | None]] = []
499
+ forced_defer: dict[str, str] = {}
493
500
 
494
501
  for item_index, raw_item in enumerate(items):
495
502
  item_path = f"items[{item_index}]"
@@ -549,11 +556,14 @@ def parse_single_pass_output(
549
556
  path=item_path, rule="relationship", actual=raw_item,
550
557
  expected_type="object", missing_fields=("scopes",),
551
558
  )
552
- if decision in {"CREATE", "UPDATE", "NO_CHANGE"} and not lookup_complete:
553
- raise ModelOutputError(
554
- "incomplete B3 lookup cannot authorize a terminal decision",
555
- validation_detail="other_schema_violation",
556
- )
559
+ if decision == "CREATE" and not lookup_complete:
560
+ # Only a CREATE claims novelty, and only a complete lookup can prove
561
+ # it. The candidate cannot be written, but nothing else in the turn
562
+ # depends on this proof, so it is deferred with the protocol's own
563
+ # reason instead of discarding every other verdict. UPDATE and
564
+ # NO_CHANGE name a target that is in the supplied catalog by
565
+ # construction, so they stay provable without it.
566
+ forced_defer[candidate_key] = "lookup_incomplete"
557
567
  candidate_ids.add(candidate_key)
558
568
  claims = raw_item.get("evidence")
559
569
  if isinstance(claims, Mapping):
@@ -742,6 +752,21 @@ def parse_single_pass_output(
742
752
  candidate_id = item["candidate_id"]
743
753
  decision = item["decision"]
744
754
  evidence = [dict(claim) for claim in bindings[candidate_id]]
755
+ forced = forced_defer.get(candidate_id.casefold())
756
+ if forced is not None:
757
+ normalized_items.append({
758
+ "candidate_id": candidate_id,
759
+ "decision": "DEFERRED",
760
+ "reason": forced,
761
+ "evidence": evidence,
762
+ })
763
+ if deferrals is not None:
764
+ deferrals.append({
765
+ "candidate_id": candidate_id,
766
+ "reason": forced,
767
+ "detail": forced,
768
+ })
769
+ continue
745
770
  normalized: dict[str, Any] = {
746
771
  "candidate_id": candidate_id,
747
772
  "decision": decision,
@@ -0,0 +1,43 @@
1
+ """Windows process-creation flags for child processes that must stay invisible.
2
+
3
+ memleaf starts three kinds of child process: the detached extraction worker,
4
+ host CLI probes, and host model-discovery commands. None of them owns a
5
+ window, and all of them can be started by a host that has no console of its
6
+ own -- a Hermes desktop process, for example. On Windows a console program
7
+ started without an explicit flag in that situation gets a brand-new console
8
+ window, which lands on the user's desktop as a stray terminal.
9
+
10
+ ``DETACHED_PROCESS`` looks like the right answer and is not: a console program
11
+ still needs a console, and Windows creates a visible one for it. Measured on
12
+ Windows 11 with Windows Terminal as the default host, spawning this interpreter
13
+ with ``CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS`` opened one new console
14
+ window whose title was the interpreter path, while ``CREATE_NO_WINDOW`` opened
15
+ none. ``CREATE_NO_WINDOW`` is therefore used, combined with
16
+ ``CREATE_NEW_PROCESS_GROUP`` so the child keeps its own group and is not taken
17
+ down by a Ctrl+C delivered to the parent's console.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import os
23
+ import subprocess
24
+
25
+
26
+ def hidden_process_flags() -> int:
27
+ """Return creation flags that keep a console child windowless on Windows."""
28
+
29
+ if os.name != "nt":
30
+ return 0
31
+ return getattr(subprocess, "CREATE_NO_WINDOW", 0) | getattr(
32
+ subprocess, "CREATE_NEW_PROCESS_GROUP", 0
33
+ )
34
+
35
+
36
+ def hidden_popen_kwargs() -> dict[str, int]:
37
+ """Return ``Popen``/``run`` keyword arguments for a windowless child."""
38
+
39
+ flags = hidden_process_flags()
40
+ return {"creationflags": flags} if flags else {}
41
+
42
+
43
+ __all__ = ["hidden_popen_kwargs", "hidden_process_flags"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memleaf
3
- Version: 0.2.49
3
+ Version: 0.2.51
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.49。**
26
+ > **版本:0.2.51。**
27
27
  > 记忆只从用户与 Agent 的可见对话提炼。Agent 已在回复中整理的事实、项目进展和明确待办可作为来源;邮件、附件、网页和终端等工具原文不进入记忆提炼。模型负责保留原话中的不确定性,现有记忆仅用于比较、去重和更新。Markdown 仍是唯一事实源。
28
28
  > **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
29
29
 
@@ -77,6 +77,7 @@ src/memleaf/single_pass_memory_planner.py
77
77
  src/memleaf/single_pass_plan.py
78
78
  src/memleaf/source_policy.py
79
79
  src/memleaf/state_layout.py
80
+ src/memleaf/subprocess_flags.py
80
81
  src/memleaf/summary_batch.py
81
82
  src/memleaf/target_reconciliation.py
82
83
  src/memleaf/turn_audit.py
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes