superlocalmemory 3.8.0 → 3.8.1
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 +60 -0
- package/README.md +32 -120
- package/package.json +9 -2
- package/plugin/.claude-plugin/plugin.json +1 -2
- 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 +2 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +134 -7
- package/src/superlocalmemory/cli/daemon.py +7 -0
- package/src/superlocalmemory/cli/loop_cmd.py +2 -7
- package/src/superlocalmemory/cli/main.py +10 -4
- package/src/superlocalmemory/cli/version_banner.py +17 -3
- package/src/superlocalmemory/core/backend_orchestrator.py +18 -16
- package/src/superlocalmemory/core/embedding_worker.py +4 -5
- package/src/superlocalmemory/core/embeddings.py +119 -45
- package/src/superlocalmemory/core/engine.py +24 -21
- package/src/superlocalmemory/core/engine_ingestion.py +332 -45
- package/src/superlocalmemory/core/ingestion_command.py +134 -25
- package/src/superlocalmemory/core/injection.py +12 -7
- package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
- package/src/superlocalmemory/core/recall_pipeline.py +5 -3
- package/src/superlocalmemory/core/store_pipeline.py +153 -20
- package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
- package/src/superlocalmemory/hooks/portable_kit.py +34 -2
- package/src/superlocalmemory/learning/model_rollback.py +3 -0
- package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
- package/src/superlocalmemory/learning/reward.py +50 -0
- package/src/superlocalmemory/learning/source_quality.py +523 -1
- package/src/superlocalmemory/loops/ledger.py +25 -5
- package/src/superlocalmemory/mcp/server.py +11 -30
- package/src/superlocalmemory/mcp/tools_core.py +4 -3
- package/src/superlocalmemory/mcp/tools_learning.py +2 -2
- package/src/superlocalmemory/retrieval/engine.py +53 -21
- package/src/superlocalmemory/retrieval/reranker.py +3 -4
- package/src/superlocalmemory/server/config_file.py +90 -0
- package/src/superlocalmemory/server/origin.py +50 -0
- package/src/superlocalmemory/server/routes/backup.py +293 -70
- package/src/superlocalmemory/server/routes/behavioral.py +336 -59
- package/src/superlocalmemory/server/routes/brain.py +57 -16
- package/src/superlocalmemory/server/routes/config_api.py +84 -82
- package/src/superlocalmemory/server/routes/entity.py +100 -23
- package/src/superlocalmemory/server/routes/evolution.py +103 -100
- package/src/superlocalmemory/server/routes/learning.py +276 -105
- package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
- package/src/superlocalmemory/server/routes/mesh.py +121 -32
- package/src/superlocalmemory/server/routes/ratelimit.py +33 -25
- package/src/superlocalmemory/server/routes/stats.py +93 -155
- package/src/superlocalmemory/server/routes/token.py +3 -13
- package/src/superlocalmemory/server/routes/v3_api.py +64 -20
- package/src/superlocalmemory/server/unified_daemon.py +467 -40
- package/src/superlocalmemory/storage/migration_runner.py +79 -1
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
- package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +270 -0
- package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
- package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
- package/src/superlocalmemory/storage/schema.py +49 -1
- package/src/superlocalmemory/storage/schema_v32.py +2 -0
- package/src/superlocalmemory/storage/schema_v347.py +4 -0
- package/src/superlocalmemory/ui/index.html +3 -6
- package/src/superlocalmemory/ui/js/core.js +52 -9
- package/src/superlocalmemory/ui/js/dashboard.js +169 -82
- package/src/superlocalmemory/ui/js/od-backup.js +156 -65
- package/src/superlocalmemory/ui/js/od-brain.js +88 -51
- package/src/superlocalmemory/ui/js/od-entities.js +22 -22
- package/src/superlocalmemory/ui/js/od-graph.js +11 -4
- package/src/superlocalmemory/ui/js/od-memories.js +47 -5
- package/src/superlocalmemory/ui/js/od-mesh.js +23 -9
- package/src/superlocalmemory/ui/js/od-settings.js +113 -59
- package/src/superlocalmemory/ui/js/od-shell.js +249 -33
- package/src/superlocalmemory/ui/js/od-skills.js +44 -17
- package/src/superlocalmemory/ui/js/settings.js +15 -1
- package/plugin-src/.mcp.json +0 -12
- package/plugin-src/agents/slm-governance-advisor.md +0 -80
- package/plugin-src/agents/slm-loop-runner.md +0 -71
- package/plugin-src/agents/slm-memory-advisor.md +0 -49
- package/plugin-src/agents/slm-optimize-advisor.md +0 -44
- package/plugin-src/commands/slm-loop.md +0 -31
- package/plugin-src/hooks/.gitkeep +0 -0
- package/plugin-src/hooks/hooks.json +0 -102
- package/plugin-src/manifest.json +0 -30
- package/plugin-src/requirements.txt +0 -1
- package/plugin-src/rules/CLAUDE.md.fragment +0 -44
- package/plugin-src/scripts/ensure-venv.bat +0 -122
- package/plugin-src/scripts/ensure-venv.sh +0 -105
- package/plugin-src/scripts/slm-launch +0 -62
- package/plugin-src/scripts/slm-launch.bat +0 -23
- package/plugin-src/settings.json +0 -25
- package/plugin-src/skills/slm-governance/SKILL.md +0 -248
- package/plugin-src/skills/slm-loop/SKILL.md +0 -99
- package/plugin-src/skills/slm-mesh/SKILL.md +0 -282
- package/plugin-src/skills/slm-profile/SKILL.md +0 -148
- package/plugin-src/skills/slm-scope/SKILL.md +0 -176
|
@@ -24,6 +24,7 @@ from typing import Any, Callable
|
|
|
24
24
|
from superlocalmemory.storage.database import DatabaseManager
|
|
25
25
|
|
|
26
26
|
_MATERIALIZATION_LOCKS = tuple(threading.RLock() for _ in range(64))
|
|
27
|
+
_MAX_AUTOMATIC_MATERIALIZATION_ATTEMPTS = 10
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
def _materialization_lock(operation_id: str) -> threading.RLock:
|
|
@@ -51,6 +52,10 @@ class OperationInProgress(RuntimeError):
|
|
|
51
52
|
"""Another live lease owner is materializing this operation."""
|
|
52
53
|
|
|
53
54
|
|
|
55
|
+
class LeaseLost(OperationInProgress):
|
|
56
|
+
"""The materializer no longer owns its durable operation lease."""
|
|
57
|
+
|
|
58
|
+
|
|
54
59
|
def _canonical_json(value: Any) -> str:
|
|
55
60
|
return json.dumps(value, sort_keys=True, separators=(",", ":"))
|
|
56
61
|
|
|
@@ -284,12 +289,19 @@ class IngestionOperationRepository:
|
|
|
284
289
|
self._from_row(row)
|
|
285
290
|
for row in self.db.execute(
|
|
286
291
|
"SELECT * FROM ingestion_operations "
|
|
287
|
-
"WHERE
|
|
292
|
+
"WHERE attempt_count < ? AND ("
|
|
293
|
+
"(state='queryable' AND created_at <= "
|
|
288
294
|
"strftime('%Y-%m-%dT%H:%M:%fZ', 'now', ?)) "
|
|
289
295
|
"OR (state='failed' AND next_retry_at <= ?) "
|
|
290
|
-
"OR (state='enriching' AND lease_expires_at <= ?) "
|
|
296
|
+
"OR (state='enriching' AND lease_expires_at <= ?)) "
|
|
291
297
|
"ORDER BY created_at, rowid LIMIT ?",
|
|
292
|
-
(
|
|
298
|
+
(
|
|
299
|
+
_MAX_AUTOMATIC_MATERIALIZATION_ATTEMPTS,
|
|
300
|
+
grace_modifier,
|
|
301
|
+
now,
|
|
302
|
+
now,
|
|
303
|
+
max(1, int(limit)),
|
|
304
|
+
),
|
|
293
305
|
)
|
|
294
306
|
]
|
|
295
307
|
|
|
@@ -415,6 +427,27 @@ class IngestionOperationRepository:
|
|
|
415
427
|
)
|
|
416
428
|
return operation
|
|
417
429
|
|
|
430
|
+
def renew_enriching_lease(
|
|
431
|
+
self,
|
|
432
|
+
operation_id: str,
|
|
433
|
+
*,
|
|
434
|
+
owner: str,
|
|
435
|
+
lease_seconds: float,
|
|
436
|
+
) -> bool:
|
|
437
|
+
"""Extend a live lease only while the same owner still holds it."""
|
|
438
|
+
rows = self.db.execute(
|
|
439
|
+
"UPDATE ingestion_operations SET lease_expires_at=?, "
|
|
440
|
+
"updated_at=strftime('%Y-%m-%dT%H:%M:%fZ', 'now') "
|
|
441
|
+
"WHERE operation_id=? AND state='enriching' AND lease_owner=? "
|
|
442
|
+
"RETURNING operation_id",
|
|
443
|
+
(
|
|
444
|
+
time.time() + max(1.0, float(lease_seconds)),
|
|
445
|
+
operation_id,
|
|
446
|
+
owner,
|
|
447
|
+
),
|
|
448
|
+
)
|
|
449
|
+
return bool(rows)
|
|
450
|
+
|
|
418
451
|
def finish_enriching(
|
|
419
452
|
self,
|
|
420
453
|
operation_id: str,
|
|
@@ -469,6 +502,7 @@ class MaterializationResult:
|
|
|
469
502
|
|
|
470
503
|
fact_ids: tuple[str, ...]
|
|
471
504
|
derivation_state: dict[str, bool]
|
|
505
|
+
last_error: str = ""
|
|
472
506
|
|
|
473
507
|
|
|
474
508
|
QueryableWriter = Callable[[IngestionRequest, str], list[str]]
|
|
@@ -500,6 +534,47 @@ class IngestionCommand:
|
|
|
500
534
|
self._lease_seconds = max(1.0, float(lease_seconds))
|
|
501
535
|
self._owner = f"ingestion-worker:{uuid.uuid4().hex}"
|
|
502
536
|
|
|
537
|
+
def _run_with_lease_heartbeat(
|
|
538
|
+
self,
|
|
539
|
+
operation_id: str,
|
|
540
|
+
callback: Callable[[], Any],
|
|
541
|
+
) -> Any:
|
|
542
|
+
"""Run slow work while periodically renewing its owner-bound lease."""
|
|
543
|
+
stop = threading.Event()
|
|
544
|
+
lost = threading.Event()
|
|
545
|
+
interval = min(30.0, max(0.1, self._lease_seconds / 3.0))
|
|
546
|
+
|
|
547
|
+
def heartbeat() -> None:
|
|
548
|
+
while not stop.wait(interval):
|
|
549
|
+
try:
|
|
550
|
+
renewed = self.repository.renew_enriching_lease(
|
|
551
|
+
operation_id,
|
|
552
|
+
owner=self._owner,
|
|
553
|
+
lease_seconds=self._lease_seconds,
|
|
554
|
+
)
|
|
555
|
+
except sqlite3.Error:
|
|
556
|
+
continue
|
|
557
|
+
if not renewed:
|
|
558
|
+
lost.set()
|
|
559
|
+
return
|
|
560
|
+
|
|
561
|
+
thread = threading.Thread(
|
|
562
|
+
target=heartbeat,
|
|
563
|
+
name=f"ingestion-lease-heartbeat:{operation_id[:8]}",
|
|
564
|
+
daemon=True,
|
|
565
|
+
)
|
|
566
|
+
thread.start()
|
|
567
|
+
try:
|
|
568
|
+
result = callback()
|
|
569
|
+
finally:
|
|
570
|
+
stop.set()
|
|
571
|
+
thread.join(timeout=max(1.0, interval * 2))
|
|
572
|
+
if lost.is_set():
|
|
573
|
+
raise LeaseLost(
|
|
574
|
+
f"ingestion lease lost for operation {operation_id}"
|
|
575
|
+
)
|
|
576
|
+
return result
|
|
577
|
+
|
|
503
578
|
def submit(self, request: IngestionRequest) -> IngestionOperation:
|
|
504
579
|
operation, _created = self.submit_with_status(request)
|
|
505
580
|
return operation
|
|
@@ -548,27 +623,41 @@ class IngestionCommand:
|
|
|
548
623
|
)
|
|
549
624
|
if enriching.state is IngestionState.COMPLETE:
|
|
550
625
|
return enriching
|
|
551
|
-
if
|
|
626
|
+
if (
|
|
627
|
+
enriching.final_fact_ids
|
|
628
|
+
and all(enriching.derivation_state.values())
|
|
629
|
+
):
|
|
552
630
|
return self._project_and_complete(enriching)
|
|
553
631
|
try:
|
|
632
|
+
# Materialization is a durable saga, not one long SQLite
|
|
633
|
+
# transaction. Extractors, embedders, and local model calls can
|
|
634
|
+
# take minutes on a mature installation; keeping a write
|
|
635
|
+
# transaction open across that work blocks every interactive
|
|
636
|
+
# remember/update/delete and makes the dashboard appear dead.
|
|
637
|
+
#
|
|
638
|
+
# The materializer commits its relational checkpoints in short
|
|
639
|
+
# database operations. The operation lease and derivation state
|
|
640
|
+
# remain the recovery boundary, and only the final state-machine
|
|
641
|
+
# checkpoint is grouped atomically below.
|
|
642
|
+
materialized = self._run_with_lease_heartbeat(
|
|
643
|
+
operation_id,
|
|
644
|
+
lambda: self._materializer(enriching),
|
|
645
|
+
)
|
|
646
|
+
if isinstance(materialized, MaterializationResult):
|
|
647
|
+
fact_ids = tuple(materialized.fact_ids)
|
|
648
|
+
derivation_state = dict(materialized.derivation_state)
|
|
649
|
+
materialization_error = materialized.last_error
|
|
650
|
+
else:
|
|
651
|
+
fact_ids = tuple(materialized)
|
|
652
|
+
derivation_state = {"materializer": True}
|
|
653
|
+
materialization_error = ""
|
|
654
|
+
if not fact_ids:
|
|
655
|
+
raise RuntimeError("materialization produced no final facts")
|
|
656
|
+
incomplete = sorted(
|
|
657
|
+
name for name, complete in derivation_state.items()
|
|
658
|
+
if not complete
|
|
659
|
+
)
|
|
554
660
|
with self.repository.db.transaction():
|
|
555
|
-
materialized = self._materializer(enriching)
|
|
556
|
-
if isinstance(materialized, MaterializationResult):
|
|
557
|
-
fact_ids = tuple(materialized.fact_ids)
|
|
558
|
-
derivation_state = dict(materialized.derivation_state)
|
|
559
|
-
else:
|
|
560
|
-
fact_ids = tuple(materialized)
|
|
561
|
-
derivation_state = {"materializer": True}
|
|
562
|
-
if not fact_ids:
|
|
563
|
-
raise RuntimeError("materialization produced no final facts")
|
|
564
|
-
incomplete = sorted(
|
|
565
|
-
name for name, complete in derivation_state.items()
|
|
566
|
-
if not complete
|
|
567
|
-
)
|
|
568
|
-
if incomplete:
|
|
569
|
-
raise RuntimeError(
|
|
570
|
-
"incomplete derivation stages: " + ", ".join(incomplete)
|
|
571
|
-
)
|
|
572
661
|
checkpointed = self.repository.checkpoint_enriching(
|
|
573
662
|
operation_id,
|
|
574
663
|
final_fact_ids=fact_ids,
|
|
@@ -577,6 +666,21 @@ class IngestionCommand:
|
|
|
577
666
|
lease_owner=self._owner,
|
|
578
667
|
lease_seconds=self._lease_seconds,
|
|
579
668
|
)
|
|
669
|
+
if materialization_error or incomplete:
|
|
670
|
+
error = materialization_error or (
|
|
671
|
+
"incomplete derivation stages: " + ", ".join(incomplete)
|
|
672
|
+
)
|
|
673
|
+
return self.repository.finish_enriching(
|
|
674
|
+
operation_id,
|
|
675
|
+
owner=self._owner,
|
|
676
|
+
target=IngestionState.FAILED,
|
|
677
|
+
final_fact_ids=fact_ids,
|
|
678
|
+
derivation_version=self._derivation_version,
|
|
679
|
+
derivation_state=derivation_state,
|
|
680
|
+
last_error=error,
|
|
681
|
+
)
|
|
682
|
+
except LeaseLost:
|
|
683
|
+
raise
|
|
580
684
|
except Exception as exc:
|
|
581
685
|
return self.repository.finish_enriching(
|
|
582
686
|
operation_id,
|
|
@@ -598,10 +702,13 @@ class IngestionCommand:
|
|
|
598
702
|
owner=self._owner,
|
|
599
703
|
lease_seconds=self._lease_seconds,
|
|
600
704
|
)
|
|
601
|
-
projection_state = (
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
705
|
+
projection_state = self._run_with_lease_heartbeat(
|
|
706
|
+
operation.operation_id,
|
|
707
|
+
lambda: (
|
|
708
|
+
dict(self._projector(operation))
|
|
709
|
+
if self._projector is not None
|
|
710
|
+
else {}
|
|
711
|
+
),
|
|
605
712
|
)
|
|
606
713
|
combined = {**operation.derivation_state, **projection_state}
|
|
607
714
|
incomplete = sorted(
|
|
@@ -619,6 +726,8 @@ class IngestionCommand:
|
|
|
619
726
|
derivation_version=self._derivation_version,
|
|
620
727
|
derivation_state=combined,
|
|
621
728
|
)
|
|
729
|
+
except LeaseLost:
|
|
730
|
+
raise
|
|
622
731
|
except Exception as exc:
|
|
623
732
|
return self.repository.finish_enriching(
|
|
624
733
|
operation.operation_id,
|
|
@@ -128,20 +128,25 @@ def _load_injection_config():
|
|
|
128
128
|
|
|
129
129
|
|
|
130
130
|
def estimate_tokens(text: str) -> int:
|
|
131
|
-
"""
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
131
|
+
"""Estimate context tokens without loading native code by default.
|
|
132
|
+
|
|
133
|
+
The stable default is the deterministic ``chars / 4`` heuristic. Loading
|
|
134
|
+
the optional native ``tiktoken`` extension in a long-lived daemon that also
|
|
135
|
+
hosts torch/scipy workers can terminate the whole process on a native fault;
|
|
136
|
+
Python exception handling cannot recover from SIGBUS/SIGSEGV. Users who
|
|
137
|
+
explicitly need exact OpenAI token counts can opt in with
|
|
138
|
+
``SLM_INJECTION_EXACT_TOKENS=1``.
|
|
137
139
|
"""
|
|
138
140
|
if not text:
|
|
139
141
|
return 0
|
|
142
|
+
heuristic = max(1, len(text) // 4)
|
|
143
|
+
if os.environ.get("SLM_INJECTION_EXACT_TOKENS", "0") != "1":
|
|
144
|
+
return heuristic
|
|
140
145
|
try:
|
|
141
146
|
import tiktoken
|
|
142
147
|
return len(tiktoken.get_encoding("cl100k_base").encode(text))
|
|
143
148
|
except Exception:
|
|
144
|
-
return
|
|
149
|
+
return heuristic
|
|
145
150
|
|
|
146
151
|
|
|
147
152
|
def resolve_budget(mode: str, cfg) -> int:
|
|
@@ -8,7 +8,8 @@ V3.3.13: Periodically triggers Langevin/Ebbinghaus/Sheaf maintenance
|
|
|
8
8
|
so users don't need to call run_maintenance manually.
|
|
9
9
|
|
|
10
10
|
Configurable interval via ForgettingConfig.scheduler_interval_minutes.
|
|
11
|
-
Defaults to 30 min.
|
|
11
|
+
Defaults to 30 min. Optional forgetting/math work follows
|
|
12
|
+
``config.forgetting.enabled``; tier evaluation and bounded housekeeping do not.
|
|
12
13
|
|
|
13
14
|
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
14
15
|
License: AGPL-3.0-or-later
|
|
@@ -79,12 +80,21 @@ class MaintenanceScheduler:
|
|
|
79
80
|
if not self._running:
|
|
80
81
|
return
|
|
81
82
|
for profile_id in self._profile_ids():
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
if self._config.forgetting.enabled:
|
|
84
|
+
try:
|
|
85
|
+
from superlocalmemory.core.maintenance import run_maintenance
|
|
86
|
+
counts = run_maintenance(self._db, self._config, profile_id)
|
|
87
|
+
logger.info(
|
|
88
|
+
"Scheduled maintenance complete for %s: %s",
|
|
89
|
+
profile_id,
|
|
90
|
+
counts,
|
|
91
|
+
)
|
|
92
|
+
except Exception as exc:
|
|
93
|
+
logger.warning(
|
|
94
|
+
"Scheduled maintenance failed for %s: %s",
|
|
95
|
+
profile_id,
|
|
96
|
+
exc,
|
|
97
|
+
)
|
|
88
98
|
|
|
89
99
|
# V3.4.11: Graph pruning (remove orphan edges)
|
|
90
100
|
try:
|
|
@@ -632,8 +632,8 @@ def run_recall(
|
|
|
632
632
|
|
|
633
633
|
Pipeline: retrieval -> agentic sufficiency (if configured) -> post-recall updates.
|
|
634
634
|
|
|
635
|
-
``fast=True`` skips
|
|
636
|
-
|
|
635
|
+
``fast=True`` skips remote agentic verification while retaining the six
|
|
636
|
+
local retrieval channels.
|
|
637
637
|
"""
|
|
638
638
|
# Pre-operation hooks
|
|
639
639
|
hook_ctx = {
|
|
@@ -658,7 +658,9 @@ def run_recall(
|
|
|
658
658
|
logger.warning("[RECALL-TIMING] %-22s %.0f ms",
|
|
659
659
|
_label, (_time_t.monotonic() - _t0) * 1000.0)
|
|
660
660
|
|
|
661
|
-
|
|
661
|
+
# The interactive path must retain the complete local retrieval contract.
|
|
662
|
+
# Only agentic verification can invoke an unbounded model round.
|
|
663
|
+
extra_disabled = None
|
|
662
664
|
response = retrieval_engine.recall(
|
|
663
665
|
query, profile_id, m, limit,
|
|
664
666
|
extra_disabled_channels=extra_disabled,
|
|
@@ -11,8 +11,10 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
-
import
|
|
14
|
+
import hashlib
|
|
15
15
|
import json
|
|
16
|
+
import logging
|
|
17
|
+
import uuid
|
|
16
18
|
from typing import TYPE_CHECKING, Any
|
|
17
19
|
|
|
18
20
|
if TYPE_CHECKING:
|
|
@@ -21,7 +23,9 @@ if TYPE_CHECKING:
|
|
|
21
23
|
from superlocalmemory.storage.database import DatabaseManager
|
|
22
24
|
|
|
23
25
|
from superlocalmemory.storage.models import (
|
|
24
|
-
AtomicFact,
|
|
26
|
+
AtomicFact,
|
|
27
|
+
FactType,
|
|
28
|
+
MemoryRecord,
|
|
25
29
|
)
|
|
26
30
|
|
|
27
31
|
logger = logging.getLogger(__name__)
|
|
@@ -30,6 +34,63 @@ logger = logging.getLogger(__name__)
|
|
|
30
34
|
_INIT_LANGEVIN_RADIUS = 0.05
|
|
31
35
|
|
|
32
36
|
|
|
37
|
+
def _ingestion_effect_id(operation_id: str, *parts: object) -> str:
|
|
38
|
+
"""Return a stable ID for a relational effect owned by one ingestion."""
|
|
39
|
+
if not operation_id:
|
|
40
|
+
return uuid.uuid4().hex
|
|
41
|
+
payload = "\0".join((operation_id, *(str(part) for part in parts)))
|
|
42
|
+
return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:32]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _record_fact_entity_association(
|
|
46
|
+
db: DatabaseManager,
|
|
47
|
+
*,
|
|
48
|
+
operation_id: str,
|
|
49
|
+
profile_id: str,
|
|
50
|
+
fact_id: str,
|
|
51
|
+
entity_id: str,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Apply one fact/entity count effect in O(1), exactly once."""
|
|
54
|
+
if not operation_id:
|
|
55
|
+
db.increment_entity_fact_count(entity_id, profile_id)
|
|
56
|
+
return
|
|
57
|
+
with db.transaction():
|
|
58
|
+
claimed = db.execute(
|
|
59
|
+
"INSERT INTO fact_entity_associations "
|
|
60
|
+
"(profile_id,fact_id,entity_id,first_operation_id,count_applied) "
|
|
61
|
+
"SELECT ?,?,?,?,"
|
|
62
|
+
"CASE WHEN fact.rowid > repair.target_fact_rowid THEN 1 ELSE 0 END "
|
|
63
|
+
"FROM canonical_entities AS entity "
|
|
64
|
+
"JOIN atomic_facts AS fact "
|
|
65
|
+
"ON fact.fact_id=? AND fact.profile_id=? "
|
|
66
|
+
"JOIN fact_entity_association_repair_state AS repair "
|
|
67
|
+
"ON repair.repair_key='historical-backfill' "
|
|
68
|
+
"WHERE entity.entity_id=? AND entity.profile_id=? "
|
|
69
|
+
"ON CONFLICT(profile_id,fact_id,entity_id) DO UPDATE SET "
|
|
70
|
+
"count_applied=excluded.count_applied,"
|
|
71
|
+
"first_operation_id=excluded.first_operation_id "
|
|
72
|
+
"WHERE fact_entity_associations.count_applied=0 "
|
|
73
|
+
"AND excluded.count_applied=1 "
|
|
74
|
+
"RETURNING count_applied",
|
|
75
|
+
(
|
|
76
|
+
profile_id,
|
|
77
|
+
fact_id,
|
|
78
|
+
entity_id,
|
|
79
|
+
operation_id,
|
|
80
|
+
fact_id,
|
|
81
|
+
profile_id,
|
|
82
|
+
entity_id,
|
|
83
|
+
profile_id,
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
if claimed and int(claimed[0]["count_applied"]) == 1:
|
|
87
|
+
db.execute(
|
|
88
|
+
"UPDATE canonical_entities SET fact_count=fact_count+1 "
|
|
89
|
+
"WHERE entity_id=? AND profile_id=?",
|
|
90
|
+
(entity_id, profile_id),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
33
94
|
def _init_langevin_position(dim: int = 8) -> list[float]:
|
|
34
95
|
"""Initialize Langevin position near origin for a new fact.
|
|
35
96
|
|
|
@@ -60,7 +121,7 @@ def enrich_fact(
|
|
|
60
121
|
temporal_parser: Any,
|
|
61
122
|
) -> AtomicFact:
|
|
62
123
|
"""Enrich fact with embeddings, entities, temporal, emotional data."""
|
|
63
|
-
from superlocalmemory.encoding.emotional import
|
|
124
|
+
from superlocalmemory.encoding.emotional import emotional_importance_boost, tag_emotion
|
|
64
125
|
from superlocalmemory.encoding.signal_inference import infer_signal
|
|
65
126
|
|
|
66
127
|
embedding = embedder.embed(fact.content) if embedder else None
|
|
@@ -189,6 +250,9 @@ def run_store(
|
|
|
189
250
|
ingestion_source_type: str = "store",
|
|
190
251
|
ingestion_operation_id: str = "",
|
|
191
252
|
derivation_report: dict[str, bool] | None = None,
|
|
253
|
+
precompleted_derivation_stages: frozenset[str] = frozenset(),
|
|
254
|
+
materialization_progress: dict[str, Any] | None = None,
|
|
255
|
+
materialization_checkpoint: Any = None,
|
|
192
256
|
) -> list[str]:
|
|
193
257
|
"""Store content and extract structured facts. Returns fact_ids.
|
|
194
258
|
|
|
@@ -286,10 +350,19 @@ def run_store(
|
|
|
286
350
|
)
|
|
287
351
|
db.store_memory(record)
|
|
288
352
|
|
|
289
|
-
extraction_complete =
|
|
290
|
-
consolidation_complete =
|
|
291
|
-
|
|
292
|
-
|
|
353
|
+
extraction_complete = "extraction" in precompleted_derivation_stages
|
|
354
|
+
consolidation_complete = (
|
|
355
|
+
"consolidation" in precompleted_derivation_stages
|
|
356
|
+
or consolidator is not None
|
|
357
|
+
)
|
|
358
|
+
canonicalization_complete = (
|
|
359
|
+
"canonicalization" in precompleted_derivation_stages
|
|
360
|
+
or entity_resolver is not None
|
|
361
|
+
)
|
|
362
|
+
graph_complete = (
|
|
363
|
+
"graph" in precompleted_derivation_stages
|
|
364
|
+
or graph_builder is not None
|
|
365
|
+
)
|
|
293
366
|
temporal_complete = True
|
|
294
367
|
provenance_complete = provenance is not None
|
|
295
368
|
|
|
@@ -330,7 +403,6 @@ def run_store(
|
|
|
330
403
|
and content.strip()
|
|
331
404
|
and len(content.strip()) >= 40
|
|
332
405
|
and len(content.strip().split()) >= _MIN_VERBATIM_WORDS):
|
|
333
|
-
import uuid
|
|
334
406
|
import re as _re
|
|
335
407
|
_verbatim_text = content.strip()
|
|
336
408
|
# Extract entities using the same regex as fact_extractor
|
|
@@ -374,7 +446,6 @@ def run_store(
|
|
|
374
446
|
# their data should NEVER be silently dropped. The min-length and min-word filters
|
|
375
447
|
# are designed for automatic conversation extraction, not explicit user storage.
|
|
376
448
|
if not facts and content.strip():
|
|
377
|
-
import uuid
|
|
378
449
|
facts = [AtomicFact(
|
|
379
450
|
fact_id=uuid.uuid4().hex[:16],
|
|
380
451
|
content=content.strip(),
|
|
@@ -403,6 +474,22 @@ def run_store(
|
|
|
403
474
|
temporal_parser=temporal_parser,
|
|
404
475
|
)
|
|
405
476
|
|
|
477
|
+
if (
|
|
478
|
+
materialization_progress is not None
|
|
479
|
+
and not materialization_progress.get("relational_started", False)
|
|
480
|
+
):
|
|
481
|
+
if materialization_checkpoint is not None:
|
|
482
|
+
materialization_checkpoint(
|
|
483
|
+
"relational_started",
|
|
484
|
+
(),
|
|
485
|
+
{
|
|
486
|
+
"pipeline_started": True,
|
|
487
|
+
"relational_started": True,
|
|
488
|
+
"pipeline": False,
|
|
489
|
+
},
|
|
490
|
+
)
|
|
491
|
+
materialization_progress["relational_started"] = True
|
|
492
|
+
|
|
406
493
|
is_queryable_promotion = fact.fact_id in queryable_ids
|
|
407
494
|
if is_queryable_promotion:
|
|
408
495
|
db.update_fact(fact.fact_id, {
|
|
@@ -515,6 +602,8 @@ def run_store(
|
|
|
515
602
|
|
|
516
603
|
if fact.fact_id not in stored_ids:
|
|
517
604
|
stored_ids.append(fact.fact_id)
|
|
605
|
+
if materialization_progress is not None:
|
|
606
|
+
materialization_progress["fact_ids"] = tuple(stored_ids)
|
|
518
607
|
|
|
519
608
|
# Dual-write embedding to ANN index + vector store (embed on-demand if
|
|
520
609
|
# a consolidated ADD fact arrived without one). See _upsert_fact_vectors.
|
|
@@ -611,12 +700,17 @@ def run_store(
|
|
|
611
700
|
for eid in fact.canonical_entities:
|
|
612
701
|
observation_builder.update_profile(eid, fact, profile_id)
|
|
613
702
|
|
|
614
|
-
#
|
|
703
|
+
# The normalized association key makes this O(1) and exactly-once
|
|
704
|
+
# across retries, crashes, and separate operations that consolidate to
|
|
705
|
+
# the same fact.
|
|
615
706
|
for eid in fact.canonical_entities:
|
|
616
|
-
|
|
617
|
-
db
|
|
618
|
-
|
|
619
|
-
|
|
707
|
+
_record_fact_entity_association(
|
|
708
|
+
db,
|
|
709
|
+
operation_id=ingestion_operation_id,
|
|
710
|
+
profile_id=profile_id,
|
|
711
|
+
fact_id=fact.fact_id,
|
|
712
|
+
entity_id=eid,
|
|
713
|
+
)
|
|
620
714
|
if scene_builder:
|
|
621
715
|
scene_builder.assign_to_scene(fact, profile_id)
|
|
622
716
|
|
|
@@ -627,6 +721,13 @@ def run_store(
|
|
|
627
721
|
from superlocalmemory.storage.models import TemporalEvent
|
|
628
722
|
for eid in fact.canonical_entities:
|
|
629
723
|
event = TemporalEvent(
|
|
724
|
+
event_id=_ingestion_effect_id(
|
|
725
|
+
ingestion_operation_id,
|
|
726
|
+
"temporal",
|
|
727
|
+
fact.fact_id,
|
|
728
|
+
eid,
|
|
729
|
+
"observed",
|
|
730
|
+
),
|
|
630
731
|
profile_id=profile_id, entity_id=eid,
|
|
631
732
|
fact_id=fact.fact_id,
|
|
632
733
|
scope=fact.scope,
|
|
@@ -644,8 +745,16 @@ def run_store(
|
|
|
644
745
|
from superlocalmemory.encoding.foresight import extract_foresight_signals
|
|
645
746
|
from superlocalmemory.storage.models import TemporalEvent as _TE
|
|
646
747
|
foresight_signals = extract_foresight_signals(fact)
|
|
647
|
-
for sig in foresight_signals:
|
|
748
|
+
for signal_index, sig in enumerate(foresight_signals):
|
|
648
749
|
f_event = _TE(
|
|
750
|
+
event_id=_ingestion_effect_id(
|
|
751
|
+
ingestion_operation_id,
|
|
752
|
+
"temporal",
|
|
753
|
+
fact.fact_id,
|
|
754
|
+
sig.get("entity_id", ""),
|
|
755
|
+
"foresight",
|
|
756
|
+
signal_index,
|
|
757
|
+
),
|
|
649
758
|
profile_id=profile_id,
|
|
650
759
|
entity_id=sig.get("entity_id", ""),
|
|
651
760
|
fact_id=fact.fact_id,
|
|
@@ -679,11 +788,6 @@ def run_store(
|
|
|
679
788
|
|
|
680
789
|
logger.info("Stored %d facts (session=%s)", len(stored_ids), session_id)
|
|
681
790
|
|
|
682
|
-
# Post-operation hooks (audit, trust signal, event bus)
|
|
683
|
-
hook_ctx["fact_ids"] = stored_ids
|
|
684
|
-
hook_ctx["fact_count"] = len(stored_ids)
|
|
685
|
-
hooks.run_post("store", hook_ctx)
|
|
686
|
-
|
|
687
791
|
if derivation_report is not None:
|
|
688
792
|
derivation_report.update({
|
|
689
793
|
"extraction": extraction_complete,
|
|
@@ -694,6 +798,35 @@ def run_store(
|
|
|
694
798
|
"provenance": provenance_complete,
|
|
695
799
|
})
|
|
696
800
|
|
|
801
|
+
# Stage observations and emitted fact IDs are recorded before optional
|
|
802
|
+
# post-hooks. A hook failure must not erase the durable retry boundary and
|
|
803
|
+
# cause extraction/consolidation to run again.
|
|
804
|
+
if materialization_progress is not None:
|
|
805
|
+
materialization_progress["fact_ids"] = tuple(stored_ids)
|
|
806
|
+
materialization_progress["relational_complete"] = True
|
|
807
|
+
materialization_progress["post_hooks"] = False
|
|
808
|
+
if materialization_checkpoint is not None and stored_ids:
|
|
809
|
+
materialization_checkpoint(
|
|
810
|
+
"relational_complete",
|
|
811
|
+
tuple(stored_ids),
|
|
812
|
+
{
|
|
813
|
+
"pipeline_started": True,
|
|
814
|
+
"relational_started": True,
|
|
815
|
+
"pipeline": True,
|
|
816
|
+
"post_hooks": False,
|
|
817
|
+
"relational": True,
|
|
818
|
+
**dict(derivation_report or {}),
|
|
819
|
+
},
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
# Post-operation hooks (audit, trust signal, event bus)
|
|
823
|
+
hook_ctx["ingestion_operation_id"] = ingestion_operation_id
|
|
824
|
+
hook_ctx["fact_ids"] = stored_ids
|
|
825
|
+
hook_ctx["fact_count"] = len(stored_ids)
|
|
826
|
+
hooks.run_post("store", hook_ctx)
|
|
827
|
+
if materialization_progress is not None:
|
|
828
|
+
materialization_progress["post_hooks"] = True
|
|
829
|
+
|
|
697
830
|
# Phase 5: Step-count trigger for lightweight consolidation (L7)
|
|
698
831
|
if consolidation_engine is not None:
|
|
699
832
|
try:
|
|
@@ -26,6 +26,8 @@ import urllib.error
|
|
|
26
26
|
import urllib.request
|
|
27
27
|
from pathlib import Path
|
|
28
28
|
|
|
29
|
+
from superlocalmemory import __version__
|
|
30
|
+
|
|
29
31
|
# ---------------------------------------------------------------------------
|
|
30
32
|
# Cross-platform temp paths
|
|
31
33
|
# ---------------------------------------------------------------------------
|
|
@@ -221,7 +223,10 @@ def _codex_mcp_session_init(project_dir: str, payload: dict) -> dict:
|
|
|
221
223
|
proc.stdin.write(json.dumps({
|
|
222
224
|
"jsonrpc": "2.0", "id": 1, "method": "initialize",
|
|
223
225
|
"params": {"protocolVersion": "2024-11-05", "capabilities": {},
|
|
224
|
-
"clientInfo": {
|
|
226
|
+
"clientInfo": {
|
|
227
|
+
"name": "superlocalmemory-codex-hook",
|
|
228
|
+
"version": __version__,
|
|
229
|
+
}},
|
|
225
230
|
}) + "\n")
|
|
226
231
|
proc.stdin.flush()
|
|
227
232
|
proc.stdout.readline()
|