superlocalmemory 3.7.1 → 3.7.3

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 (32) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +16 -8
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/requirements.txt +1 -1
  6. package/plugin/scripts/slm-launch +11 -3
  7. package/plugin/scripts/slm-launch.bat +8 -2
  8. package/plugin-src/manifest.json +1 -1
  9. package/plugin-src/requirements.txt +1 -1
  10. package/plugin-src/scripts/slm-launch +11 -3
  11. package/plugin-src/scripts/slm-launch.bat +8 -2
  12. package/pyproject.toml +1 -1
  13. package/src/superlocalmemory/__init__.py +1 -1
  14. package/src/superlocalmemory/cli/main.py +2 -2
  15. package/src/superlocalmemory/cli/scale_engine_cmd.py +13 -1
  16. package/src/superlocalmemory/core/backend_orchestrator.py +15 -0
  17. package/src/superlocalmemory/core/embedding_worker.py +7 -1
  18. package/src/superlocalmemory/core/embeddings.py +8 -0
  19. package/src/superlocalmemory/core/engine.py +1 -3
  20. package/src/superlocalmemory/core/engine_wiring.py +0 -5
  21. package/src/superlocalmemory/core/ram_lock.py +42 -4
  22. package/src/superlocalmemory/core/scale_engine.py +450 -35
  23. package/src/superlocalmemory/evolution/budget.py +43 -8
  24. package/src/superlocalmemory/hooks/claude_code_hooks.py +2 -1
  25. package/src/superlocalmemory/hooks/context_payload.py +2 -1
  26. package/src/superlocalmemory/mcp/http_transport.py +2 -1
  27. package/src/superlocalmemory/mcp/tools_core.py +46 -29
  28. package/src/superlocalmemory/mesh/broker.py +111 -61
  29. package/src/superlocalmemory/optimize/proxy/server.py +2 -1
  30. package/src/superlocalmemory/retrieval/spreading_activation.py +53 -3
  31. package/src/superlocalmemory/server/routes/brain.py +2 -1
  32. package/src/superlocalmemory/server/unified_daemon.py +24 -7
package/CHANGELOG.md CHANGED
@@ -5,6 +5,25 @@ All notable changes to SuperLocalMemory V3 will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.7.3] - 2026-07-18 — Scale Engine integrity release
9
+
10
+ ### Fixed
11
+
12
+ - Added explicit, structurally verified adoption for pre-v3.7 CozoDB/LanceDB projections.
13
+ - Added durable promotion recovery, lifecycle serialization, and a final canonical-source consistency fence.
14
+ - Made Scale Engine status distinguish installed projection paths, lifecycle state, and live daemon backend health.
15
+ - Unified shipped runtime identity metadata at the release version.
16
+
17
+ ## [3.7.2] - 2026-07-16 — Reliability release
18
+
19
+ ### Fixed
20
+
21
+ - Strengthened daemon-owned local write coordination across Mesh and MCP paths.
22
+ - Kept durable facts queryable when optional enrichment needs a controlled retry.
23
+ - Preserved graph-aware retrieval through the canonical fallback when sqlite-vec is unavailable.
24
+ - Bounded local embedding-worker dependency failures to prevent repeated worker respawns.
25
+ - Hardened Windows RAM reservations and cross-platform installer validation.
26
+
8
27
  ## [3.7.1] - 2026-07-16 — Installer-parity hotfix
9
28
 
10
29
  ### Fixed
package/README.md CHANGED
@@ -1,11 +1,14 @@
1
1
  <p align="center">
2
- <img src="https://superlocalmemory.com/assets/logo-mark.png" alt="SuperLocalMemory" width="200"/>
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="assets/branding/slm-wordmark-dark.svg">
4
+ <img src="assets/branding/slm-wordmark-light.svg" alt="SuperLocalMemory" width="390">
5
+ </picture>
3
6
  </p>
4
7
 
5
- <h1 align="center">SuperLocalMemory V3.7.1</h1>
8
+ <h1 align="center">SuperLocalMemory V3.7.3</h1>
6
9
  <p align="center"><strong>Cache. Compress. Remember. Three surfaces — proxy, MCP tools, or skill. Every setup covered.</strong><br/>
7
10
  <em>Local-first agent memory with explicit operating modes, auditable retrieval, and optional Optimize tools.</em></p>
8
- <p align="center"><code>v3.7.1</code> — <strong>Installer-parity hotfix; release witnesses required before outreach.</strong><br/>
11
+ <p align="center"><code>v3.7.3</code> — <strong>Scale Engine integrity release: explicit legacy adoption, durable promotion recovery, and honest runtime status.</strong><br/>
9
12
  Proxy: <code>slm wrap claude</code> &nbsp;·&nbsp; MCP: add <code>slm_compress</code> to your config &nbsp;·&nbsp; Skill: zero-config</p>
10
13
  <p align="center"><strong>3 public research preprints</strong> (arXiv + Zenodo archives) · <a href="https://arxiv.org/abs/2603.02240">arXiv:2603.02240</a> · <a href="https://arxiv.org/abs/2603.14588">arXiv:2603.14588</a> · <a href="https://arxiv.org/abs/2604.04514">arXiv:2604.04514</a></p>
11
14
 
@@ -108,7 +111,7 @@ health surfaces expose the stages actually completed by the installed runtime.
108
111
  | **Retrieval and recall** | Semantic, lexical, temporal, Hopfield and spreading-activation candidate channels; RRF fusion, optional reranking and graph score enhancement | Healthy channels participate; response provenance states the evidence used. |
109
112
  | **Brain and learning** | Behavioral patterns, feedback/outcome records, rewards, consolidation, LightGBM-related ranking components, soft prompts, and guarded skill-evolution workflows | Learning is evidence-driven; it does not claim autonomous correctness or guaranteed improvement. |
110
113
  | **Knowledge graph and entities** | Canonical entities, aliases, entity profiles, graph edges, scenes, timelines, explorer and graph APIs | Stored/derived graph data is evidence, not an instruction authority. |
111
- | **Scale Engine** | SQLite + sqlite-vec are canonical. CozoDB graph and LanceDB vector projections are packaged and managed with prepare → verify → promote → rollback | Promotion is explicit and parity-gated; do not advertise an unverified projection as the source of truth. |
114
+ | **Scale Engine** | SQLite + sqlite-vec are canonical. CozoDB graph and LanceDB vector projections are managed with prepare → verify → promote → rollback; a structurally detected pre-v3.7 projection can be explicitly adopted. | Promotion is parity-gated and crash-recoverable. Legacy adoption preserves the prior projection as a rollback backup; if proof fails, routing remains on canonical SQLite and status reports repair required. |
112
115
  | **Optimize** | Exact cache, tagged invalidation, safe compression, opt-in aggressive prose compression, CCR originals, proxy/MCP/skill surfaces | Only proxy intercepts a primary provider turn. MCP/skill cache results explicitly routed through SLM. |
113
116
  | **Mesh** | Authenticated peer messages, inbox/outbox, locks, offline queue, optional discovery and mesh MCP tools | Mesh is coordination, not automatic replicated memory or conflict resolution. |
114
117
  | **Governance and operations** | Provenance, audit/retention/policy surfaces, export/erasure controls, diagnostics, health, backups and daemon lifecycle | These are engineering controls, not a legal certification. |
@@ -227,10 +230,15 @@ optional reranking, and entity-graph score enhancement. The entity graph does
227
230
  not create an independent candidate in the current implementation. Core memory
228
231
  is SQLite-backed. SQLite and sqlite-vec remain the canonical source of truth.
229
232
  The packaged Scale Engine can maintain CozoDB graph and LanceDB vector
230
- projections, and it remains outside active retrieval paths until
231
- `slm db scale prepare`, `verify`, and `promote` prove parity against the
232
- canonical store. This makes the capability available on a fresh installation
233
- without silently migrating an existing user's data.
233
+ projections, and it remains outside active retrieval paths until a staged
234
+ parity witness proves it matches the canonical store. New installations remain
235
+ on Local Core. During upgrade, `slm db scale status` can identify a positive
236
+ pre-v3.7 layout candidate; the operator confirms it with `slm db scale adopt`.
237
+ SLM then rebuilds from canonical SQLite, verifies it, and promotes it with a
238
+ durable recovery journal while retaining the prior directories as a rollback
239
+ backup. `adopt` reports `restart_required: true`; run `slm restart` before
240
+ checking daemon health. If proof fails, recall remains on SQLite and status
241
+ reports repair required.
234
242
 
235
243
  Canonical ingestion is a durable state machine: `raw → queryable → enriching →
236
244
  complete`, with `failed` retaining raw evidence, error details, attempt count,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superlocalmemory",
