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.
Files changed (45) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +3 -3
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/CLAUDE.md +3 -3
  6. package/plugin/agents/slm-governance-advisor.md +1 -1
  7. package/plugin/agents/slm-loop-runner.md +1 -1
  8. package/plugin/agents/slm-memory-advisor.md +1 -1
  9. package/plugin/agents/slm-optimize-advisor.md +1 -1
  10. package/plugin/requirements.txt +1 -1
  11. package/plugin/skills/slm-cache/SKILL.md +1 -1
  12. package/plugin/skills/slm-compress/SKILL.md +1 -1
  13. package/plugin/skills/slm-governance/SKILL.md +1 -1
  14. package/plugin/skills/slm-graph/SKILL.md +1 -1
  15. package/plugin/skills/slm-loop/SKILL.md +1 -1
  16. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  17. package/plugin/skills/slm-profile/SKILL.md +1 -1
  18. package/plugin/skills/slm-recall/SKILL.md +1 -1
  19. package/plugin/skills/slm-remember/SKILL.md +1 -1
  20. package/plugin/skills/slm-scope/SKILL.md +1 -1
  21. package/plugin/skills/slm-session/SKILL.md +1 -1
  22. package/plugin/skills/slm-status/SKILL.md +1 -1
  23. package/plugin-src/rules/AGENTS.md +1 -1
  24. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  25. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  26. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  31. package/pyproject.toml +1 -1
  32. package/src/superlocalmemory/__init__.py +1 -1
  33. package/src/superlocalmemory/cli/daemon.py +89 -16
  34. package/src/superlocalmemory/core/embeddings.py +35 -4
  35. package/src/superlocalmemory/core/ollama_embedder.py +11 -2
  36. package/src/superlocalmemory/core/remember_admission.py +14 -5
  37. package/src/superlocalmemory/hooks/adapter_base.py +10 -3
  38. package/src/superlocalmemory/mcp/tools_core.py +16 -5
  39. package/src/superlocalmemory/optimize/proxy/capture.py +148 -30
  40. package/src/superlocalmemory/optimize/storage/db.py +6 -2
  41. package/src/superlocalmemory/server/unified_daemon.py +8 -1
  42. package/src/superlocalmemory/storage/admission_codec.py +10 -0
  43. package/src/superlocalmemory/storage/admission_journal.py +182 -67
  44. package/src/superlocalmemory/storage/embedding_migrator.py +27 -13
  45. 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
- if (
425
- vector_store is not None
426
- and getattr(vector_store, "available", False)
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
- logger.warning(
435
- "backfill: vector projection failed for fact %s",
436
- fid[:16],
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
- remaining = max(0.0, item.deadline - time.monotonic())
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
- remaining = max(0.0, item.deadline - time.monotonic())
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
- with self._process_write_lock:
567
- synchronous = "FULL" if item.lane is Lane.FOREGROUND else "NORMAL"
568
- conn.execute(f"PRAGMA synchronous={synchronous}")
569
- conn.execute("BEGIN IMMEDIATE")
570
- if item.command is not None:
571
- item.result = self._execute_command(conn, item.command)
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
- if item.sql is None: # pragma: no cover - execution invariant
574
- raise WriteCoordinatorError("missing coordinator SQL command")
575
- cursor = conn.execute(item.sql, item.parameters)
576
- item.rows = cursor.fetchall()
577
- conn.commit()
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: