superlocalmemory 3.8.13 → 4.0.0
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/ATTRIBUTION.md +4 -4
- package/CHANGELOG.md +113 -121
- package/README.md +65 -63
- package/docs/pi-dev-integration.md +1 -1
- package/package.json +6 -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-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-loop/SKILL.md +99 -0
- package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
- package/plugin-src/skills/slm-profile/SKILL.md +148 -0
- 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-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +11 -4
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +125 -11
- package/src/superlocalmemory/cli/daemon.py +5 -1
- package/src/superlocalmemory/cli/main.py +35 -2
- package/src/superlocalmemory/cli/ops_cmd.py +281 -0
- package/src/superlocalmemory/cli/setup_wizard.py +1 -1
- package/src/superlocalmemory/compliance/audit.py +65 -0
- package/src/superlocalmemory/compliance/eu_ai_act.py +27 -57
- package/src/superlocalmemory/compliance/gdpr.py +416 -20
- package/src/superlocalmemory/compliance/retention.py +74 -22
- package/src/superlocalmemory/compliance/scheduler.py +78 -9
- package/src/superlocalmemory/core/actor_context.py +166 -0
- package/src/superlocalmemory/core/admission.py +549 -0
- package/src/superlocalmemory/core/backend_orchestrator.py +23 -10
- package/src/superlocalmemory/core/config.py +202 -24
- package/src/superlocalmemory/core/consolidation_engine.py +13 -13
- package/src/superlocalmemory/core/context_cache.py +28 -0
- package/src/superlocalmemory/core/embeddings.py +64 -2
- package/src/superlocalmemory/core/engine.py +7 -2
- package/src/superlocalmemory/core/engine_ingestion.py +65 -3
- package/src/superlocalmemory/core/engine_wiring.py +36 -9
- package/src/superlocalmemory/core/ingest_policy.py +38 -0
- package/src/superlocalmemory/core/maintenance.py +255 -0
- package/src/superlocalmemory/core/modes.py +40 -13
- package/src/superlocalmemory/core/mutations.py +437 -44
- package/src/superlocalmemory/core/operation_policy.py +92 -0
- package/src/superlocalmemory/core/operation_policy_registry.py +542 -0
- package/src/superlocalmemory/core/operation_request.py +127 -0
- package/src/superlocalmemory/core/ops_remediation.py +542 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -0
- package/src/superlocalmemory/core/remember_runtime.py +202 -4
- package/src/superlocalmemory/core/remote_mode.py +20 -5
- package/src/superlocalmemory/core/store_pipeline.py +150 -0
- package/src/superlocalmemory/core/topic_signature.py +19 -4
- package/src/superlocalmemory/core/transactions/__init__.py +78 -0
- package/src/superlocalmemory/core/transactions/concrete_owners.py +597 -0
- package/src/superlocalmemory/core/transactions/erasure.py +825 -0
- package/src/superlocalmemory/core/transactions/manifest.py +255 -0
- package/src/superlocalmemory/core/transactions/manifest_key.py +155 -0
- package/src/superlocalmemory/core/transactions/obligations.py +272 -0
- package/src/superlocalmemory/core/transactions/owners.py +114 -0
- package/src/superlocalmemory/core/transactions/reconciler.py +285 -0
- package/src/superlocalmemory/core/transactions/service.py +330 -0
- package/src/superlocalmemory/core/worker_pool.py +33 -5
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +70 -28
- package/src/superlocalmemory/encoding/emotional.py +75 -14
- package/src/superlocalmemory/encoding/scene_builder.py +115 -13
- package/src/superlocalmemory/encoding/temporal_parser.py +4 -0
- package/src/superlocalmemory/evolution/blind_verifier.py +11 -4
- package/src/superlocalmemory/evolution/evolution_store.py +244 -4
- package/src/superlocalmemory/evolution/llm_dispatch.py +40 -0
- package/src/superlocalmemory/evolution/model_selection.py +18 -3
- package/src/superlocalmemory/evolution/mutation_generator.py +3 -0
- package/src/superlocalmemory/evolution/skill_activator.py +270 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +281 -59
- package/src/superlocalmemory/evolution/types.py +30 -8
- package/src/superlocalmemory/graph/cozo_backend.py +17 -9
- package/src/superlocalmemory/hooks/auto_invoker.py +2 -1
- package/src/superlocalmemory/hooks/auto_recall.py +64 -30
- package/src/superlocalmemory/hooks/codex_assets.py +14 -1
- package/src/superlocalmemory/infra/backup.py +434 -7
- package/src/superlocalmemory/infra/process_reaper.py +18 -0
- package/src/superlocalmemory/infra/self_heal.py +401 -0
- package/src/superlocalmemory/learning/feedback.py +52 -9
- package/src/superlocalmemory/loops/engine.py +10 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/http_transport.py +30 -331
- package/src/superlocalmemory/mcp/profiles.py +5 -0
- package/src/superlocalmemory/mcp/resources.py +8 -0
- package/src/superlocalmemory/mcp/server.py +51 -4
- package/src/superlocalmemory/mcp/shared.py +19 -0
- package/src/superlocalmemory/mcp/tools_active.py +25 -4
- package/src/superlocalmemory/mcp/tools_code_graph.py +26 -18
- package/src/superlocalmemory/mcp/tools_context.py +50 -8
- package/src/superlocalmemory/mcp/tools_core.py +69 -21
- package/src/superlocalmemory/mcp/tools_evolution.py +9 -2
- package/src/superlocalmemory/mcp/tools_learning.py +21 -10
- package/src/superlocalmemory/mcp/tools_loops.py +29 -18
- package/src/superlocalmemory/mcp/tools_mesh.py +8 -0
- package/src/superlocalmemory/mcp/tools_ops.py +115 -0
- package/src/superlocalmemory/mcp/tools_optimize.py +4 -0
- package/src/superlocalmemory/mcp/tools_v28.py +10 -3
- package/src/superlocalmemory/mcp/tools_v3.py +34 -14
- package/src/superlocalmemory/mcp/tools_v33.py +18 -33
- package/src/superlocalmemory/mesh/broker.py +124 -46
- package/src/superlocalmemory/mesh/broker_security.py +470 -0
- package/src/superlocalmemory/mesh/discovery.py +365 -0
- package/src/superlocalmemory/mesh/lock_protocol.py +313 -0
- package/src/superlocalmemory/mesh/node_identity.py +97 -0
- package/src/superlocalmemory/mesh/outbox_remote.py +429 -0
- package/src/superlocalmemory/mesh/remote_sync.py +511 -28
- package/src/superlocalmemory/mesh/state_sync.py +286 -0
- package/src/superlocalmemory/optimize/config/store.py +45 -0
- package/src/superlocalmemory/parameterization/cross_project.py +12 -0
- package/src/superlocalmemory/parameterization/prompt_injector.py +13 -11
- package/src/superlocalmemory/parameterization/prompt_lifecycle.py +8 -2
- package/src/superlocalmemory/parameterization/workflow_miner.py +17 -0
- package/src/superlocalmemory/retrieval/ann_index.py +5 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +49 -2
- package/src/superlocalmemory/retrieval/engine.py +19 -4
- package/src/superlocalmemory/retrieval/fusion.py +4 -1
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -3
- package/src/superlocalmemory/retrieval/remote_reranker.py +47 -22
- package/src/superlocalmemory/retrieval/reranker.py +32 -1
- package/src/superlocalmemory/retrieval/temporal_channel.py +16 -3
- package/src/superlocalmemory/retrieval/temporal_utils.py +107 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +155 -42
- package/src/superlocalmemory/retrieval/vector_store.py +214 -8
- package/src/superlocalmemory/server/api.py +5 -5
- package/src/superlocalmemory/server/egress_policy.py +258 -0
- package/src/superlocalmemory/server/rbac_enforce.py +32 -0
- package/src/superlocalmemory/server/route_mutations.py +20 -0
- package/src/superlocalmemory/server/routes/compliance.py +153 -7
- package/src/superlocalmemory/server/routes/data_io.py +43 -2
- package/src/superlocalmemory/server/routes/events.py +15 -0
- package/src/superlocalmemory/server/routes/memories.py +56 -3
- package/src/superlocalmemory/server/routes/mesh.py +82 -1
- package/src/superlocalmemory/server/routes/mesh_lock.py +54 -0
- package/src/superlocalmemory/server/routes/mesh_state.py +63 -0
- package/src/superlocalmemory/server/routes/v3_api.py +50 -27
- package/src/superlocalmemory/server/routes/ws.py +86 -0
- package/src/superlocalmemory/server/ui.py +6 -6
- package/src/superlocalmemory/server/unified_daemon.py +942 -119
- package/src/superlocalmemory/storage/_migration_internals.py +568 -0
- package/src/superlocalmemory/storage/_schema_version.py +110 -0
- package/src/superlocalmemory/storage/database.py +329 -24
- package/src/superlocalmemory/storage/embedding_migrator.py +246 -51
- package/src/superlocalmemory/storage/erasure_fence.py +45 -0
- package/src/superlocalmemory/storage/generation_fence.py +63 -0
- package/src/superlocalmemory/storage/migration_runner.py +140 -417
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +40 -0
- package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +148 -0
- package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +58 -0
- package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +113 -0
- package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +107 -0
- package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +162 -0
- package/src/superlocalmemory/storage/migrations/{M033_learning_feedback_channel.py → M038_learning_feedback_channel.py} +3 -3
- package/src/superlocalmemory/storage/migrations/M039_scene_fact_members.py +137 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -2
- package/src/superlocalmemory/storage/schema.py +67 -0
- package/src/superlocalmemory/storage/write_coordinator.py +125 -0
- package/src/superlocalmemory/trust/scorer.py +28 -4
- package/src/superlocalmemory/ui/index.html +14 -3
- package/src/superlocalmemory/ui/js/auto-settings.js +12 -1
- package/src/superlocalmemory/ui/js/brain.js +6 -4
- package/src/superlocalmemory/ui/js/compliance.js +66 -12
- package/src/superlocalmemory/ui/js/dashboard.js +13 -3
- package/src/superlocalmemory/ui/js/feedback.js +8 -2
- package/src/superlocalmemory/ui/js/lifecycle.js +7 -1
- package/src/superlocalmemory/ui/js/modal.js +272 -5
- package/src/superlocalmemory/ui/js/od-backup.js +9 -2
- package/src/superlocalmemory/ui/js/od-compliance-ext.js +301 -0
- package/src/superlocalmemory/ui/js/od-operations.js +154 -23
- package/src/superlocalmemory/ui/js/od-ops-health.js +417 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +35 -21
- package/src/superlocalmemory/ui/js/od-team.js +9 -2
- package/src/superlocalmemory/ui/js/optimize.js +13 -16
- package/src/superlocalmemory/ui/js/profiles.js +7 -3
- package/src/superlocalmemory/ui/js/settings.js +7 -1
- package/src/superlocalmemory/vector/lancedb_backend.py +19 -9
- package/src/superlocalmemory/attribution/mathematical_dna.py +0 -235
- package/src/superlocalmemory/cli/post_install.py +0 -114
- package/src/superlocalmemory/core/clock_monitor.py +0 -45
- package/src/superlocalmemory/core/db_pool.py +0 -80
- package/src/superlocalmemory/core/error_catalog.py +0 -113
- package/src/superlocalmemory/core/loop_watchdog.py +0 -56
- package/src/superlocalmemory/core/priority_queue.py +0 -61
- package/src/superlocalmemory/core/pruning_engine.py +0 -216
- package/src/superlocalmemory/core/queue_dispatcher.py +0 -73
- package/src/superlocalmemory/core/slmignore.py +0 -125
- package/src/superlocalmemory/infra/heartbeat_monitor.py +0 -140
- package/src/superlocalmemory/infra/webhook_dispatcher.py +0 -247
- package/src/superlocalmemory/learning/quantization_scheduler.py +0 -320
- package/src/superlocalmemory/storage/access_control.py +0 -182
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
-
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
-
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
-
|
|
5
|
-
"""Weighted fair scheduler for high/low job lanes.
|
|
6
|
-
|
|
7
|
-
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
import threading
|
|
13
|
-
from typing import Literal
|
|
14
|
-
|
|
15
|
-
Lane = Literal["high", "low"]
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class WFQScheduler:
|
|
19
|
-
"""Deficit-round-robin-ish scheduler over two lanes.
|
|
20
|
-
|
|
21
|
-
Tracks served counts and picks the lane whose ratio is furthest
|
|
22
|
-
below its target share. Approximates the target ratio over any
|
|
23
|
-
rolling window without requiring a time source.
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
def __init__(self, high_weight: int = 70, low_weight: int = 30) -> None:
|
|
27
|
-
if high_weight <= 0 or low_weight <= 0:
|
|
28
|
-
raise ValueError("weights must be positive")
|
|
29
|
-
self.high_weight = high_weight
|
|
30
|
-
self.low_weight = low_weight
|
|
31
|
-
self._served = {"high": 0, "low": 0}
|
|
32
|
-
self._lock = threading.Lock()
|
|
33
|
-
|
|
34
|
-
def pick_lane(self, *, has_high: bool, has_low: bool) -> Lane | None:
|
|
35
|
-
if not has_high and not has_low:
|
|
36
|
-
return None
|
|
37
|
-
if has_high and not has_low:
|
|
38
|
-
return "high"
|
|
39
|
-
if has_low and not has_high:
|
|
40
|
-
return "low"
|
|
41
|
-
with self._lock:
|
|
42
|
-
total = self._served["high"] + self._served["low"]
|
|
43
|
-
if total == 0:
|
|
44
|
-
return "high"
|
|
45
|
-
total_weight = self.high_weight + self.low_weight
|
|
46
|
-
high_target = self.high_weight / total_weight
|
|
47
|
-
low_target = self.low_weight / total_weight
|
|
48
|
-
high_ratio = self._served["high"] / total
|
|
49
|
-
low_ratio = self._served["low"] / total
|
|
50
|
-
# Pick whichever lane is further below its target
|
|
51
|
-
high_deficit = high_target - high_ratio
|
|
52
|
-
low_deficit = low_target - low_ratio
|
|
53
|
-
return "high" if high_deficit >= low_deficit else "low"
|
|
54
|
-
|
|
55
|
-
def record_served(self, lane: Lane) -> None:
|
|
56
|
-
with self._lock:
|
|
57
|
-
self._served[lane] += 1
|
|
58
|
-
|
|
59
|
-
def snapshot(self) -> dict[str, int]:
|
|
60
|
-
with self._lock:
|
|
61
|
-
return dict(self._served)
|
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
-
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
-
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
-
|
|
5
|
-
"""SuperLocalMemory v3.4.5 — Graph Pruning Engine.
|
|
6
|
-
|
|
7
|
-
Reduces graph edge count without losing meaningful connections.
|
|
8
|
-
Strategies:
|
|
9
|
-
1. Chain collapse: A→B→C → remove B→C if A→B exists with higher weight
|
|
10
|
-
2. Garbage entity removal: remove edges connected to garbage entities
|
|
11
|
-
3. Low-activity edge decay: edges between entities not accessed in 90+ days
|
|
12
|
-
|
|
13
|
-
CRITICAL RULE: NEVER delete atomic_facts. Only prune graph_edges.
|
|
14
|
-
Edges are derivable from facts — they can be regenerated.
|
|
15
|
-
Facts are the permanent record.
|
|
16
|
-
|
|
17
|
-
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
from __future__ import annotations
|
|
21
|
-
|
|
22
|
-
import logging
|
|
23
|
-
from datetime import datetime, timedelta, UTC
|
|
24
|
-
from typing import TYPE_CHECKING
|
|
25
|
-
|
|
26
|
-
if TYPE_CHECKING:
|
|
27
|
-
from superlocalmemory.storage.database import DatabaseManager
|
|
28
|
-
|
|
29
|
-
logger = logging.getLogger(__name__)
|
|
30
|
-
|
|
31
|
-
# Thresholds
|
|
32
|
-
LOW_ACTIVITY_DAYS = 90
|
|
33
|
-
CHAIN_COLLAPSE_MIN_WEIGHT_RATIO = 0.8
|
|
34
|
-
BATCH_SIZE = 500
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# ---------------------------------------------------------------------------
|
|
38
|
-
# Public API
|
|
39
|
-
# ---------------------------------------------------------------------------
|
|
40
|
-
|
|
41
|
-
def prune_graph(
|
|
42
|
-
db: DatabaseManager,
|
|
43
|
-
profile_id: str = "default",
|
|
44
|
-
dry_run: bool = False,
|
|
45
|
-
) -> dict[str, int]:
|
|
46
|
-
"""Prune graph edges using all strategies.
|
|
47
|
-
|
|
48
|
-
Returns counts of edges removed per strategy.
|
|
49
|
-
Safe to run repeatedly — idempotent.
|
|
50
|
-
"""
|
|
51
|
-
stats = {
|
|
52
|
-
"chain_collapsed": 0,
|
|
53
|
-
"garbage_removed": 0,
|
|
54
|
-
"low_activity_decayed": 0,
|
|
55
|
-
"total_removed": 0,
|
|
56
|
-
"edges_before": 0,
|
|
57
|
-
"edges_after": 0,
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
# Count before
|
|
61
|
-
rows = db.execute(
|
|
62
|
-
"SELECT COUNT(*) as c FROM graph_edges", ()
|
|
63
|
-
)
|
|
64
|
-
stats["edges_before"] = rows[0]["c"] if rows else 0
|
|
65
|
-
|
|
66
|
-
# Strategy 1: Chain collapse
|
|
67
|
-
collapsed = _collapse_chains(db, profile_id, dry_run)
|
|
68
|
-
stats["chain_collapsed"] = collapsed
|
|
69
|
-
|
|
70
|
-
# Strategy 2: Garbage entity edges
|
|
71
|
-
garbage = _remove_garbage_edges(db, profile_id, dry_run)
|
|
72
|
-
stats["garbage_removed"] = garbage
|
|
73
|
-
|
|
74
|
-
# Strategy 3: Low-activity edge decay
|
|
75
|
-
decayed = _decay_low_activity_edges(db, profile_id, dry_run)
|
|
76
|
-
stats["low_activity_decayed"] = decayed
|
|
77
|
-
|
|
78
|
-
stats["total_removed"] = collapsed + garbage + decayed
|
|
79
|
-
|
|
80
|
-
# Count after
|
|
81
|
-
rows = db.execute(
|
|
82
|
-
"SELECT COUNT(*) as c FROM graph_edges", ()
|
|
83
|
-
)
|
|
84
|
-
stats["edges_after"] = rows[0]["c"] if rows else 0
|
|
85
|
-
|
|
86
|
-
if stats["total_removed"] > 0:
|
|
87
|
-
logger.info(
|
|
88
|
-
"Graph pruning: %d edges removed (%d → %d)",
|
|
89
|
-
stats["total_removed"], stats["edges_before"], stats["edges_after"],
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
return stats
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
# ---------------------------------------------------------------------------
|
|
96
|
-
# Strategy 1: Chain Collapse
|
|
97
|
-
# ---------------------------------------------------------------------------
|
|
98
|
-
|
|
99
|
-
def _collapse_chains(db: DatabaseManager, profile_id: str, dry_run: bool) -> int:
|
|
100
|
-
"""Collapse redundant chain edges.
|
|
101
|
-
|
|
102
|
-
If A→B (weight=0.9) and B→C (weight=0.5), and A→C also exists,
|
|
103
|
-
remove B→C if A→C weight >= B→C weight * threshold.
|
|
104
|
-
|
|
105
|
-
This preserves the semantic connection (A→C is stronger) while
|
|
106
|
-
removing intermediate edges.
|
|
107
|
-
"""
|
|
108
|
-
try:
|
|
109
|
-
rows = db.execute("""
|
|
110
|
-
SELECT ge1.source_id as a, ge1.target_id as b, ge1.weight as w_ab,
|
|
111
|
-
ge2.source_id as b2, ge2.target_id as c, ge2.weight as w_bc,
|
|
112
|
-
ge3.weight as w_ac
|
|
113
|
-
FROM graph_edges ge1
|
|
114
|
-
JOIN graph_edges ge2 ON ge1.target_id = ge2.source_id
|
|
115
|
-
LEFT JOIN graph_edges ge3 ON ge1.source_id = ge3.source_id
|
|
116
|
-
AND ge2.target_id = ge3.target_id
|
|
117
|
-
WHERE ge3.weight >= ge2.weight * ?
|
|
118
|
-
LIMIT ?
|
|
119
|
-
""", (CHAIN_COLLAPSE_MIN_WEIGHT_RATIO, BATCH_SIZE))
|
|
120
|
-
except Exception as exc:
|
|
121
|
-
logger.warning("Chain collapse query failed: %s", exc)
|
|
122
|
-
return 0
|
|
123
|
-
|
|
124
|
-
remove_ids = []
|
|
125
|
-
for row in rows:
|
|
126
|
-
b_id = row["b"]
|
|
127
|
-
c_id = row["c"]
|
|
128
|
-
# Remove B→C edge
|
|
129
|
-
if not dry_run:
|
|
130
|
-
db.execute(
|
|
131
|
-
"DELETE FROM graph_edges WHERE source_id = ? AND target_id = ?",
|
|
132
|
-
(b_id, c_id),
|
|
133
|
-
)
|
|
134
|
-
remove_ids.append((b_id, c_id))
|
|
135
|
-
|
|
136
|
-
if remove_ids and not dry_run:
|
|
137
|
-
logger.info("Chain collapse: removed %d edges", len(remove_ids))
|
|
138
|
-
|
|
139
|
-
return len(remove_ids)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
# ---------------------------------------------------------------------------
|
|
143
|
-
# Strategy 2: Garbage Entity Edges
|
|
144
|
-
# ---------------------------------------------------------------------------
|
|
145
|
-
|
|
146
|
-
def _remove_garbage_edges(db: DatabaseManager, profile_id: str, dry_run: bool) -> int:
|
|
147
|
-
"""Remove edges connected to garbage/blacklisted entities."""
|
|
148
|
-
try:
|
|
149
|
-
rows = db.execute("""
|
|
150
|
-
SELECT ge.source_id, ge.target_id
|
|
151
|
-
FROM graph_edges ge
|
|
152
|
-
WHERE ge.source_id IN (SELECT term FROM entity_blacklist)
|
|
153
|
-
OR ge.target_id IN (SELECT term FROM entity_blacklist)
|
|
154
|
-
LIMIT ?
|
|
155
|
-
""", (BATCH_SIZE,))
|
|
156
|
-
except Exception:
|
|
157
|
-
return 0
|
|
158
|
-
|
|
159
|
-
count = 0
|
|
160
|
-
for row in rows:
|
|
161
|
-
if not dry_run:
|
|
162
|
-
db.execute(
|
|
163
|
-
"DELETE FROM graph_edges WHERE source_id = ? AND target_id = ?",
|
|
164
|
-
(row["source_id"], row["target_id"]),
|
|
165
|
-
)
|
|
166
|
-
count += 1
|
|
167
|
-
|
|
168
|
-
if count and not dry_run:
|
|
169
|
-
logger.info("Garbage edges removed: %d", count)
|
|
170
|
-
|
|
171
|
-
return count
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
# ---------------------------------------------------------------------------
|
|
175
|
-
# Strategy 3: Low-Activity Edge Decay
|
|
176
|
-
# ---------------------------------------------------------------------------
|
|
177
|
-
|
|
178
|
-
def _decay_low_activity_edges(
|
|
179
|
-
db: DatabaseManager, profile_id: str, dry_run: bool,
|
|
180
|
-
) -> int:
|
|
181
|
-
"""Remove edges between entities not accessed in 90+ days.
|
|
182
|
-
|
|
183
|
-
Only removes edges where BOTH connected entities have no recent access.
|
|
184
|
-
"""
|
|
185
|
-
cutoff = (datetime.now(UTC) - timedelta(days=LOW_ACTIVITY_DAYS)).isoformat()
|
|
186
|
-
|
|
187
|
-
try:
|
|
188
|
-
rows = db.execute("""
|
|
189
|
-
SELECT ge.source_id, ge.target_id
|
|
190
|
-
FROM graph_edges ge
|
|
191
|
-
WHERE ge.source_id NOT IN (
|
|
192
|
-
SELECT DISTINCT entity_id FROM fact_access_log
|
|
193
|
-
WHERE accessed_at >= ?
|
|
194
|
-
)
|
|
195
|
-
AND ge.target_id NOT IN (
|
|
196
|
-
SELECT DISTINCT entity_id FROM fact_access_log
|
|
197
|
-
WHERE accessed_at >= ?
|
|
198
|
-
)
|
|
199
|
-
LIMIT ?
|
|
200
|
-
""", (cutoff, cutoff, BATCH_SIZE))
|
|
201
|
-
except Exception:
|
|
202
|
-
return 0
|
|
203
|
-
|
|
204
|
-
count = 0
|
|
205
|
-
for row in rows:
|
|
206
|
-
if not dry_run:
|
|
207
|
-
db.execute(
|
|
208
|
-
"DELETE FROM graph_edges WHERE source_id = ? AND target_id = ?",
|
|
209
|
-
(row["source_id"], row["target_id"]),
|
|
210
|
-
)
|
|
211
|
-
count += 1
|
|
212
|
-
|
|
213
|
-
if count and not dry_run:
|
|
214
|
-
logger.info("Low-activity edges decayed: %d", count)
|
|
215
|
-
|
|
216
|
-
return count
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
-
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
-
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
-
|
|
5
|
-
"""Queue-backed dispatcher coordinator.
|
|
6
|
-
|
|
7
|
-
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
import os
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
from typing import Any
|
|
15
|
-
|
|
16
|
-
from superlocalmemory.core import rate_limit as rl
|
|
17
|
-
from superlocalmemory.core import recall_queue as rq
|
|
18
|
-
from superlocalmemory.core.engine_lock import EngineRWLock
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class QueueDispatcher:
|
|
22
|
-
"""Coordinates rate-limit check, enqueue, and poll for callers."""
|
|
23
|
-
|
|
24
|
-
def __init__(
|
|
25
|
-
self,
|
|
26
|
-
*,
|
|
27
|
-
db_path: Path | str,
|
|
28
|
-
global_rps: float = 100.0,
|
|
29
|
-
per_pid_rps: float = 30.0,
|
|
30
|
-
per_agent_rps: float = 10.0,
|
|
31
|
-
) -> None:
|
|
32
|
-
self.queue = rq.RecallQueue(db_path=db_path)
|
|
33
|
-
self.engine_lock = EngineRWLock()
|
|
34
|
-
self._rate = rl.LayeredRateLimiter(
|
|
35
|
-
global_rps=global_rps,
|
|
36
|
-
per_pid_rps=per_pid_rps,
|
|
37
|
-
per_agent_rps=per_agent_rps,
|
|
38
|
-
)
|
|
39
|
-
# Module handles kept for test introspection.
|
|
40
|
-
self.rl = rl
|
|
41
|
-
self.rq = rq
|
|
42
|
-
|
|
43
|
-
def _check_rate(self, *, pid: int, agent_id: str | None) -> None:
|
|
44
|
-
self._rate.check_and_consume(pid=pid, agent_id=agent_id)
|
|
45
|
-
|
|
46
|
-
def dispatch(
|
|
47
|
-
self,
|
|
48
|
-
*,
|
|
49
|
-
query: str,
|
|
50
|
-
limit_n: int,
|
|
51
|
-
mode: str,
|
|
52
|
-
agent_id: str,
|
|
53
|
-
session_id: str,
|
|
54
|
-
tenant_id: str = "",
|
|
55
|
-
namespace: str = "",
|
|
56
|
-
priority: str = "high",
|
|
57
|
-
stall_timeout_s: float = 25.0,
|
|
58
|
-
timeout_s: float = 30.0,
|
|
59
|
-
) -> dict[str, Any]:
|
|
60
|
-
self._check_rate(pid=os.getpid(), agent_id=agent_id)
|
|
61
|
-
rid = self.queue.enqueue(
|
|
62
|
-
query=query, limit_n=limit_n, mode=mode,
|
|
63
|
-
agent_id=agent_id, session_id=session_id,
|
|
64
|
-
tenant_id=tenant_id, namespace=namespace,
|
|
65
|
-
priority=priority, stall_timeout_s=stall_timeout_s,
|
|
66
|
-
)
|
|
67
|
-
try:
|
|
68
|
-
return self.queue.poll_result(rid, timeout_s=timeout_s)
|
|
69
|
-
finally:
|
|
70
|
-
self.queue.unsubscribe(rid)
|
|
71
|
-
|
|
72
|
-
def close(self) -> None:
|
|
73
|
-
self.queue.close()
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
-
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
-
# Part of SuperLocalMemory v3.4.22 — Stage 8 SB-5
|
|
4
|
-
|
|
5
|
-
"""Path-level opt-out via ``.slmignore``.
|
|
6
|
-
|
|
7
|
-
A repository-scoped escape hatch: drop a ``.slmignore`` at any
|
|
8
|
-
ancestor of a workspace and SLM will skip hooks / recall / remember
|
|
9
|
-
for any path inside that ancestor. Roughly the shape of ``.gitignore``
|
|
10
|
-
but with simpler matching — one path pattern per line, ``#`` comments,
|
|
11
|
-
whitespace stripped.
|
|
12
|
-
|
|
13
|
-
Matching rules (intentionally minimal):
|
|
14
|
-
|
|
15
|
-
- Lines starting with ``#`` or empty ⇒ ignored.
|
|
16
|
-
- A line like ``node_modules`` matches any path segment named
|
|
17
|
-
``node_modules`` anywhere in the resolved absolute path.
|
|
18
|
-
- A line starting with ``/`` is an absolute-prefix match
|
|
19
|
-
(``/Users/me/secret`` matches everything under that dir).
|
|
20
|
-
- Glob chars ``*`` / ``?`` are treated literally — keep it boring,
|
|
21
|
-
avoid re-implementing ``.gitignore``'s subtleties.
|
|
22
|
-
|
|
23
|
-
Cache: the parsed ignore list is memoised per ignore-file path + mtime.
|
|
24
|
-
Look-up cost at the hook hot path is O(depth × patterns) and
|
|
25
|
-
patterns ≤ 50 in practice.
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
from __future__ import annotations
|
|
29
|
-
|
|
30
|
-
import os
|
|
31
|
-
from pathlib import Path
|
|
32
|
-
|
|
33
|
-
_FILENAME = ".slmignore"
|
|
34
|
-
_CACHE: dict[tuple[str, float], tuple[str, ...]] = {}
|
|
35
|
-
_CACHE_CAP = 64
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _load_patterns(ignore_path: Path) -> tuple[str, ...]:
|
|
39
|
-
"""Parse one ``.slmignore`` file, memoised by path + mtime."""
|
|
40
|
-
try:
|
|
41
|
-
stat = ignore_path.stat()
|
|
42
|
-
except OSError:
|
|
43
|
-
return ()
|
|
44
|
-
key = (str(ignore_path), stat.st_mtime)
|
|
45
|
-
cached = _CACHE.get(key)
|
|
46
|
-
if cached is not None:
|
|
47
|
-
return cached
|
|
48
|
-
try:
|
|
49
|
-
raw = ignore_path.read_text(encoding="utf-8", errors="ignore")
|
|
50
|
-
except OSError:
|
|
51
|
-
return ()
|
|
52
|
-
patterns: list[str] = []
|
|
53
|
-
for line in raw.splitlines():
|
|
54
|
-
stripped = line.strip()
|
|
55
|
-
if not stripped or stripped.startswith("#"):
|
|
56
|
-
continue
|
|
57
|
-
patterns.append(stripped)
|
|
58
|
-
result = tuple(patterns)
|
|
59
|
-
if len(_CACHE) >= _CACHE_CAP:
|
|
60
|
-
_CACHE.clear()
|
|
61
|
-
_CACHE[key] = result
|
|
62
|
-
return result
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def _iter_ancestor_ignores(target: Path) -> list[tuple[Path, tuple[str, ...]]]:
|
|
66
|
-
"""Walk from ``target`` up to the filesystem root collecting ignore files."""
|
|
67
|
-
hits: list[tuple[Path, tuple[str, ...]]] = []
|
|
68
|
-
seen_dirs: set[Path] = set()
|
|
69
|
-
probe = target if target.is_dir() else target.parent
|
|
70
|
-
while probe not in seen_dirs:
|
|
71
|
-
seen_dirs.add(probe)
|
|
72
|
-
candidate = probe / _FILENAME
|
|
73
|
-
if candidate.is_file():
|
|
74
|
-
patterns = _load_patterns(candidate)
|
|
75
|
-
if patterns:
|
|
76
|
-
hits.append((candidate.parent, patterns))
|
|
77
|
-
if probe.parent == probe:
|
|
78
|
-
break
|
|
79
|
-
probe = probe.parent
|
|
80
|
-
return hits
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def path_is_ignored(target: str | Path) -> bool:
|
|
84
|
-
"""Return True iff any ancestor ``.slmignore`` ignores the given path.
|
|
85
|
-
|
|
86
|
-
Absolute paths are resolved (symlinks preserved — we match on name,
|
|
87
|
-
not realpath, so a symlink into an ignored dir still matches).
|
|
88
|
-
Non-existent targets are allowed; we walk the theoretical ancestry.
|
|
89
|
-
"""
|
|
90
|
-
p = Path(target)
|
|
91
|
-
try:
|
|
92
|
-
abs_path = p.resolve(strict=False)
|
|
93
|
-
except OSError:
|
|
94
|
-
abs_path = p
|
|
95
|
-
segments = set(abs_path.parts)
|
|
96
|
-
for ignore_dir, patterns in _iter_ancestor_ignores(abs_path):
|
|
97
|
-
for pat in patterns:
|
|
98
|
-
if pat.startswith("/"):
|
|
99
|
-
# Absolute prefix: the ignore file's directory provides the
|
|
100
|
-
# anchor for relative-looking absolute patterns.
|
|
101
|
-
candidate = Path(pat)
|
|
102
|
-
try:
|
|
103
|
-
abs_path.relative_to(candidate)
|
|
104
|
-
return True
|
|
105
|
-
except ValueError:
|
|
106
|
-
continue
|
|
107
|
-
else:
|
|
108
|
-
# Match any path segment.
|
|
109
|
-
if pat in segments:
|
|
110
|
-
return True
|
|
111
|
-
# Also honour a per-ignore-dir relative path.
|
|
112
|
-
rel = abs_path.relative_to(ignore_dir) if (
|
|
113
|
-
ignore_dir in abs_path.parents or ignore_dir == abs_path
|
|
114
|
-
) else None
|
|
115
|
-
if rel is not None and pat in set(rel.parts):
|
|
116
|
-
return True
|
|
117
|
-
return False
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def clear_cache() -> None:
|
|
121
|
-
"""Test-only helper: drop the memoised pattern cache."""
|
|
122
|
-
_CACHE.clear()
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
__all__ = ("path_is_ignored", "clear_cache")
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
-
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
-
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
-
|
|
5
|
-
"""SuperLocalMemory V3 -- Parent Heartbeat Monitor.
|
|
6
|
-
|
|
7
|
-
Daemon thread that checks if the parent process (IDE/Claude session) is
|
|
8
|
-
still alive. If the parent dies, initiates graceful shutdown to prevent
|
|
9
|
-
zombie SLM processes consuming 1.5-2 GB each.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
|
-
import logging
|
|
15
|
-
import os
|
|
16
|
-
import threading
|
|
17
|
-
from typing import Callable, Optional
|
|
18
|
-
|
|
19
|
-
logger = logging.getLogger(__name__)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class HeartbeatMonitor:
|
|
23
|
-
"""Monitor parent process liveness via a daemon thread.
|
|
24
|
-
|
|
25
|
-
When the parent PID is detected as dead, calls the provided
|
|
26
|
-
shutdown_callback. The monitoring thread is a daemon thread
|
|
27
|
-
(auto-dies with the main process per HR-06).
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
def __init__(
|
|
31
|
-
self,
|
|
32
|
-
parent_pid: int,
|
|
33
|
-
interval_seconds: int,
|
|
34
|
-
shutdown_callback: Callable[[], None],
|
|
35
|
-
) -> None:
|
|
36
|
-
self._parent_pid = parent_pid
|
|
37
|
-
self._interval = interval_seconds
|
|
38
|
-
self._shutdown_callback = shutdown_callback
|
|
39
|
-
self._thread: Optional[threading.Thread] = None
|
|
40
|
-
self._stop_event = threading.Event()
|
|
41
|
-
self._running = False
|
|
42
|
-
|
|
43
|
-
# -- Lifecycle ----------------------------------------------------------
|
|
44
|
-
|
|
45
|
-
def start(self) -> None:
|
|
46
|
-
"""Start the heartbeat monitoring daemon thread."""
|
|
47
|
-
if self._running:
|
|
48
|
-
logger.warning("Heartbeat monitor already running")
|
|
49
|
-
return
|
|
50
|
-
|
|
51
|
-
# HR-02 equivalent: refuse to monitor PID 0 or 1
|
|
52
|
-
if self._parent_pid <= 1:
|
|
53
|
-
logger.warning(
|
|
54
|
-
"Refusing to monitor PID %d (<= 1), heartbeat not started",
|
|
55
|
-
self._parent_pid,
|
|
56
|
-
)
|
|
57
|
-
return
|
|
58
|
-
|
|
59
|
-
self._stop_event.clear()
|
|
60
|
-
self._thread = threading.Thread(
|
|
61
|
-
target=self._monitor_loop,
|
|
62
|
-
name="slm-heartbeat",
|
|
63
|
-
daemon=True,
|
|
64
|
-
)
|
|
65
|
-
self._thread.start()
|
|
66
|
-
self._running = True
|
|
67
|
-
logger.info(
|
|
68
|
-
"Heartbeat monitor started: watching parent PID %d every %ds",
|
|
69
|
-
self._parent_pid,
|
|
70
|
-
self._interval,
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
def stop(self) -> None:
|
|
74
|
-
"""Stop the heartbeat monitor gracefully."""
|
|
75
|
-
if not self._running:
|
|
76
|
-
return
|
|
77
|
-
|
|
78
|
-
self._stop_event.set()
|
|
79
|
-
if self._thread is not None and self._thread.is_alive():
|
|
80
|
-
self._thread.join(timeout=self._interval + 2)
|
|
81
|
-
|
|
82
|
-
self._running = False
|
|
83
|
-
logger.info("Heartbeat monitor stopped")
|
|
84
|
-
|
|
85
|
-
# -- Properties ---------------------------------------------------------
|
|
86
|
-
|
|
87
|
-
@property
|
|
88
|
-
def is_running(self) -> bool:
|
|
89
|
-
"""Whether the monitor thread is active."""
|
|
90
|
-
return self._running
|
|
91
|
-
|
|
92
|
-
# -- Internal -----------------------------------------------------------
|
|
93
|
-
|
|
94
|
-
def _monitor_loop(self) -> None:
|
|
95
|
-
"""Heartbeat loop running in daemon thread.
|
|
96
|
-
|
|
97
|
-
Uses threading.Event.wait(timeout) instead of time.sleep()
|
|
98
|
-
because Event.wait() is immediately interruptible by stop(),
|
|
99
|
-
while sleep() blocks for the full duration.
|
|
100
|
-
"""
|
|
101
|
-
logger.debug(
|
|
102
|
-
"Heartbeat loop started for parent PID %d", self._parent_pid
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
while not self._stop_event.is_set():
|
|
106
|
-
stopped = self._stop_event.wait(timeout=self._interval)
|
|
107
|
-
if stopped:
|
|
108
|
-
break
|
|
109
|
-
|
|
110
|
-
if not self._is_parent_alive():
|
|
111
|
-
logger.warning(
|
|
112
|
-
"Parent PID %d died, initiating graceful shutdown",
|
|
113
|
-
self._parent_pid,
|
|
114
|
-
)
|
|
115
|
-
try:
|
|
116
|
-
self._shutdown_callback()
|
|
117
|
-
except Exception:
|
|
118
|
-
logger.exception("Shutdown callback failed")
|
|
119
|
-
break
|
|
120
|
-
|
|
121
|
-
logger.debug("Heartbeat loop exited")
|
|
122
|
-
|
|
123
|
-
def _is_parent_alive(self) -> bool:
|
|
124
|
-
"""Check if parent PID is still a running process.
|
|
125
|
-
|
|
126
|
-
Conservative: returns True on PermissionError (parent exists
|
|
127
|
-
but is owned by another user).
|
|
128
|
-
"""
|
|
129
|
-
if self._parent_pid <= 1:
|
|
130
|
-
return False
|
|
131
|
-
|
|
132
|
-
try:
|
|
133
|
-
os.kill(self._parent_pid, 0)
|
|
134
|
-
return True
|
|
135
|
-
except ProcessLookupError:
|
|
136
|
-
return False
|
|
137
|
-
except PermissionError:
|
|
138
|
-
return True # Alive, different user -- conservative
|
|
139
|
-
except OSError:
|
|
140
|
-
return False
|