3
- "version": "3.7.1",
3
+ "version": "3.7.3",
4
4
  "description": "Local-first agent memory with MCP and an agent-native CLI. Documented clients include Claude Code, Cursor, and Windsurf.",
5
5
  "keywords": [
6
6
  "ai-memory",
@@ -16,5 +16,5 @@
16
16
  "mcpServers": "./.mcp.json",
17
17
  "name": "superlocalmemory",
18
18
  "repository": "https://github.com/qualixar/superlocalmemory",
19
- "version": "3.7.1"
19
+ "version": "3.7.3"
20
20
  }
@@ -1 +1 @@
1
- superlocalmemory==3.7.1
1
+ superlocalmemory==3.7.3
@@ -4,12 +4,20 @@
4
4
  # Cross-platform counterpart: slm-launch.bat (Windows)
5
5
  # Referenced by plugin/.mcp.json as the MCP server command.
6
6
  #
7
- # Resolves the correct venv binary for POSIX:
8
- # ${CLAUDE_PLUGIN_DATA}/venv/bin/slm mcp
7
+ # Resolves the correct venv binary for POSIX and joins the namespace daemon
8
+ # before opening the MCP stdio transport. This preserves one writer for all
9
+ # parallel Claude sessions that share CLAUDE_PLUGIN_DATA.
9
10
  #
10
11
  # On Windows, Claude Code invokes slm-launch.bat instead (same-stem, .bat extension).
11
12
  #
12
13
  # Environment:
13
14
  # CLAUDE_PLUGIN_DATA — persistent data dir where venv lives
14
15
 
15
- exec "${CLAUDE_PLUGIN_DATA}/venv/bin/slm" mcp
16
+ SLM_BIN="${CLAUDE_PLUGIN_DATA}/venv/bin/slm"
17
+
18
+ if ! "${SLM_BIN}" serve start >&2; then
19
+ echo "SLM plugin: unable to start the owned daemon; refusing a direct MCP writer." >&2
20
+ exit 1
21
+ fi
22
+
23
+ exec "${SLM_BIN}" mcp
@@ -4,8 +4,8 @@
4
4
  :: Cross-platform counterpart: slm-launch (POSIX bash)
5
5
  :: Referenced by plugin/.mcp.json as the MCP server command (Windows picks .bat automatically).
6
6
  ::
7
- :: Resolves the correct venv binary for Windows:
8
- :: %CLAUDE_PLUGIN_DATA%\venv\Scripts\slm.exe mcp
7
+ :: Resolves the correct venv binary for Windows and joins the namespace daemon
8
+ :: before opening MCP, preserving one writer for parallel Claude sessions.
9
9
  ::
10
10
  :: On Windows, Python venv places entry points in Scripts\ (not bin\ like POSIX).
11
11
  :: This launcher bridges the path difference so ONE .mcp.json command field works
@@ -14,4 +14,10 @@
14
14
  :: Environment:
15
15
  :: CLAUDE_PLUGIN_DATA — persistent data dir where venv lives
16
16
 
17
+ "%CLAUDE_PLUGIN_DATA%\venv\Scripts\slm.exe" serve start 1>&2
18
+ if errorlevel 1 (
19
+ echo SLM plugin: unable to start the owned daemon; refusing a direct MCP writer. 1>&2
20
+ exit /b 1
21
+ )
22
+
17
23
  "%CLAUDE_PLUGIN_DATA%\venv\Scripts\slm.exe" mcp
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "3.7.1",
2
+ "version": "3.7.3",
3
3
  "pluginName": "superlocalmemory",
4
4
  "displayName": "SuperLocalMemory",
5
5
  "repository": "https://github.com/qualixar/superlocalmemory",
@@ -1 +1 @@
1
- superlocalmemory==3.7.1
1
+ superlocalmemory==3.7.3
@@ -4,12 +4,20 @@
4
4
  # Cross-platform counterpart: slm-launch.bat (Windows)
5
5
  # Referenced by plugin/.mcp.json as the MCP server command.
6
6
  #
7
- # Resolves the correct venv binary for POSIX:
8
- # ${CLAUDE_PLUGIN_DATA}/venv/bin/slm mcp
7
+ # Resolves the correct venv binary for POSIX and joins the namespace daemon
8
+ # before opening the MCP stdio transport. This preserves one writer for all
9
+ # parallel Claude sessions that share CLAUDE_PLUGIN_DATA.
9
10
  #
10
11
  # On Windows, Claude Code invokes slm-launch.bat instead (same-stem, .bat extension).
11
12
  #
12
13
  # Environment:
13
14
  # CLAUDE_PLUGIN_DATA — persistent data dir where venv lives
14
15
 
15
- exec "${CLAUDE_PLUGIN_DATA}/venv/bin/slm" mcp
16
+ SLM_BIN="${CLAUDE_PLUGIN_DATA}/venv/bin/slm"
17
+
18
+ if ! "${SLM_BIN}" serve start >&2; then
19
+ echo "SLM plugin: unable to start the owned daemon; refusing a direct MCP writer." >&2
20
+ exit 1
21
+ fi
22
+
23
+ exec "${SLM_BIN}" mcp
@@ -4,8 +4,8 @@
4
4
  :: Cross-platform counterpart: slm-launch (POSIX bash)
5
5
  :: Referenced by plugin/.mcp.json as the MCP server command (Windows picks .bat automatically).
6
6
  ::
7
- :: Resolves the correct venv binary for Windows:
8
- :: %CLAUDE_PLUGIN_DATA%\venv\Scripts\slm.exe mcp
7
+ :: Resolves the correct venv binary for Windows and joins the namespace daemon
8
+ :: before opening MCP, preserving one writer for parallel Claude sessions.
9
9
  ::
10
10
  :: On Windows, Python venv places entry points in Scripts\ (not bin\ like POSIX).
11
11
  :: This launcher bridges the path difference so ONE .mcp.json command field works
@@ -14,4 +14,10 @@
14
14
  :: Environment:
15
15
  :: CLAUDE_PLUGIN_DATA — persistent data dir where venv lives
16
16
 
17
+ "%CLAUDE_PLUGIN_DATA%\venv\Scripts\slm.exe" serve start 1>&2
18
+ if errorlevel 1 (
19
+ echo SLM plugin: unable to start the owned daemon; refusing a direct MCP writer. 1>&2
20
+ exit /b 1
21
+ )
22
+
17
23
  "%CLAUDE_PLUGIN_DATA%\venv\Scripts\slm.exe" mcp
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "superlocalmemory"
3
- version = "3.7.1"
3
+ version = "3.7.3"
4
4
  description = "Local-first agent memory with auditable hybrid retrieval"
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-or-later"
@@ -32,7 +32,7 @@ if "OMP_NUM_THREADS" not in os.environ:
32
32
  os.environ["OMP_NUM_THREADS"] = "2"
33
33
  # ---------------------------------------------------------------------------
34
34
 
35
- __version__ = "3.7.1"
35
+ __version__ = "3.7.3"
36
36
 
37
37
  _REQUIRED_VERSIONS = {
38
38
  "sentence_transformers": "5.3.0",
@@ -270,8 +270,8 @@ def main() -> None:
270
270
  )
271
271
  db_scale_p.add_argument(
272
272
  "scale_action",
273
- choices=("status", "prepare", "verify", "promote", "rollback"),
274
- help="Lifecycle action. Prepare and verify never mutate active projection paths.",
273
+ choices=("status", "adopt", "prepare", "verify", "promote", "rollback"),
274
+ help="Lifecycle action. Adopt confirms a detected legacy projection; prepare and verify never mutate active paths.",
275
275
  )
276
276
  db_scale_p.add_argument("--stage-id", help="Stage identifier required by verify/promote")
277
277
  db_scale_p.add_argument("--backup-id", help="Backup identifier required by rollback")
@@ -9,11 +9,23 @@ def cmd_db_scale(args: Namespace) -> int:
9
9
  from superlocalmemory.core.config import SLMConfig
10
10
  from superlocalmemory.core.scale_engine import ScaleEngineError, ScaleEngineManager
11
11
 
12
- manager = ScaleEngineManager(SLMConfig.load())
13
12
  action = args.scale_action
13
+ # Scale Engine projections are currently canonical-default-profile data.
14
+ # Adoption must not fail merely because the operator is working in another
15
+ # profile, and it must not rewrite that selected profile.
16
+ manager = ScaleEngineManager(
17
+ SLMConfig.load(), profile_id="default" if action == "adopt" else None
18
+ )
14
19
  try:
15
20
  if action == "status":
16
21
  result = manager.status()
