memleaf 0.2.48__tar.gz → 0.2.50__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {memleaf-0.2.48 → memleaf-0.2.50}/CHANGELOG.md +19 -0
- {memleaf-0.2.48/src/memleaf.egg-info → memleaf-0.2.50}/PKG-INFO +2 -2
- {memleaf-0.2.48 → memleaf-0.2.50}/README.en.md +1 -1
- {memleaf-0.2.48 → memleaf-0.2.50}/README.md +1 -1
- {memleaf-0.2.48 → memleaf-0.2.50}/pyproject.toml +1 -1
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/__init__.py +1 -1
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/adapters/base.py +2 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/hermes_provider/plugin.yaml +1 -1
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/installer.py +2 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/planning_context.py +20 -15
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/process_common.py +23 -6
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/process_jobs.py +5 -1
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/single_pass_plan.py +115 -31
- memleaf-0.2.50/src/memleaf/subprocess_flags.py +43 -0
- {memleaf-0.2.48 → memleaf-0.2.50/src/memleaf.egg-info}/PKG-INFO +2 -2
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf.egg-info/SOURCES.txt +1 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/LICENSE +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/MANIFEST.in +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/docs/capture-budget-design.md +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/docs/config-migrations.md +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/docs/core-refactor.md +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/docs/evidence-retention.md +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/docs/extraction-latency.md +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/docs/gate-evidence-boundary.md +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/docs/general-processing.md +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/docs/hermes-mcp-runtime.md +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/docs/processing-quality-acceptance.md +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/docs/v0.2.26-processing-status.md +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/examples/README.md +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/examples/basic_usage.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/examples/mcp_stdio.ndjson +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/install.ps1 +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/install.sh +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/setup.cfg +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/__main__.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/adapters/__init__.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/adapters/antigravity.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/adapters/codex.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/adapters/hermes.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/admission.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/batch_review.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/budget.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/capture.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/cli.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/compaction.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/config.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/create_coordinator.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/credentials.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/evidence_budget.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/evidence_policy.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/evidence_structure.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/evidence_syntax.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/extraction_budget.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/extraction_capability.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/extraction_work_state.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/frontmatter.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/hermes_provider/README.md +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/hermes_provider/__init__.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/hermes_provider/_mcp_client.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/hermes_provider/_provider.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/hermes_provider/_shared.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/hermes_provider/evidence_budget.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/hermes_runtime.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/host_events.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/host_runtime.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/inbox.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/index.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/inspection.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/llm/__init__.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/llm/base.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/llm/claude_compatible.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/llm/gemini.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/llm/openai_compatible.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/llm/router.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/llm/thinking.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/locking.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/mcp_server.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/memory_commit.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/memory_planner.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/memory_writer.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/model_capabilities.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/model_discovery.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/model_execution.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/models.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/native_index.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/native_registration.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/parallel_model.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/process_journal.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/process_owner.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/processing.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/prompts.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/provenance.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/recording_policy.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/redaction.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/retention.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/retrieval.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/retrieval_gate.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/scope_maintenance.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/scope_state.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/service.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/single_pass_memory_planner.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/source_policy.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/state_layout.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/summary_batch.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/target_reconciliation.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/turn_audit.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/turn_plan.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/update_coordinator.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/update_review.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/validation.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf/vault.py +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf.egg-info/dependency_links.txt +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf.egg-info/entry_points.txt +0 -0
- {memleaf-0.2.48 → memleaf-0.2.50}/src/memleaf.egg-info/top_level.txt +0 -0
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to memleaf are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.2.50 — 2026-09-13
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
- 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.
|
|
9
|
+
- 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.
|
|
10
|
+
- 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.
|
|
11
|
+
- 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.
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
## 0.2.49 — 2026-09-13
|
|
16
|
+
|
|
17
|
+
- 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.
|
|
18
|
+
- Keep the deferral boundary narrow and explicit. Only details that describe the candidate itself are converted: scope authorisation and update-target problems map to `scope_ambiguous` and `target_ambiguous`, while a malformed proposal (todo fields, ungrounded or malformed dates, type, source shape) maps to `maintenance_uncertain` and missing admitted evidence maps to `evidence_insufficient`. Every other failure — transport, truncation, envelope shape, evidence coverage, binding scope, and any detail Core does not recognise — still fails the whole turn closed, so a systemic fault cannot hide behind a deferral.
|
|
19
|
+
- Make the deferral visible rather than silent. Each one is counted as `b3_candidate_deferred_count` on the turn, the cause is recorded against the candidate, and the deferred work continues to surface through `deferred_candidates`, `retryable_deferred_turns` and `coverage_status: partial` in the read-only processing status. The persisted cause is restricted to a bounded identifier so no model text can reach state files.
|
|
20
|
+
- Keep the deferral from being mistaken for a collision. Same-target reconciliation groups items that reach a terminal disposition for one memory, and a deferred candidate reaches none: it writes nothing, carries no update target, and cannot conflict with an admitted update to the same memory. Only the candidates that actually reach UPDATE or NO_CHANGE are grouped now, so a turn that updates one memory and defers another does not spend its second request reconciling a candidate that was never admitted.
|
|
21
|
+
|
|
22
|
+
Verification for this release repeated the same real-model replay used for 0.2.48 and added focused synthetic tests: 90 tests pass with no model call, covering the sibling-commits case, every deferrable detail and the reason it maps to, deferral being driven by an explicit allowlist, the optional argument for direct parser callers, a deferred member not being grouped as a same-target collision, and the turn-level failures that must still abort. Three further preview runs of the previously failing production session completed with `execution_status: ok` and `coverage_status: complete`. A deferral is a visible degradation rather than a success: the candidate is retried, not written, and a prompt or configuration fault that made every proposal invalid would now show up as a fully deferred turn rather than as a hard failure.
|
|
23
|
+
|
|
5
24
|
## 0.2.48 — 2026-09-13
|
|
6
25
|
|
|
7
26
|
- Restore same-target reconciliation in the B3 single-pass path. A real turn was discarded with `B3 target referenced more than once` whenever the model admitted two changes for one existing memory. One memory can only receive one terminal disposition per turn, because the writer archives the previous version and overwrites the same file, so the colliding items are now reconciled with one bounded call into a single current state and an unresolvable group is deferred as `target_ambiguous` instead of guessed. This is the capability the legacy staged path already had; it is not a new retry class, and the compact contract now tells the model to merge such changes into one UPDATE in the first place. The call is charged against the same two-request turn budget as the primary extraction and its structural repair, so a collision can never lengthen a turn: when a repair has already spent the budget, the group defers and every other item still commits. The strict "a target may be used once" rejection remains in the parser for direct callers.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.50
|
|
4
4
|
Summary: A local-first Markdown memory core for AI agents
|
|
5
5
|
Author: memleaf contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -23,7 +23,7 @@ Dynamic: license-file
|
|
|
23
23
|
|
|
24
24
|
[English](README.en.md) · [PyPI](https://pypi.org/project/memleaf/) · [GitHub](https://github.com/miffyblueboo/memleaf)
|
|
25
25
|
|
|
26
|
-
> **版本:0.2.
|
|
26
|
+
> **版本:0.2.50。**
|
|
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.
|
|
7
|
+
> **Version: 0.2.50.**
|
|
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.
|
|
7
|
+
> **版本:0.2.50。**
|
|
8
8
|
> 记忆只从用户与 Agent 的可见对话提炼。Agent 已在回复中整理的事实、项目进展和明确待办可作为来源;邮件、附件、网页和终端等工具原文不进入记忆提炼。模型负责保留原话中的不确定性,现有记忆仅用于比较、去重和更新。Markdown 仍是唯一事实源。
|
|
9
9
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
10
10
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Local-first Markdown memory core for AI agents."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.2.
|
|
3
|
+
__version__ = "0.2.50"
|
|
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)
|
|
@@ -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,
|
|
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
|
|
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?
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
@@ -45,6 +45,29 @@ _DEFER_REASONS = frozenset({
|
|
|
45
45
|
"lookup_incomplete",
|
|
46
46
|
"maintenance_uncertain",
|
|
47
47
|
})
|
|
48
|
+
# A rejected proposal is a candidate-local problem, not a turn-local one.
|
|
49
|
+
# v0.2.26 established that "invalid proposals receive bounded model correction,
|
|
50
|
+
# then explicit candidate-local deferral; valid siblings proceed", and v0.2.34
|
|
51
|
+
# required that candidate-local deferral and idempotent partial retries be
|
|
52
|
+
# preserved. Only details that describe the candidate itself are listed here:
|
|
53
|
+
# anything unrecognised, and every transport or invariant failure, still fails
|
|
54
|
+
# the whole turn closed.
|
|
55
|
+
_B3_DEFERRABLE_DETAILS = {
|
|
56
|
+
"scope_not_grounded": "scope_ambiguous",
|
|
57
|
+
"scope_drift": "scope_ambiguous",
|
|
58
|
+
"invalid_scope": "scope_ambiguous",
|
|
59
|
+
"invalid_scope_source": "scope_ambiguous",
|
|
60
|
+
"invalid_update_target": "target_ambiguous",
|
|
61
|
+
"duplicate_update_target": "target_ambiguous",
|
|
62
|
+
"invalid_due_date": "maintenance_uncertain",
|
|
63
|
+
"due_date_not_grounded": "maintenance_uncertain",
|
|
64
|
+
"relative_time": "maintenance_uncertain",
|
|
65
|
+
"todo_fields": "maintenance_uncertain",
|
|
66
|
+
"invalid_type": "maintenance_uncertain",
|
|
67
|
+
"source_shape": "maintenance_uncertain",
|
|
68
|
+
"invalid_evidence": "evidence_insufficient",
|
|
69
|
+
}
|
|
70
|
+
_DETAIL_TEXT_RE = re.compile(r"^[a-z_]{1,48}$")
|
|
48
71
|
_MEMORY_FIELDS = frozenset({
|
|
49
72
|
"title", "body", "tags", "aliases", "keywords", "status", "completed_at", "due_date",
|
|
50
73
|
"shadow_native_ids",
|
|
@@ -409,6 +432,7 @@ def parse_single_pass_output(
|
|
|
409
432
|
validate_memory: MemoryValidator,
|
|
410
433
|
target_rows: list[dict[str, Any]] | None = None,
|
|
411
434
|
normalizations: list[str] | None = None,
|
|
435
|
+
deferrals: list[dict[str, Any]] | None = None,
|
|
412
436
|
) -> dict[str, Any]:
|
|
413
437
|
if not callable(validate_memory):
|
|
414
438
|
raise TypeError("validate_memory must be callable")
|
|
@@ -466,6 +490,7 @@ def parse_single_pass_output(
|
|
|
466
490
|
used_targets: set[str] = set()
|
|
467
491
|
binding_rows: list[dict[str, Any]] = []
|
|
468
492
|
prepared: list[tuple[dict[str, Any], Mapping[str, Any] | None]] = []
|
|
493
|
+
forced_defer: dict[str, str] = {}
|
|
469
494
|
|
|
470
495
|
for item_index, raw_item in enumerate(items):
|
|
471
496
|
item_path = f"items[{item_index}]"
|
|
@@ -525,11 +550,14 @@ def parse_single_pass_output(
|
|
|
525
550
|
path=item_path, rule="relationship", actual=raw_item,
|
|
526
551
|
expected_type="object", missing_fields=("scopes",),
|
|
527
552
|
)
|
|
528
|
-
if decision
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
553
|
+
if decision == "CREATE" and not lookup_complete:
|
|
554
|
+
# Only a CREATE claims novelty, and only a complete lookup can prove
|
|
555
|
+
# it. The candidate cannot be written, but nothing else in the turn
|
|
556
|
+
# depends on this proof, so it is deferred with the protocol's own
|
|
557
|
+
# reason instead of discarding every other verdict. UPDATE and
|
|
558
|
+
# NO_CHANGE name a target that is in the supplied catalog by
|
|
559
|
+
# construction, so they stay provable without it.
|
|
560
|
+
forced_defer[candidate_key] = "lookup_incomplete"
|
|
533
561
|
candidate_ids.add(candidate_key)
|
|
534
562
|
claims = raw_item.get("evidence")
|
|
535
563
|
if isinstance(claims, Mapping):
|
|
@@ -717,30 +745,69 @@ def parse_single_pass_output(
|
|
|
717
745
|
for item, target_record in prepared:
|
|
718
746
|
candidate_id = item["candidate_id"]
|
|
719
747
|
decision = item["decision"]
|
|
748
|
+
evidence = [dict(claim) for claim in bindings[candidate_id]]
|
|
749
|
+
forced = forced_defer.get(candidate_id.casefold())
|
|
750
|
+
if forced is not None:
|
|
751
|
+
normalized_items.append({
|
|
752
|
+
"candidate_id": candidate_id,
|
|
753
|
+
"decision": "DEFERRED",
|
|
754
|
+
"reason": forced,
|
|
755
|
+
"evidence": evidence,
|
|
756
|
+
})
|
|
757
|
+
if deferrals is not None:
|
|
758
|
+
deferrals.append({
|
|
759
|
+
"candidate_id": candidate_id,
|
|
760
|
+
"reason": forced,
|
|
761
|
+
"detail": forced,
|
|
762
|
+
})
|
|
763
|
+
continue
|
|
720
764
|
normalized: dict[str, Any] = {
|
|
721
765
|
"candidate_id": candidate_id,
|
|
722
766
|
"decision": decision,
|
|
723
|
-
"evidence":
|
|
767
|
+
"evidence": evidence,
|
|
724
768
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
769
|
+
try:
|
|
770
|
+
if decision == "CREATE":
|
|
771
|
+
validated = validate_memory(
|
|
772
|
+
candidate_id, decision, None, None, item["memory"], evidence, item,
|
|
773
|
+
)
|
|
774
|
+
normalized.update({
|
|
775
|
+
"type": item["type"], "scopes": list(item["scopes"]), "memory": dict(validated),
|
|
776
|
+
})
|
|
777
|
+
elif decision == "UPDATE":
|
|
778
|
+
target_id = item["target_memory_id"]
|
|
779
|
+
validated = validate_memory(
|
|
780
|
+
candidate_id, decision, target_id, target_record,
|
|
781
|
+
item["memory"], evidence, item,
|
|
782
|
+
)
|
|
783
|
+
normalized["target_memory_id"] = target_id
|
|
784
|
+
normalized["memory"] = dict(validated)
|
|
785
|
+
elif decision == "NO_CHANGE":
|
|
786
|
+
normalized["target_memory_id"] = item["target_memory_id"]
|
|
787
|
+
else:
|
|
788
|
+
normalized["reason"] = item["reason"]
|
|
789
|
+
except ModelOutputError as error:
|
|
790
|
+
detail = getattr(error, "validation_detail", None)
|
|
791
|
+
reason = _B3_DEFERRABLE_DETAILS.get(detail) if isinstance(detail, str) else None
|
|
792
|
+
if reason is None:
|
|
793
|
+
raise
|
|
794
|
+
# The proposal is unsafe to write, but its siblings are unaffected.
|
|
795
|
+
# Defer this one candidate so the turn keeps every verdict it can
|
|
796
|
+
# justify, and record the cause instead of failing silently.
|
|
797
|
+
normalized = {
|
|
798
|
+
"candidate_id": candidate_id,
|
|
799
|
+
"decision": "DEFERRED",
|
|
800
|
+
"reason": reason,
|
|
801
|
+
"evidence": evidence,
|
|
802
|
+
}
|
|
803
|
+
if deferrals is not None:
|
|
804
|
+
deferrals.append({
|
|
805
|
+
"candidate_id": candidate_id,
|
|
806
|
+
"reason": reason,
|
|
807
|
+
"detail": (
|
|
808
|
+
detail if _DETAIL_TEXT_RE.fullmatch(detail) else "unrecognised_detail"
|
|
809
|
+
),
|
|
810
|
+
})
|
|
744
811
|
normalized_items.append(normalized)
|
|
745
812
|
return {"protocol_version": PROTOCOL_VERSION, "items": normalized_items, "no_memory": normalized_no_memory}
|
|
746
813
|
|
|
@@ -1060,10 +1127,12 @@ def run_single_pass_stage(
|
|
|
1060
1127
|
primary_system = "" if inline_system else SINGLE_PASS_SYSTEM
|
|
1061
1128
|
target_rows: list[dict[str, Any]] = []
|
|
1062
1129
|
normalizations: list[str] = []
|
|
1130
|
+
deferrals: list[dict[str, Any]] = []
|
|
1063
1131
|
|
|
1064
1132
|
def parse(raw: str) -> dict[str, Any]:
|
|
1065
1133
|
target_rows.clear()
|
|
1066
1134
|
normalizations.clear()
|
|
1135
|
+
deferrals.clear()
|
|
1067
1136
|
return parse_single_pass_output(
|
|
1068
1137
|
raw,
|
|
1069
1138
|
evidence_units=source_units,
|
|
@@ -1072,6 +1141,7 @@ def run_single_pass_stage(
|
|
|
1072
1141
|
validate_memory=validate_memory,
|
|
1073
1142
|
target_rows=target_rows,
|
|
1074
1143
|
normalizations=normalizations,
|
|
1144
|
+
deferrals=deferrals,
|
|
1075
1145
|
)
|
|
1076
1146
|
|
|
1077
1147
|
def finalize(parsed: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -1084,12 +1154,6 @@ def run_single_pass_stage(
|
|
|
1084
1154
|
and an unresolvable group is deferred instead of guessed.
|
|
1085
1155
|
"""
|
|
1086
1156
|
|
|
1087
|
-
grouped: dict[str, list[str]] = {}
|
|
1088
|
-
for row in target_rows:
|
|
1089
|
-
grouped.setdefault(str(row["target"]), []).append(str(row["candidate_id"]))
|
|
1090
|
-
collisions = {key: ids for key, ids in grouped.items() if len(ids) > 1}
|
|
1091
|
-
if not collisions:
|
|
1092
|
-
return parsed
|
|
1093
1157
|
items = parsed.get("items")
|
|
1094
1158
|
if not isinstance(items, list) or not items:
|
|
1095
1159
|
return parsed
|
|
@@ -1098,6 +1162,22 @@ def run_single_pass_stage(
|
|
|
1098
1162
|
for index, row in enumerate(items)
|
|
1099
1163
|
if isinstance(row, Mapping)
|
|
1100
1164
|
}
|
|
1165
|
+
terminal_ids = {
|
|
1166
|
+
str(row.get("candidate_id"))
|
|
1167
|
+
for row in items
|
|
1168
|
+
if isinstance(row, Mapping) and row.get("decision") in {"UPDATE", "NO_CHANGE"}
|
|
1169
|
+
}
|
|
1170
|
+
grouped: dict[str, list[str]] = {}
|
|
1171
|
+
for row in target_rows:
|
|
1172
|
+
candidate_id = str(row["candidate_id"])
|
|
1173
|
+
if candidate_id not in terminal_ids:
|
|
1174
|
+
# A candidate Core deferred writes nothing, so it cannot
|
|
1175
|
+
# collide with a terminal disposition for the same memory.
|
|
1176
|
+
continue
|
|
1177
|
+
grouped.setdefault(str(row["target"]), []).append(candidate_id)
|
|
1178
|
+
collisions = {key: ids for key, ids in grouped.items() if len(ids) > 1}
|
|
1179
|
+
if not collisions:
|
|
1180
|
+
return parsed
|
|
1101
1181
|
|
|
1102
1182
|
def union_evidence(members: Sequence[Mapping[str, Any]]) -> list[Any]:
|
|
1103
1183
|
merged: list[Any] = []
|
|
@@ -1273,6 +1353,8 @@ def run_single_pass_stage(
|
|
|
1273
1353
|
event(repair_context, "parse_accepted_count")
|
|
1274
1354
|
if normalizations:
|
|
1275
1355
|
event(repair_context, "b3_normalization_count", len(normalizations))
|
|
1356
|
+
if deferrals:
|
|
1357
|
+
event(repair_context, "b3_candidate_deferred_count", len(deferrals))
|
|
1276
1358
|
writer = getattr(model_executor, "_write_model_diagnostic", None)
|
|
1277
1359
|
if callable(writer):
|
|
1278
1360
|
try:
|
|
@@ -1289,6 +1371,8 @@ def run_single_pass_stage(
|
|
|
1289
1371
|
event(primary_context, "parse_accepted_count")
|
|
1290
1372
|
if normalizations:
|
|
1291
1373
|
event(primary_context, "b3_normalization_count", len(normalizations))
|
|
1374
|
+
if deferrals:
|
|
1375
|
+
event(primary_context, "b3_candidate_deferred_count", len(deferrals))
|
|
1292
1376
|
writer = getattr(model_executor, "_write_model_diagnostic", None)
|
|
1293
1377
|
if callable(writer):
|
|
1294
1378
|
try:
|
|
@@ -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.
|
|
3
|
+
Version: 0.2.50
|
|
4
4
|
Summary: A local-first Markdown memory core for AI agents
|
|
5
5
|
Author: memleaf contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -23,7 +23,7 @@ Dynamic: license-file
|
|
|
23
23
|
|
|
24
24
|
[English](README.en.md) · [PyPI](https://pypi.org/project/memleaf/) · [GitHub](https://github.com/miffyblueboo/memleaf)
|
|
25
25
|
|
|
26
|
-
> **版本:0.2.
|
|
26
|
+
> **版本:0.2.50。**
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|