superlocalmemory 3.8.9 → 3.8.10
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.
- package/CHANGELOG.md +29 -0
- package/README.md +3 -3
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/daemon.py +89 -16
- package/src/superlocalmemory/core/embeddings.py +35 -4
- package/src/superlocalmemory/core/ollama_embedder.py +11 -2
- package/src/superlocalmemory/core/remember_admission.py +14 -5
- package/src/superlocalmemory/hooks/adapter_base.py +10 -3
- package/src/superlocalmemory/mcp/tools_core.py +16 -5
- package/src/superlocalmemory/optimize/proxy/capture.py +148 -30
- package/src/superlocalmemory/optimize/storage/db.py +6 -2
- package/src/superlocalmemory/server/unified_daemon.py +8 -1
- package/src/superlocalmemory/storage/admission_codec.py +10 -0
- package/src/superlocalmemory/storage/admission_journal.py +182 -67
- package/src/superlocalmemory/storage/embedding_migrator.py +27 -13
- package/src/superlocalmemory/storage/write_coordinator.py +68 -21
|
@@ -412,29 +412,43 @@ def backfill_missing_embeddings(
|
|
|
412
412
|
continue
|
|
413
413
|
try:
|
|
414
414
|
embedding_json = json.dumps(vec)
|
|
415
|
-
# Mirror run_embedding_migration's write path exactly.
|
|
416
|
-
db.execute(
|
|
417
|
-
"UPDATE atomic_facts SET embedding = ? WHERE fact_id = ?",
|
|
418
|
-
(embedding_json, fid),
|
|
419
|
-
)
|
|
420
415
|
# Metadata is not an independent record: it is the pointer to
|
|
421
416
|
# a sqlite-vec row. Creating it before the vector payload leaves
|
|
422
417
|
# semantic recall permanently blind while reporting success.
|
|
423
418
|
# VectorStore owns the atomic pair and repairs legacy orphans.
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
and not vector_store.upsert(
|
|
419
|
+
projection_written = False
|
|
420
|
+
if vector_store is not None and getattr(vector_store, "available", False):
|
|
421
|
+
projection_written = vector_store.upsert(
|
|
428
422
|
fid,
|
|
429
423
|
pid,
|
|
430
424
|
vec,
|
|
431
425
|
model_name=current_model,
|
|
432
426
|
)
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
427
|
+
if not projection_written:
|
|
428
|
+
# Keep the canonical embedding NULL so the next
|
|
429
|
+
# bounded self-heal pass retries this fact. The old
|
|
430
|
+
# order wrote JSON first, permanently removed the fact
|
|
431
|
+
# from the backfill query, and silently stranded recall
|
|
432
|
+
# without its sqlite-vec projection.
|
|
433
|
+
logger.warning(
|
|
434
|
+
"backfill: vector projection failed for fact %s; "
|
|
435
|
+
"leaving it pending for retry",
|
|
436
|
+
fid[:16],
|
|
437
|
+
)
|
|
438
|
+
continue
|
|
439
|
+
|
|
440
|
+
try:
|
|
441
|
+
# Publish the canonical JSON only after the derived vector
|
|
442
|
+
# projection is durable. When sqlite-vec is unavailable,
|
|
443
|
+
# this remains the supported JSON-only fallback path.
|
|
444
|
+
db.execute(
|
|
445
|
+
"UPDATE atomic_facts SET embedding = ? WHERE fact_id = ?",
|
|
446
|
+
(embedding_json, fid),
|
|
437
447
|
)
|
|
448
|
+
except Exception:
|
|
449
|
+
if projection_written and vector_store is not None:
|
|
450
|
+
vector_store.delete(fid)
|
|
451
|
+
raise
|
|
438
452
|
embedded += 1
|
|
439
453
|
# Cooperative yield: release db._lock briefly so concurrent
|
|
440
454
|
# user writes can acquire it between facts. Without this,
|
|
@@ -223,6 +223,7 @@ class _Execution:
|
|
|
223
223
|
result: WriteResult | None = None
|
|
224
224
|
error: BaseException | None = None
|
|
225
225
|
cancelled: bool = False
|
|
226
|
+
commit_started: bool = False
|
|
226
227
|
|
|
227
228
|
|
|
228
229
|
class WriteCoordinator:
|
|
@@ -432,11 +433,7 @@ class WriteCoordinator:
|
|
|
432
433
|
deadline=time.monotonic() + timeout,
|
|
433
434
|
)
|
|
434
435
|
self._enqueue(item)
|
|
435
|
-
|
|
436
|
-
if not item.completion.wait(remaining):
|
|
437
|
-
with self._condition:
|
|
438
|
-
item.cancelled = True
|
|
439
|
-
raise WriteDeadlineExceededError("canonical write exceeded its caller deadline")
|
|
436
|
+
self._wait_for_completion(item)
|
|
440
437
|
if item.error is not None:
|
|
441
438
|
raise item.error
|
|
442
439
|
return item.rows or []
|
|
@@ -480,17 +477,31 @@ class WriteCoordinator:
|
|
|
480
477
|
command=command,
|
|
481
478
|
)
|
|
482
479
|
self._enqueue(item)
|
|
483
|
-
|
|
484
|
-
if not item.completion.wait(remaining):
|
|
485
|
-
with self._condition:
|
|
486
|
-
item.cancelled = True
|
|
487
|
-
raise WriteDeadlineExceededError("canonical write exceeded its caller deadline")
|
|
480
|
+
self._wait_for_completion(item)
|
|
488
481
|
if item.error is not None:
|
|
489
482
|
raise item.error
|
|
490
483
|
if item.result is None: # pragma: no cover - defensive worker invariant
|
|
491
484
|
raise WriteCoordinatorError("canonical command completed without a receipt")
|
|
492
485
|
return item.result
|
|
493
486
|
|
|
487
|
+
def _wait_for_completion(self, item: _Execution) -> None:
|
|
488
|
+
"""Cancel before commit or wait through an already-linearized commit."""
|
|
489
|
+
remaining = max(0.0, item.deadline - time.monotonic())
|
|
490
|
+
if item.completion.wait(remaining):
|
|
491
|
+
return
|
|
492
|
+
with self._condition:
|
|
493
|
+
if item.completion.is_set():
|
|
494
|
+
return
|
|
495
|
+
if not item.commit_started:
|
|
496
|
+
item.cancelled = True
|
|
497
|
+
raise WriteDeadlineExceededError(
|
|
498
|
+
"canonical write exceeded its caller deadline"
|
|
499
|
+
)
|
|
500
|
+
# Commit is the linearization point. Once it starts, return its real
|
|
501
|
+
# result instead of reporting an ambiguous timeout followed by a
|
|
502
|
+
# durable mutation.
|
|
503
|
+
item.completion.wait()
|
|
504
|
+
|
|
494
505
|
def _enqueue(self, item: _Execution) -> None:
|
|
495
506
|
with self._condition:
|
|
496
507
|
if self._stopping:
|
|
@@ -562,19 +573,54 @@ class WriteCoordinator:
|
|
|
562
573
|
item.error = WriteDeadlineExceededError("canonical write expired before execution")
|
|
563
574
|
item.completion.set()
|
|
564
575
|
return
|
|
576
|
+
remaining = max(0.0, item.deadline - time.monotonic())
|
|
577
|
+
if not self._process_write_lock.acquire(timeout=remaining):
|
|
578
|
+
item.error = WriteDeadlineExceededError(
|
|
579
|
+
"canonical write expired waiting for its process lock"
|
|
580
|
+
)
|
|
581
|
+
item.completion.set()
|
|
582
|
+
return
|
|
565
583
|
try:
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
584
|
+
remaining = item.deadline - time.monotonic()
|
|
585
|
+
if remaining <= 0:
|
|
586
|
+
item.error = WriteDeadlineExceededError(
|
|
587
|
+
"canonical write expired after waiting for its process lock"
|
|
588
|
+
)
|
|
589
|
+
return
|
|
590
|
+
remaining_ms = max(
|
|
591
|
+
1,
|
|
592
|
+
int(remaining * 1_000),
|
|
593
|
+
)
|
|
594
|
+
conn.execute(f"PRAGMA busy_timeout={remaining_ms}")
|
|
595
|
+
synchronous = "FULL" if item.lane is Lane.FOREGROUND else "NORMAL"
|
|
596
|
+
conn.execute(f"PRAGMA synchronous={synchronous}")
|
|
597
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
598
|
+
with self._condition:
|
|
599
|
+
if item.cancelled or time.monotonic() >= item.deadline:
|
|
600
|
+
item.error = WriteDeadlineExceededError(
|
|
601
|
+
"canonical write expired before its mutation"
|
|
602
|
+
)
|
|
603
|
+
if item.error is not None:
|
|
604
|
+
conn.rollback()
|
|
605
|
+
return
|
|
606
|
+
if item.command is not None:
|
|
607
|
+
item.result = self._execute_command(conn, item.command)
|
|
608
|
+
else:
|
|
609
|
+
if item.sql is None: # pragma: no cover - execution invariant
|
|
610
|
+
raise WriteCoordinatorError("missing coordinator SQL command")
|
|
611
|
+
cursor = conn.execute(item.sql, item.parameters)
|
|
612
|
+
item.rows = cursor.fetchall()
|
|
613
|
+
with self._condition:
|
|
614
|
+
if item.cancelled or time.monotonic() >= item.deadline:
|
|
615
|
+
item.error = WriteDeadlineExceededError(
|
|
616
|
+
"canonical write expired during its mutation"
|
|
617
|
+
)
|
|
572
618
|
else:
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
619
|
+
item.commit_started = True
|
|
620
|
+
if item.error is not None:
|
|
621
|
+
conn.rollback()
|
|
622
|
+
return
|
|
623
|
+
conn.commit()
|
|
578
624
|
except WriteCoordinatorError as exc:
|
|
579
625
|
conn.rollback()
|
|
580
626
|
item.error = exc
|
|
@@ -590,6 +636,7 @@ class WriteCoordinator:
|
|
|
590
636
|
item.error = WriteCoordinatorError("canonical write command failed")
|
|
591
637
|
item.error.__cause__ = exc
|
|
592
638
|
finally:
|
|
639
|
+
self._process_write_lock.release()
|
|
593
640
|
item.completion.set()
|
|
594
641
|
|
|
595
642
|
def _execute_command(self, conn: sqlite3.Connection, command: WriteCommand) -> WriteResult:
|