22
+ elif action == "adopt":
23
+ result = manager.adopt_legacy_projection()
24
+ if result is None:
25
+ raise ScaleEngineError(
26
+ "no confirmed legacy projection candidate; see `slm db scale status`"
27
+ )
28
+ result = {**result, "restart_required": True}
17
29
  elif action == "prepare":
18
30
  result = manager.prepare()
19
31
  elif action == "verify":
@@ -97,6 +97,8 @@ class BackendOrchestrator:
97
97
  except Exception as exc:
98
98
  logger.warning("Initial rebalance failed (non-fatal): %s", exc)
99
99
 
100
+ self._recover_interrupted_scale_promotion()
101
+
100
102
  # Backends may be installed with the product, but installing a wheel
101
103
  # is not authorization to mutate an existing data root. Only a
102
104
  # verified, explicit promotion may initialize and migrate projections.
@@ -134,6 +136,19 @@ class BackendOrchestrator:
134
136
  "active" if self._cozo and self._cozo_status() == "active" else "off",
135
137
  "active" if self._lancedb and self._lancedb_status() == "active" else "off")
136
138
 
139
+ def _recover_interrupted_scale_promotion(self) -> None:
140
+ """Repair an interrupted promotion; never auto-mutate a legacy root."""
141
+ try:
142
+ from superlocalmemory.core.scale_engine import ScaleEngineManager
143
+
144
+ result = ScaleEngineManager(self._config, profile_id="default").recover_interrupted_promotion()
145
+ if result:
146
+ logger.warning("Scale Engine promotion recovery: %s", result)
147
+ except Exception as exc:
148
+ # A scale projection is derived data. Startup must keep serving
149
+ # canonical SQLite even if optional recovery itself is unhealthy.
150
+ logger.error("Scale Engine recovery requires repair; Local Core remains active: %s", exc)
151
+
137
152
  # ------------------------------------------------------------------
138
153
  # Incremental Sync (F-04: called from store_pipeline)
139
154
  # ------------------------------------------------------------------
@@ -70,7 +70,13 @@ def _load_embedding_model(name: str) -> tuple:
70
70
 
71
71
  Returns (model, backend_name) or (None, "").
72
72
  """
73
- from sentence_transformers import SentenceTransformer
73
+ try:
74
+ from sentence_transformers import SentenceTransformer
75
+ except Exception:
76
+ # Dependency/version errors must stay inside the JSON-lines protocol.
77
+ # Letting this import escape kills the worker before it can explain the
78
+ # failure, which the parent previously mislabeled as a long timeout.
79
+ return None, ""
74
80
 
75
81
  for backend in _embedding_backend_order():
76
82
  try:
@@ -295,6 +295,8 @@ class EmbeddingService:
295
295
  never hangs indefinitely on cold model loads or network issues.
296
296
  """
297
297
  with self._lock:
298
+ if not self._available:
299
+ return None
298
300
  # Worker recycling: restart after N requests to prevent
299
301
  # C++ allocator fragmentation over long-running sessions.
300
302
  if self._request_count >= _WORKER_RECYCLE_AFTER and self._worker_proc is not None:
@@ -339,6 +341,12 @@ class EmbeddingService:
339
341
  resp = json.loads(resp_line)
340
342
  if not resp.get("ok"):
341
343
  logger.warning("Worker error: %s", resp.get("error"))
344
+ # A well-formed worker error is a terminal local
345
+ # dependency/model failure, not a transient pipe race.
346
+ # Disable this service and terminate the child so every
347
+ # recall does not respawn a heavyweight failing process.
348
+ self._available = False
349
+ self._kill_worker()
342
350
  return None
343
351
  self._reset_idle_timer()
344
352
  self._request_count += 1
@@ -24,6 +24,7 @@ from typing import Any
24
24
  from superlocalmemory.core.config import CANONICAL_RECALL_LIMIT, SLMConfig
25
25
  from superlocalmemory.core.engine_capabilities import Capabilities, CapabilityError
26
26
  from superlocalmemory.core.modes import get_capabilities
27
+ from superlocalmemory.learning.outcome_queue import RecallEvent, enqueue_recall
27
28
  from superlocalmemory.storage.models import (
28
29
  AtomicFact, MemoryRecord, Mode, RecallResponse,
29
30
  )
@@ -664,9 +665,6 @@ class MemoryEngine:
664
665
  # recall correctness (LLD-02 §4.9, LLD-08 §4.1).
