superlocalmemory 3.8.0 → 3.8.2
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 +112 -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 +2 -2
- 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 +3 -5
- 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 +2 -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 +3 -5
- 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/scripts/postinstall.js +7 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +494 -9
- package/src/superlocalmemory/cli/daemon.py +7 -0
- package/src/superlocalmemory/cli/loop_cmd.py +2 -7
- package/src/superlocalmemory/cli/main.py +72 -7
- package/src/superlocalmemory/cli/setup_wizard.py +142 -16
- package/src/superlocalmemory/cli/version_banner.py +17 -3
- package/src/superlocalmemory/core/backend_orchestrator.py +18 -16
- package/src/superlocalmemory/core/component_healer.py +144 -0
- package/src/superlocalmemory/core/component_registry.py +487 -0
- package/src/superlocalmemory/core/config.py +21 -0
- package/src/superlocalmemory/core/embedding_worker.py +4 -5
- package/src/superlocalmemory/core/embeddings.py +132 -45
- package/src/superlocalmemory/core/engine.py +29 -22
- package/src/superlocalmemory/core/engine_ingestion.py +332 -45
- package/src/superlocalmemory/core/ingestion_command.py +154 -25
- package/src/superlocalmemory/core/injection.py +12 -7
- package/src/superlocalmemory/core/maintenance.py +43 -0
- package/src/superlocalmemory/core/maintenance_scheduler.py +44 -6
- package/src/superlocalmemory/core/recall_pipeline.py +42 -4
- package/src/superlocalmemory/core/store_pipeline.py +195 -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/_daemon_proxy.py +6 -2
- package/src/superlocalmemory/mcp/_pool_adapter.py +4 -1
- package/src/superlocalmemory/mcp/server.py +11 -30
- package/src/superlocalmemory/mcp/tools_active.py +1 -1
- package/src/superlocalmemory/mcp/tools_core.py +21 -5
- package/src/superlocalmemory/mcp/tools_learning.py +2 -2
- package/src/superlocalmemory/retrieval/bridge_discovery.py +14 -0
- package/src/superlocalmemory/retrieval/engine.py +53 -21
- package/src/superlocalmemory/retrieval/reranker.py +3 -4
- package/src/superlocalmemory/retrieval/spreading_activation.py +68 -38
- 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 +342 -61
- 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 +286 -105
- package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
- package/src/superlocalmemory/server/routes/memories.py +8 -3
- 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 +184 -20
- package/src/superlocalmemory/server/unified_daemon.py +732 -41
- package/src/superlocalmemory/storage/embedding_migrator.py +235 -0
- 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 +6 -8
- 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-components.js +147 -0
- package/src/superlocalmemory/ui/js/od-entities.js +65 -22
- package/src/superlocalmemory/ui/js/od-graph.js +46 -4
- package/src/superlocalmemory/ui/js/od-health.js +18 -0
- package/src/superlocalmemory/ui/js/od-memories.js +84 -5
- package/src/superlocalmemory/ui/js/od-mesh.js +23 -9
- package/src/superlocalmemory/ui/js/od-operations.js +36 -0
- package/src/superlocalmemory/ui/js/od-settings.js +186 -63
- 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
|
@@ -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,105 @@ 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
|
+
elif not claimed:
|
|
93
|
+
# An empty result has TWO causes: (a) the 'historical-backfill'
|
|
94
|
+
# repair-state row is missing — M028 DDL ran but its INSERT rolled
|
|
95
|
+
# back (partial migration) — so the JOIN returned zero rows and the
|
|
96
|
+
# main INSERT produced no output; or (b) the association already
|
|
97
|
+
# exists and the ON CONFLICT update was correctly skipped (an
|
|
98
|
+
# idempotent re-run). Only (a) needs the fallback insert; (b) is a
|
|
99
|
+
# no-op. Disambiguate by probing the repair-state table directly —
|
|
100
|
+
# a DIFFERENT table, so this stays a constant-query, idempotent
|
|
101
|
+
# effect and never re-fires the insert on an already-applied row.
|
|
102
|
+
repair_ready = db.execute(
|
|
103
|
+
"SELECT 1 FROM fact_entity_association_repair_state "
|
|
104
|
+
"WHERE repair_key='historical-backfill' LIMIT 1",
|
|
105
|
+
)
|
|
106
|
+
if repair_ready:
|
|
107
|
+
# (b) idempotent skip — the association was already applied.
|
|
108
|
+
return
|
|
109
|
+
# (a) partial migration: insert with count_applied=1 (no historical-
|
|
110
|
+
# rowid check needed — all associations in this state are
|
|
111
|
+
# post-migration). ON CONFLICT DO NOTHING makes this retry-safe.
|
|
112
|
+
fallback = db.execute(
|
|
113
|
+
"INSERT INTO fact_entity_associations "
|
|
114
|
+
"(profile_id,fact_id,entity_id,first_operation_id,count_applied) "
|
|
115
|
+
"SELECT ?,?,?,?,1 "
|
|
116
|
+
"FROM canonical_entities AS entity "
|
|
117
|
+
"JOIN atomic_facts AS fact "
|
|
118
|
+
"ON fact.fact_id=? AND fact.profile_id=? "
|
|
119
|
+
"WHERE entity.entity_id=? AND entity.profile_id=? "
|
|
120
|
+
"ON CONFLICT(profile_id,fact_id,entity_id) DO NOTHING "
|
|
121
|
+
"RETURNING count_applied",
|
|
122
|
+
(
|
|
123
|
+
profile_id, fact_id, entity_id, operation_id,
|
|
124
|
+
fact_id, profile_id,
|
|
125
|
+
entity_id, profile_id,
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
if fallback:
|
|
129
|
+
db.execute(
|
|
130
|
+
"UPDATE canonical_entities SET fact_count=fact_count+1 "
|
|
131
|
+
"WHERE entity_id=? AND profile_id=?",
|
|
132
|
+
(entity_id, profile_id),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
33
136
|
def _init_langevin_position(dim: int = 8) -> list[float]:
|
|
34
137
|
"""Initialize Langevin position near origin for a new fact.
|
|
35
138
|
|
|
@@ -60,7 +163,7 @@ def enrich_fact(
|
|
|
60
163
|
temporal_parser: Any,
|
|
61
164
|
) -> AtomicFact:
|
|
62
165
|
"""Enrich fact with embeddings, entities, temporal, emotional data."""
|
|
63
|
-
from superlocalmemory.encoding.emotional import
|
|
166
|
+
from superlocalmemory.encoding.emotional import emotional_importance_boost, tag_emotion
|
|
64
167
|
from superlocalmemory.encoding.signal_inference import infer_signal
|
|
65
168
|
|
|
66
169
|
embedding = embedder.embed(fact.content) if embedder else None
|
|
@@ -189,6 +292,9 @@ def run_store(
|
|
|
189
292
|
ingestion_source_type: str = "store",
|
|
190
293
|
ingestion_operation_id: str = "",
|
|
191
294
|
derivation_report: dict[str, bool] | None = None,
|
|
295
|
+
precompleted_derivation_stages: frozenset[str] = frozenset(),
|
|
296
|
+
materialization_progress: dict[str, Any] | None = None,
|
|
297
|
+
materialization_checkpoint: Any = None,
|
|
192
298
|
) -> list[str]:
|
|
193
299
|
"""Store content and extract structured facts. Returns fact_ids.
|
|
194
300
|
|
|
@@ -286,10 +392,19 @@ def run_store(
|
|
|
286
392
|
)
|
|
287
393
|
db.store_memory(record)
|
|
288
394
|
|
|
289
|
-
extraction_complete =
|
|
290
|
-
consolidation_complete =
|
|
291
|
-
|
|
292
|
-
|
|
395
|
+
extraction_complete = "extraction" in precompleted_derivation_stages
|
|
396
|
+
consolidation_complete = (
|
|
397
|
+
"consolidation" in precompleted_derivation_stages
|
|
398
|
+
or consolidator is not None
|
|
399
|
+
)
|
|
400
|
+
canonicalization_complete = (
|
|
401
|
+
"canonicalization" in precompleted_derivation_stages
|
|
402
|
+
or entity_resolver is not None
|
|
403
|
+
)
|
|
404
|
+
graph_complete = (
|
|
405
|
+
"graph" in precompleted_derivation_stages
|
|
406
|
+
or graph_builder is not None
|
|
407
|
+
)
|
|
293
408
|
temporal_complete = True
|
|
294
409
|
provenance_complete = provenance is not None
|
|
295
410
|
|
|
@@ -330,7 +445,6 @@ def run_store(
|
|
|
330
445
|
and content.strip()
|
|
331
446
|
and len(content.strip()) >= 40
|
|
332
447
|
and len(content.strip().split()) >= _MIN_VERBATIM_WORDS):
|
|
333
|
-
import uuid
|
|
334
448
|
import re as _re
|
|
335
449
|
_verbatim_text = content.strip()
|
|
336
450
|
# Extract entities using the same regex as fact_extractor
|
|
@@ -374,7 +488,6 @@ def run_store(
|
|
|
374
488
|
# their data should NEVER be silently dropped. The min-length and min-word filters
|
|
375
489
|
# are designed for automatic conversation extraction, not explicit user storage.
|
|
376
490
|
if not facts and content.strip():
|
|
377
|
-
import uuid
|
|
378
491
|
facts = [AtomicFact(
|
|
379
492
|
fact_id=uuid.uuid4().hex[:16],
|
|
380
493
|
content=content.strip(),
|
|
@@ -403,6 +516,22 @@ def run_store(
|
|
|
403
516
|
temporal_parser=temporal_parser,
|
|
404
517
|
)
|
|
405
518
|
|
|
519
|
+
if (
|
|
520
|
+
materialization_progress is not None
|
|
521
|
+
and not materialization_progress.get("relational_started", False)
|
|
522
|
+
):
|
|
523
|
+
if materialization_checkpoint is not None:
|
|
524
|
+
materialization_checkpoint(
|
|
525
|
+
"relational_started",
|
|
526
|
+
(),
|
|
527
|
+
{
|
|
528
|
+
"pipeline_started": True,
|
|
529
|
+
"relational_started": True,
|
|
530
|
+
"pipeline": False,
|
|
531
|
+
},
|
|
532
|
+
)
|
|
533
|
+
materialization_progress["relational_started"] = True
|
|
534
|
+
|
|
406
535
|
is_queryable_promotion = fact.fact_id in queryable_ids
|
|
407
536
|
if is_queryable_promotion:
|
|
408
537
|
db.update_fact(fact.fact_id, {
|
|
@@ -515,6 +644,8 @@ def run_store(
|
|
|
515
644
|
|
|
516
645
|
if fact.fact_id not in stored_ids:
|
|
517
646
|
stored_ids.append(fact.fact_id)
|
|
647
|
+
if materialization_progress is not None:
|
|
648
|
+
materialization_progress["fact_ids"] = tuple(stored_ids)
|
|
518
649
|
|
|
519
650
|
# Dual-write embedding to ANN index + vector store (embed on-demand if
|
|
520
651
|
# a consolidated ADD fact arrived without one). See _upsert_fact_vectors.
|
|
@@ -611,12 +742,17 @@ def run_store(
|
|
|
611
742
|
for eid in fact.canonical_entities:
|
|
612
743
|
observation_builder.update_profile(eid, fact, profile_id)
|
|
613
744
|
|
|
614
|
-
#
|
|
745
|
+
# The normalized association key makes this O(1) and exactly-once
|
|
746
|
+
# across retries, crashes, and separate operations that consolidate to
|
|
747
|
+
# the same fact.
|
|
615
748
|
for eid in fact.canonical_entities:
|
|
616
|
-
|
|
617
|
-
db
|
|
618
|
-
|
|
619
|
-
|
|
749
|
+
_record_fact_entity_association(
|
|
750
|
+
db,
|
|
751
|
+
operation_id=ingestion_operation_id,
|
|
752
|
+
profile_id=profile_id,
|
|
753
|
+
fact_id=fact.fact_id,
|
|
754
|
+
entity_id=eid,
|
|
755
|
+
)
|
|
620
756
|
if scene_builder:
|
|
621
757
|
scene_builder.assign_to_scene(fact, profile_id)
|
|
622
758
|
|
|
@@ -627,6 +763,13 @@ def run_store(
|
|
|
627
763
|
from superlocalmemory.storage.models import TemporalEvent
|
|
628
764
|
for eid in fact.canonical_entities:
|
|
629
765
|
event = TemporalEvent(
|
|
766
|
+
event_id=_ingestion_effect_id(
|
|
767
|
+
ingestion_operation_id,
|
|
768
|
+
"temporal",
|
|
769
|
+
fact.fact_id,
|
|
770
|
+
eid,
|
|
771
|
+
"observed",
|
|
772
|
+
),
|
|
630
773
|
profile_id=profile_id, entity_id=eid,
|
|
631
774
|
fact_id=fact.fact_id,
|
|
632
775
|
scope=fact.scope,
|
|
@@ -644,8 +787,16 @@ def run_store(
|
|
|
644
787
|
from superlocalmemory.encoding.foresight import extract_foresight_signals
|
|
645
788
|
from superlocalmemory.storage.models import TemporalEvent as _TE
|
|
646
789
|
foresight_signals = extract_foresight_signals(fact)
|
|
647
|
-
for sig in foresight_signals:
|
|
790
|
+
for signal_index, sig in enumerate(foresight_signals):
|
|
648
791
|
f_event = _TE(
|
|
792
|
+
event_id=_ingestion_effect_id(
|
|
793
|
+
ingestion_operation_id,
|
|
794
|
+
"temporal",
|
|
795
|
+
fact.fact_id,
|
|
796
|
+
sig.get("entity_id", ""),
|
|
797
|
+
"foresight",
|
|
798
|
+
signal_index,
|
|
799
|
+
),
|
|
649
800
|
profile_id=profile_id,
|
|
650
801
|
entity_id=sig.get("entity_id", ""),
|
|
651
802
|
fact_id=fact.fact_id,
|
|
@@ -679,11 +830,6 @@ def run_store(
|
|
|
679
830
|
|
|
680
831
|
logger.info("Stored %d facts (session=%s)", len(stored_ids), session_id)
|
|
681
832
|
|
|
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
833
|
if derivation_report is not None:
|
|
688
834
|
derivation_report.update({
|
|
689
835
|
"extraction": extraction_complete,
|
|
@@ -694,6 +840,35 @@ def run_store(
|
|
|
694
840
|
"provenance": provenance_complete,
|
|
695
841
|
})
|
|
696
842
|
|
|
843
|
+
# Stage observations and emitted fact IDs are recorded before optional
|
|
844
|
+
# post-hooks. A hook failure must not erase the durable retry boundary and
|
|
845
|
+
# cause extraction/consolidation to run again.
|
|
846
|
+
if materialization_progress is not None:
|
|
847
|
+
materialization_progress["fact_ids"] = tuple(stored_ids)
|
|
848
|
+
materialization_progress["relational_complete"] = True
|
|
849
|
+
materialization_progress["post_hooks"] = False
|
|
850
|
+
if materialization_checkpoint is not None and stored_ids:
|
|
851
|
+
materialization_checkpoint(
|
|
852
|
+
"relational_complete",
|
|
853
|
+
tuple(stored_ids),
|
|
854
|
+
{
|
|
855
|
+
"pipeline_started": True,
|
|
856
|
+
"relational_started": True,
|
|
857
|
+
"pipeline": True,
|
|
858
|
+
"post_hooks": False,
|
|
859
|
+
"relational": True,
|
|
860
|
+
**dict(derivation_report or {}),
|
|
861
|
+
},
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
# Post-operation hooks (audit, trust signal, event bus)
|
|
865
|
+
hook_ctx["ingestion_operation_id"] = ingestion_operation_id
|
|
866
|
+
hook_ctx["fact_ids"] = stored_ids
|
|
867
|
+
hook_ctx["fact_count"] = len(stored_ids)
|
|
868
|
+
hooks.run_post("store", hook_ctx)
|
|
869
|
+
if materialization_progress is not None:
|
|
870
|
+
materialization_progress["post_hooks"] = True
|
|
871
|
+
|
|
697
872
|
# Phase 5: Step-count trigger for lightweight consolidation (L7)
|
|
698
873
|
if consolidation_engine is not None:
|
|
699
874
|
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()
|
|
@@ -240,11 +240,12 @@ def connect_ide(
|
|
|
240
240
|
here: bool = False,
|
|
241
241
|
profile: str | None = None,
|
|
242
242
|
agents_md_source: Callable[[], str] | None = None,
|
|
243
|
+
dry_run: bool = False,
|
|
243
244
|
) -> dict[str, Any]:
|
|
244
245
|
"""Wire SLM into the target IDE config via merge-not-clobber.
|
|
245
246
|
|
|
246
247
|
Returns a result dict:
|
|
247
|
-
{ide, mcp_config: wrote|merged|unchanged|skipped|error,
|
|
248
|
+
{ide, mcp_config: wrote|merged|unchanged|would_write|skipped|error,
|
|
248
249
|
mcp_path, agents_md: wrote|skipped(...)|unchanged|error,
|
|
249
250
|
servers_preserved: int, error: str|None}
|
|
250
251
|
"""
|
|
@@ -265,6 +266,13 @@ def connect_ide(
|
|
|
265
266
|
)
|
|
266
267
|
return result
|
|
267
268
|
|
|
269
|
+
if ide_id == "vscode-copilot" and not here:
|
|
270
|
+
result["error"] = (
|
|
271
|
+
"VS Code / Copilot integration is project-scoped. "
|
|
272
|
+
"Run `slm connect vscode-copilot --here` from the project root."
|
|
273
|
+
)
|
|
274
|
+
return result
|
|
275
|
+
|
|
268
276
|
# Step 1a — claude-code short-circuit (AC6)
|
|
269
277
|
if desc.fmt == "":
|
|
270
278
|
print(CLAUDE_CODE_PLUGIN_POINTER)
|
|
@@ -325,7 +333,28 @@ def connect_ide(
|
|
|
325
333
|
result["servers_preserved"] = max(0, pre_count - (0 if pre_slm is None else 1))
|
|
326
334
|
result["mcp_config"] = mcp_status
|
|
327
335
|
|
|
328
|
-
# Step 6 — atomic write
|
|
336
|
+
# Step 6 — atomic write. A dry run deliberately exercises the merge and
|
|
337
|
+
# packaged-asset lookup path without touching a user-owned IDE config.
|
|
338
|
+
if dry_run:
|
|
339
|
+
result["mcp_config"] = "would_write"
|
|
340
|
+
if desc.agents_md_path is None:
|
|
341
|
+
result["agents_md"] = "skipped(unsupported)"
|
|
342
|
+
elif agents_md_source is None:
|
|
343
|
+
result["agents_md"] = "skipped(no-source)"
|
|
344
|
+
else:
|
|
345
|
+
try:
|
|
346
|
+
source_content = agents_md_source()
|
|
347
|
+
except Exception as exc:
|
|
348
|
+
result["agents_md"] = "error(source-unavailable)"
|
|
349
|
+
result["error"] = f"AGENTS.md asset lookup failed: {exc}"
|
|
350
|
+
return result
|
|
351
|
+
if not isinstance(source_content, str) or not source_content.strip():
|
|
352
|
+
result["agents_md"] = "error(empty-source)"
|
|
353
|
+
result["error"] = "AGENTS.md asset is empty"
|
|
354
|
+
return result
|
|
355
|
+
result["agents_md"] = "would_write"
|
|
356
|
+
return result
|
|
357
|
+
|
|
329
358
|
try:
|
|
330
359
|
_atomic_write(config_path, data, desc.fmt)
|
|
331
360
|
except Exception as exc:
|
|
@@ -528,6 +557,9 @@ def _handle_agents_md(
|
|
|
528
557
|
except Exception as exc:
|
|
529
558
|
logger.warning("agents_md_source() failed: %s — skipping AGENTS.md write", exc)
|
|
530
559
|
return "skipped(source-error)"
|
|
560
|
+
if not isinstance(source_content, str) or not source_content.strip():
|
|
561
|
+
logger.warning("agents_md_source() returned no content — skipping AGENTS.md write")
|
|
562
|
+
return "skipped(no-source-content)"
|
|
531
563
|
|
|
532
564
|
# Read existing content
|
|
533
565
|
existing = ""
|
|
@@ -34,6 +34,8 @@ import sqlite3
|
|
|
34
34
|
from datetime import datetime, timedelta, timezone
|
|
35
35
|
from typing import Final
|
|
36
36
|
|
|
37
|
+
from superlocalmemory.learning.model_cache import invalidate as invalidate_model_cache
|
|
38
|
+
|
|
37
39
|
logger = logging.getLogger(__name__)
|
|
38
40
|
|
|
39
41
|
|
|
@@ -235,6 +237,7 @@ class ModelRollback:
|
|
|
235
237
|
)
|
|
236
238
|
return False
|
|
237
239
|
|
|
240
|
+
invalidate_model_cache(self._profile_id)
|
|
238
241
|
logger.warning(
|
|
239
242
|
"AUTO-ROLLBACK profile=%s reason=%s observations=%d "
|
|
240
243
|
"baseline_ndcg=%.4f current_ndcg=%.4f",
|
|
@@ -33,6 +33,7 @@ from datetime import datetime, timezone
|
|
|
33
33
|
from pathlib import Path
|
|
34
34
|
from typing import Final
|
|
35
35
|
|
|
36
|
+
from superlocalmemory.learning.model_cache import invalidate as invalidate_model_cache
|
|
36
37
|
from superlocalmemory.learning.ranker_common import (
|
|
37
38
|
_build_training_matrix,
|
|
38
39
|
_compute_eval_metrics,
|
|
@@ -389,6 +390,7 @@ def _promote_candidate(
|
|
|
389
390
|
(json.dumps(meta), candidate_id),
|
|
390
391
|
)
|
|
391
392
|
conn.commit()
|
|
393
|
+
invalidate_model_cache(profile_id)
|
|
392
394
|
return True
|
|
393
395
|
except sqlite3.Error as exc:
|
|
394
396
|
conn.rollback()
|
|
@@ -73,6 +73,24 @@ _DWELL_MAX_MS: Final[int] = 3_600_000 # 1 h
|
|
|
73
73
|
_BUSY_TIMEOUT_MS: Final[int] = 50
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
def _feed_source_quality(
|
|
77
|
+
memory_db_path: Path,
|
|
78
|
+
rewards: list[tuple[str, str, list[str], float]],
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Fail-soft provenance bridge kept outside the reward transaction."""
|
|
81
|
+
try:
|
|
82
|
+
from superlocalmemory.learning.source_quality import (
|
|
83
|
+
update_source_quality_for_reward_batch,
|
|
84
|
+
)
|
|
85
|
+
update_source_quality_for_reward_batch(
|
|
86
|
+
memory_db_path=memory_db_path,
|
|
87
|
+
learning_db_path=memory_db_path.parent / "learning.db",
|
|
88
|
+
rewards=rewards,
|
|
89
|
+
)
|
|
90
|
+
except Exception as exc: # noqa: BLE001 - reward finalization must survive
|
|
91
|
+
logger.debug("source-quality reward feed failed (non-fatal): %s", exc)
|
|
92
|
+
|
|
93
|
+
|
|
76
94
|
# ---------------------------------------------------------------------------
|
|
77
95
|
# Signal contract — names match the manifest A.1 label formula.
|
|
78
96
|
# ---------------------------------------------------------------------------
|
|
@@ -604,6 +622,7 @@ class EngagementRewardModel:
|
|
|
604
622
|
# lock-scope — SQLite rows are tied to the connection.
|
|
605
623
|
_pid = pending["profile_id"]
|
|
606
624
|
_qid = pending["recall_query_id"]
|
|
625
|
+
_facts_json = str(pending["fact_ids_json"] or "[]")
|
|
607
626
|
except sqlite3.Error as exc:
|
|
608
627
|
logger.debug("finalize_outcome SQLite error: %s", exc)
|
|
609
628
|
return _FALLBACK_REWARD
|
|
@@ -633,6 +652,21 @@ class EngagementRewardModel:
|
|
|
633
652
|
except Exception as exc: # noqa: BLE001 — defence in depth
|
|
634
653
|
logger.debug("feed_recall_settled failed (non-fatal): %s", exc)
|
|
635
654
|
|
|
655
|
+
try:
|
|
656
|
+
_fact_ids = json.loads(_facts_json)
|
|
657
|
+
if not isinstance(_fact_ids, list):
|
|
658
|
+
_fact_ids = []
|
|
659
|
+
except (TypeError, ValueError, json.JSONDecodeError):
|
|
660
|
+
_fact_ids = []
|
|
661
|
+
_feed_source_quality(
|
|
662
|
+
self._db,
|
|
663
|
+
[(
|
|
664
|
+
str(_pid),
|
|
665
|
+
str(outcome_id),
|
|
666
|
+
[str(fid) for fid in _fact_ids if fid],
|
|
667
|
+
float(reward),
|
|
668
|
+
)],
|
|
669
|
+
)
|
|
636
670
|
return reward
|
|
637
671
|
|
|
638
672
|
# ------------------------------------------------------------------
|
|
@@ -749,6 +783,22 @@ class EngagementRewardModel:
|
|
|
749
783
|
except sqlite3.Error:
|
|
750
784
|
conn.execute("ROLLBACK")
|
|
751
785
|
raise
|
|
786
|
+
reward_sources = []
|
|
787
|
+
for item in i_chunk:
|
|
788
|
+
try:
|
|
789
|
+
fact_ids = json.loads(str(item[2] or "[]"))
|
|
790
|
+
except (TypeError, ValueError, json.JSONDecodeError):
|
|
791
|
+
fact_ids = []
|
|
792
|
+
reward_sources.append((
|
|
793
|
+
str(item[1]),
|
|
794
|
+
str(item[0]),
|
|
795
|
+
(
|
|
796
|
+
[str(fid) for fid in fact_ids if fid]
|
|
797
|
+
if isinstance(fact_ids, list) else []
|
|
798
|
+
),
|
|
799
|
+
float(item[4]),
|
|
800
|
+
))
|
|
801
|
+
_feed_source_quality(self._db, reward_sources)
|
|
752
802
|
except sqlite3.Error as exc: # pragma: no cover — defensive
|
|
753
803
|
logger.debug("reap_stale SQLite error: %s", exc)
|
|
754
804
|
return written
|