665
666
  if session_id:
666
667
  try:
667
- from superlocalmemory.learning.outcome_queue import (
668
- RecallEvent, enqueue_recall,
669
- )
670
668
  fact_ids = tuple(
671
669
  getattr(r.fact, "fact_id", "") or ""
672
670
  for r in getattr(response, "results", [])
@@ -388,11 +388,6 @@ def _init_spreading_activation(
388
388
  vector_store: Any,
389
389
  ) -> Any | None:
390
390
  """Create SpreadingActivation for Phase 3 5th retrieval channel."""
391
- # V3.3.21: Guard against None vector_store. Without embeddings, SA's
392
- # search() crashes with "'NoneType' has no attribute 'search'".
393
- if vector_store is None:
394
- logger.debug("SpreadingActivation skipped: no vector_store")
395
- return None
396
391
  try:
397
392
  from superlocalmemory.retrieval.spreading_activation import (
398
393
  SpreadingActivation,
@@ -15,12 +15,13 @@ Design notes:
15
15
  available before we even try — better to defer than thrash.
16
16
  - Lock file lives at ``~/.superlocalmemory/ram_lock.sem`` by default,
17
17
  tests may monkeypatch ``RAM_LOCK_PATH``.
18
- - POSIX only; Windows build is deferred per LLD-00 §7 and MASTER-PLAN H-01.
18
+ - Uses a standard-library advisory lock on POSIX and Windows. The lock is
19
+ deliberately file-backed so independent daemon, CLI, and worker processes
20
+ share the same RAM reservation.
19
21
  """
20
22
 
21
23
  from __future__ import annotations
22
24
 
23
- import fcntl
24
25
  import os
25
26
  import time
26
27
  from contextlib import contextmanager
@@ -29,6 +30,11 @@ from typing import Iterator
29
30
 
30
31
  import psutil
31
32
 
33
+ try: # POSIX advisory locks (macOS/Linux)
34
+ import fcntl as _fcntl
35
+ except ImportError: # Windows uses the stdlib byte-range equivalent below.
36
+ _fcntl = None
37
+
32
38
  # Public override retained for tests and embedders. ``None`` means resolve the
33
39
  # lock dynamically so a long-lived process can switch canonical namespaces
34
40
  # without retaining the first root it observed.
@@ -44,6 +50,38 @@ def _ram_lock_path() -> Path:
44
50
  return state_path("ram_lock.sem")
45
51
 
46
52
 
53
+ def _acquire_lock(fd: int) -> None:
54
+ """Acquire one non-blocking cross-process lock for the semaphore."""
55
+ if _fcntl is not None:
56
+ _fcntl.flock(fd, _fcntl.LOCK_EX | _fcntl.LOCK_NB)
57
+ return
58
+
59
+ import msvcrt
60
+
61
+ # ``msvcrt.locking`` cannot reserve an empty file. The marker byte is
62
+ # purely structural; the human-readable audit marker is written after the
63
+ # reservation is acquired.
64
+ if os.fstat(fd).st_size == 0:
65
+ os.write(fd, b"\0")
66
+ os.lseek(fd, 0, os.SEEK_SET)
67
+ try:
68
+ msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
69
+ except OSError as exc:
70
+ raise BlockingIOError("RAM reservation is already held") from exc
71
+
72
+
73
+ def _release_lock(fd: int) -> None:
74
+ """Release a reservation acquired by :func:`_acquire_lock`."""
75
+ if _fcntl is not None:
76
+ _fcntl.flock(fd, _fcntl.LOCK_UN)
77
+ return
78
+
79
+ import msvcrt
80
+
81
+ os.lseek(fd, 0, os.SEEK_SET)
82
+ msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
83
+
84
+
47
85
  @contextmanager
48
86
  def ram_reservation(
49
87
  name: str,
@@ -96,7 +134,7 @@ def ram_reservation(
96
134
  deadline = time.time() + timeout_s
97
135
  while True:
98
136
  try:
99
- fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
137
+ _acquire_lock(fd)
100
138
  break
101
139
  except BlockingIOError:
102
140
  if time.time() >= deadline:
@@ -114,7 +152,7 @@ def ram_reservation(
114
152
  yield
115
153
  finally:
116
154
  try:
117
- fcntl.flock(fd, fcntl.LOCK_UN)
155
+ _release_lock(fd)
118
156
  finally:
119
157
  os.close(fd)
120
158