superlocalmemory 3.7.8 → 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/.claude-plugin/marketplace.json +1 -1
- package/ATTRIBUTION.md +1 -3
- package/CHANGELOG.md +129 -0
- package/README.md +205 -123
- package/package.json +12 -3
- package/plugin/.claude-plugin/plugin.json +2 -3
- package/plugin/CLAUDE.md +8 -8
- package/plugin/agents/slm-governance-advisor.md +80 -0
- package/plugin/agents/slm-loop-runner.md +71 -0
- package/plugin/agents/slm-memory-advisor.md +10 -5
- package/plugin/agents/slm-optimize-advisor.md +9 -3
- package/plugin/commands/slm-loop.md +31 -0
- package/plugin/hooks/hooks.json +79 -0
- package/plugin/requirements.txt +1 -1
- package/plugin/scripts/slm-launch +46 -7
- package/plugin/settings.json +9 -0
- package/plugin/skills/slm-cache/SKILL.md +9 -1
- package/plugin/skills/slm-compress/SKILL.md +8 -1
- package/plugin/skills/slm-governance/SKILL.md +248 -0
- package/plugin/skills/slm-graph/SKILL.md +17 -3
- package/plugin/skills/slm-loop/SKILL.md +99 -0
- package/plugin/skills/slm-mesh/SKILL.md +282 -0
- package/plugin/skills/slm-profile/SKILL.md +148 -0
- package/plugin/skills/slm-recall/SKILL.md +46 -10
- package/plugin/skills/slm-remember/SKILL.md +48 -1
- package/plugin/skills/slm-scope/SKILL.md +176 -0
- package/plugin/skills/slm-session/SKILL.md +24 -1
- package/plugin/skills/slm-status/SKILL.md +18 -1
- package/plugin-src/rules/AGENTS.md +57 -18
- package/plugin-src/skills/slm-cache/SKILL.md +9 -1
- package/plugin-src/skills/slm-compress/SKILL.md +8 -1
- package/plugin-src/skills/slm-graph/SKILL.md +17 -3
- package/plugin-src/skills/slm-recall/SKILL.md +46 -10
- package/plugin-src/skills/slm-remember/SKILL.md +48 -1
- package/plugin-src/skills/slm-session/SKILL.md +24 -1
- package/plugin-src/skills/slm-status/SKILL.md +18 -1
- package/pyproject.toml +2 -1
- package/scripts/postinstall/validation.js +2 -0
- package/scripts/postinstall-interactive.js +74 -2
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/__init__.py +3 -0
- package/src/superlocalmemory/access/rbac.py +477 -0
- package/src/superlocalmemory/cli/commands.py +228 -17
- package/src/superlocalmemory/cli/compress_cmd.py +17 -7
- package/src/superlocalmemory/cli/daemon.py +7 -0
- package/src/superlocalmemory/cli/loop_cmd.py +187 -0
- package/src/superlocalmemory/cli/main.py +49 -8
- package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
- package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
- package/src/superlocalmemory/cli/pending_store.py +49 -13
- package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
- package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
- package/src/superlocalmemory/cli/setup_wizard.py +22 -13
- package/src/superlocalmemory/cli/version_banner.py +17 -3
- package/src/superlocalmemory/compliance/audit.py +6 -0
- package/src/superlocalmemory/compliance/gdpr.py +128 -138
- package/src/superlocalmemory/compliance/retention.py +176 -45
- package/src/superlocalmemory/core/backend_orchestrator.py +23 -59
- package/src/superlocalmemory/core/community_summary.py +267 -0
- package/src/superlocalmemory/core/config.py +216 -3
- package/src/superlocalmemory/core/consolidation_engine.py +95 -22
- package/src/superlocalmemory/core/context_cache.py +61 -18
- package/src/superlocalmemory/core/embedding_worker.py +21 -7
- package/src/superlocalmemory/core/embeddings.py +131 -46
- package/src/superlocalmemory/core/engine.py +41 -22
- package/src/superlocalmemory/core/engine_ingestion.py +359 -43
- package/src/superlocalmemory/core/engine_wiring.py +13 -0
- package/src/superlocalmemory/core/entity_community.py +178 -0
- package/src/superlocalmemory/core/graph_analyzer.py +39 -2
- package/src/superlocalmemory/core/graph_pruner.py +13 -8
- package/src/superlocalmemory/core/ingestion_command.py +134 -25
- package/src/superlocalmemory/core/injection.py +12 -7
- package/src/superlocalmemory/core/key_expander.py +138 -0
- package/src/superlocalmemory/core/maintenance.py +23 -0
- package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
- package/src/superlocalmemory/core/modes.py +1 -1
- package/src/superlocalmemory/core/mutations.py +2 -2
- package/src/superlocalmemory/core/pii.py +105 -0
- package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -3
- package/src/superlocalmemory/core/recall_worker.py +20 -6
- package/src/superlocalmemory/core/scale_engine.py +60 -1
- package/src/superlocalmemory/core/security_primitives.py +40 -2
- package/src/superlocalmemory/core/store_pipeline.py +186 -29
- package/src/superlocalmemory/core/worker_pool.py +21 -6
- package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
- package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
- package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
- package/src/superlocalmemory/evolution/evolution_store.py +122 -45
- package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
- package/src/superlocalmemory/evolution/model_selection.py +160 -0
- package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
- package/src/superlocalmemory/evolution/triggers.py +22 -13
- package/src/superlocalmemory/graph/cozo_backend.py +43 -20
- package/src/superlocalmemory/hooks/adapter_base.py +5 -1
- package/src/superlocalmemory/hooks/auto_recall.py +13 -1
- package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
- package/src/superlocalmemory/hooks/codex_assets.py +64 -5
- package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
- package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
- package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
- package/src/superlocalmemory/hooks/portable_kit.py +148 -3
- package/src/superlocalmemory/infra/backup.py +12 -1
- package/src/superlocalmemory/infra/daemon_identity.py +40 -4
- package/src/superlocalmemory/infra/data_root.py +43 -4
- package/src/superlocalmemory/infra/event_bus.py +107 -24
- package/src/superlocalmemory/infra/rate_limiter.py +93 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
- package/src/superlocalmemory/ingestion/credentials.py +1 -1
- package/src/superlocalmemory/learning/cross_project.py +28 -19
- 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/reward_proxy.py +42 -9
- package/src/superlocalmemory/learning/source_quality.py +523 -1
- package/src/superlocalmemory/loops/__init__.py +56 -0
- package/src/superlocalmemory/loops/budget.py +58 -0
- package/src/superlocalmemory/loops/engine.py +164 -0
- package/src/superlocalmemory/loops/ledger.py +263 -0
- package/src/superlocalmemory/loops/models.py +152 -0
- package/src/superlocalmemory/loops/rules.py +52 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
- package/src/superlocalmemory/mcp/profiles.py +103 -0
- package/src/superlocalmemory/mcp/server.py +32 -79
- package/src/superlocalmemory/mcp/tools_active.py +4 -7
- package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
- package/src/superlocalmemory/mcp/tools_core.py +12 -4
- package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
- package/src/superlocalmemory/mcp/tools_learning.py +2 -2
- package/src/superlocalmemory/mcp/tools_loops.py +300 -0
- package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
- package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
- package/src/superlocalmemory/mesh/broker.py +237 -129
- package/src/superlocalmemory/mesh/remote_sync.py +50 -8
- package/src/superlocalmemory/optimize/NOTICE +1 -6
- package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
- package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
- package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
- package/src/superlocalmemory/optimize/compress/align.py +32 -26
- package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
- package/src/superlocalmemory/optimize/compress/router.py +105 -22
- package/src/superlocalmemory/optimize/config/defaults.py +1 -1
- package/src/superlocalmemory/optimize/config/schema.py +87 -4
- package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
- package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
- package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
- package/src/superlocalmemory/optimize/storage/db.py +38 -9
- package/src/superlocalmemory/optimize/storage/schema.py +10 -0
- package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
- package/src/superlocalmemory/retrieval/engine.py +221 -47
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
- package/src/superlocalmemory/retrieval/reranker.py +3 -4
- package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
- package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
- package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
- package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
- package/src/superlocalmemory/retrieval/time_window.py +181 -0
- package/src/superlocalmemory/server/api.py +4 -4
- package/src/superlocalmemory/server/config_file.py +90 -0
- package/src/superlocalmemory/server/origin.py +50 -0
- package/src/superlocalmemory/server/profile_runtime.py +125 -8
- package/src/superlocalmemory/server/rbac_enforce.py +142 -0
- package/src/superlocalmemory/server/recall_health.py +24 -3
- package/src/superlocalmemory/server/recall_serializer.py +19 -1
- package/src/superlocalmemory/server/routes/abstraction.py +115 -0
- package/src/superlocalmemory/server/routes/agents.py +128 -38
- package/src/superlocalmemory/server/routes/backup.py +317 -70
- package/src/superlocalmemory/server/routes/behavioral.py +349 -71
- package/src/superlocalmemory/server/routes/brain.py +69 -12
- package/src/superlocalmemory/server/routes/chat.py +10 -5
- package/src/superlocalmemory/server/routes/compliance.py +171 -21
- package/src/superlocalmemory/server/routes/config_api.py +438 -0
- package/src/superlocalmemory/server/routes/data_io.py +30 -8
- package/src/superlocalmemory/server/routes/entity.py +108 -26
- package/src/superlocalmemory/server/routes/events.py +24 -8
- package/src/superlocalmemory/server/routes/evolution.py +189 -68
- package/src/superlocalmemory/server/routes/helpers.py +16 -1
- package/src/superlocalmemory/server/routes/ingest.py +7 -4
- package/src/superlocalmemory/server/routes/insights.py +3 -3
- package/src/superlocalmemory/server/routes/learning.py +289 -118
- package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
- package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
- package/src/superlocalmemory/server/routes/memories.py +182 -57
- package/src/superlocalmemory/server/routes/mesh.py +200 -31
- package/src/superlocalmemory/server/routes/optimize.py +33 -1
- package/src/superlocalmemory/server/routes/prewarm.py +2 -0
- package/src/superlocalmemory/server/routes/profiles.py +63 -17
- package/src/superlocalmemory/server/routes/ratelimit.py +132 -0
- package/src/superlocalmemory/server/routes/rbac.py +367 -0
- package/src/superlocalmemory/server/routes/stats.py +103 -158
- package/src/superlocalmemory/server/routes/tiers.py +11 -9
- package/src/superlocalmemory/server/routes/token.py +3 -13
- package/src/superlocalmemory/server/routes/v3_api.py +247 -89
- package/src/superlocalmemory/server/routes/ws.py +5 -2
- package/src/superlocalmemory/server/security_middleware.py +12 -5
- package/src/superlocalmemory/server/ui.py +20 -5
- package/src/superlocalmemory/server/unified_daemon.py +827 -72
- package/src/superlocalmemory/server/write_identity.py +38 -8
- package/src/superlocalmemory/storage/database.py +265 -53
- package/src/superlocalmemory/storage/migration_runner.py +132 -1
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
- package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
- package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
- package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
- package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
- package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
- package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
- package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -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/models.py +4 -0
- package/src/superlocalmemory/storage/schema.py +136 -1
- package/src/superlocalmemory/storage/schema_v32.py +2 -0
- package/src/superlocalmemory/storage/schema_v343.py +24 -12
- package/src/superlocalmemory/storage/schema_v347.py +4 -0
- package/src/superlocalmemory/trust/gate.py +49 -8
- package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
- package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
- package/src/superlocalmemory/ui/css/design-system.css +621 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
- package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
- package/src/superlocalmemory/ui/favicon.svg +35 -4
- package/src/superlocalmemory/ui/index.html +303 -173
- package/src/superlocalmemory/ui/js/brain.js +5 -20
- package/src/superlocalmemory/ui/js/core.js +100 -41
- package/src/superlocalmemory/ui/js/dashboard.js +403 -65
- package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
- package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
- package/src/superlocalmemory/ui/js/math-health.js +1 -1
- package/src/superlocalmemory/ui/js/memories.js +15 -4
- package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
- package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
- package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
- package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
- package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
- package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
- package/src/superlocalmemory/ui/js/od-agents.js +544 -0
- package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
- package/src/superlocalmemory/ui/js/od-backup.js +871 -0
- package/src/superlocalmemory/ui/js/od-brain.js +816 -0
- package/src/superlocalmemory/ui/js/od-entities.js +579 -0
- package/src/superlocalmemory/ui/js/od-graph.js +600 -0
- package/src/superlocalmemory/ui/js/od-health.js +539 -0
- package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
- package/src/superlocalmemory/ui/js/od-memories.js +929 -0
- package/src/superlocalmemory/ui/js/od-mesh.js +553 -0
- package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
- package/src/superlocalmemory/ui/js/od-settings.js +1107 -0
- package/src/superlocalmemory/ui/js/od-shell.js +809 -0
- package/src/superlocalmemory/ui/js/od-skills.js +600 -0
- package/src/superlocalmemory/ui/js/od-team.js +258 -0
- package/src/superlocalmemory/ui/js/profiles.js +159 -46
- package/src/superlocalmemory/ui/js/settings.js +17 -3
- package/src/superlocalmemory/ui/js/timeline.js +34 -5
- package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
- package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
- package/plugin-src/.mcp.json +0 -12
- package/plugin-src/agents/slm-memory-advisor.md +0 -44
- package/plugin-src/agents/slm-optimize-advisor.md +0 -38
- package/plugin-src/hooks/.gitkeep +0 -0
- package/plugin-src/hooks/hooks.json +0 -23
- package/plugin-src/manifest.json +0 -25
- 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 -23
- package/plugin-src/scripts/slm-launch.bat +0 -23
- package/plugin-src/settings.json +0 -16
- package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
|
@@ -124,15 +124,36 @@ def run_health_tick(engine, state: RecallHealth, *, probe: str = DEFAULT_PROBE,
|
|
|
124
124
|
|
|
125
125
|
Mutates and returns ``state``. Never raises — a timed-out / failing recall
|
|
126
126
|
marks the path unhealthy instead of propagating.
|
|
127
|
+
|
|
128
|
+
Uses ``operation_nowait()`` when a runtime is supplied so that a pending
|
|
129
|
+
profile switch is not blocked by the health-probe recall. If a transition
|
|
130
|
+
is in progress the tick is skipped entirely — the next scheduled tick will
|
|
131
|
+
re-warm once the switch has committed.
|
|
127
132
|
"""
|
|
128
133
|
state.checks += 1
|
|
129
134
|
|
|
130
135
|
# Tier 1: re-warm. A real full-fusion recall keeps the graph page cache hot
|
|
131
136
|
# and the embedder resident.
|
|
137
|
+
# Cooperative preemption: use operation_nowait() so a pending profile switch
|
|
138
|
+
# is not held hostage by a slow full-fusion recall (2–10s with fast=False).
|
|
132
139
|
try:
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
140
|
+
if runtime is not None:
|
|
141
|
+
lease = runtime.operation_nowait()
|
|
142
|
+
else:
|
|
143
|
+
lease = nullcontext()
|
|
144
|
+
with lease as _snap:
|
|
145
|
+
if runtime is not None and _snap is None:
|
|
146
|
+
# A profile transition is in progress — skip this tick so we
|
|
147
|
+
# do not hold the drain window. Health state is unchanged;
|
|
148
|
+
# the next tick fires after the switch commits.
|
|
149
|
+
log.debug(
|
|
150
|
+
"recall-health: tick skipped — profile transition in progress"
|
|
151
|
+
)
|
|
152
|
+
return state
|
|
153
|
+
# fast=True: a health probe must release its operation lease well
|
|
154
|
+
# within the 5s profile-switch drain window (fast=False is 2-10s and
|
|
155
|
+
# would make every profile switch time out while a tick is in flight).
|
|
156
|
+
resp = engine.recall(probe, limit=3, fast=True)
|
|
136
157
|
except Exception as exc:
|
|
137
158
|
state.healthy = False
|
|
138
159
|
state.consecutive_failures += 1
|
|
@@ -24,6 +24,7 @@ import re
|
|
|
24
24
|
from typing import Any
|
|
25
25
|
|
|
26
26
|
from superlocalmemory.core.config import CANONICAL_RECALL_LIMIT
|
|
27
|
+
from superlocalmemory.retrieval.temporal_frame import relative_age, temporal_frame
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
# ---------------------------------------------------------------------------
|
|
@@ -190,9 +191,13 @@ def serialize_recall_response(
|
|
|
190
191
|
is the evidence-floor signal lifted from the response (additive).
|
|
191
192
|
"""
|
|
192
193
|
memory_map = memory_map or {}
|
|
194
|
+
# T-inject: one shared "now" so every result's age label is consistent.
|
|
195
|
+
from datetime import datetime as _dt, timezone as _tz
|
|
196
|
+
_now = _dt.now(_tz.utc)
|
|
193
197
|
raw: list[dict] = []
|
|
194
198
|
for r in (response.results or [])[:limit]:
|
|
195
199
|
fact = r.fact
|
|
200
|
+
_created = getattr(fact, "created_at", "") or ""
|
|
196
201
|
fact_type = getattr(fact, "fact_type", None)
|
|
197
202
|
lifecycle = getattr(fact, "lifecycle", None)
|
|
198
203
|
raw.append({
|
|
@@ -225,7 +230,10 @@ def serialize_recall_response(
|
|
|
225
230
|
if lifecycle is not None and hasattr(lifecycle, "value")
|
|
226
231
|
else (lifecycle or ""),
|
|
227
232
|
"access_count": getattr(fact, "access_count", 0),
|
|
228
|
-
"created_at":
|
|
233
|
+
"created_at": _created,
|
|
234
|
+
# T-inject: human-relative age so consumers (and the LLM) can
|
|
235
|
+
# weigh recency without doing date math. "" when undated.
|
|
236
|
+
"age_label": relative_age(_created, _now),
|
|
229
237
|
"evidence_chain": list(getattr(r, "evidence_chain", []) or []),
|
|
230
238
|
})
|
|
231
239
|
|
|
@@ -241,6 +249,12 @@ def serialize_recall_response(
|
|
|
241
249
|
|
|
242
250
|
def recall_response_metadata(response: Any) -> dict:
|
|
243
251
|
"""Return Score Contract v2 response metadata for transport envelopes."""
|
|
252
|
+
# T-inject: a one-line temporal frame anchoring the result set to "now"
|
|
253
|
+
# and its age span, so time-blind LLMs get an explicit recency signal.
|
|
254
|
+
_timestamps = [
|
|
255
|
+
getattr(getattr(r, "fact", None), "created_at", "") or ""
|
|
256
|
+
for r in (getattr(response, "results", None) or [])
|
|
257
|
+
]
|
|
244
258
|
return {
|
|
245
259
|
"score_contract_version": getattr(response, "score_contract_version", "2"),
|
|
246
260
|
"calibration_status": getattr(response, "calibration_status", "uncalibrated"),
|
|
@@ -248,4 +262,8 @@ def recall_response_metadata(response: Any) -> dict:
|
|
|
248
262
|
"answer_confidence": getattr(response, "answer_confidence", None),
|
|
249
263
|
"abstained": bool(getattr(response, "abstained", False)),
|
|
250
264
|
"abstention_reason": getattr(response, "abstention_reason", None),
|
|
265
|
+
"temporal_frame": temporal_frame(_timestamps),
|
|
266
|
+
# Q2b: thematic community summary (pure pass-through; computed upstream
|
|
267
|
+
# in the engine where DB access is available). None on most recalls.
|
|
268
|
+
"thematic_context": getattr(response, "community_context", None),
|
|
251
269
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
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
|
|
4
|
+
|
|
5
|
+
"""Progressive-abstraction read API (Wave Q3).
|
|
6
|
+
|
|
7
|
+
Exposes the abstraction hierarchy so the dashboard can browse it and drill
|
|
8
|
+
down to source atoms:
|
|
9
|
+
|
|
10
|
+
GET /api/v3/abstraction/persona — the per-profile persona roll-up
|
|
11
|
+
GET /api/v3/abstraction/communities — community summaries (Q2)
|
|
12
|
+
GET /api/v3/abstraction/sources — drill-down (node -> source atoms)
|
|
13
|
+
|
|
14
|
+
Read-only, profile-scoped (Rule 01), direct sqlite3 (Rule 06). All handlers
|
|
15
|
+
fail-soft: a missing DB or table returns an empty payload, never a 500.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import sqlite3
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from fastapi import APIRouter, Query
|
|
25
|
+
from fastapi.responses import JSONResponse
|
|
26
|
+
|
|
27
|
+
from superlocalmemory.server.routes.helpers import DB_PATH, get_active_profile
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
router = APIRouter(prefix="/api/v3/abstraction", tags=["abstraction"])
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class _ReadDB:
|
|
35
|
+
"""Adapt a raw sqlite3 connection to the .execute(...) -> list contract
|
|
36
|
+
the read-only builder methods expect (matches DatabaseManager.execute)."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, conn: sqlite3.Connection) -> None:
|
|
39
|
+
self._conn = conn
|
|
40
|
+
|
|
41
|
+
def execute(self, sql: str, params: tuple = ()) -> list:
|
|
42
|
+
return self._conn.execute(sql, params).fetchall()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _conn() -> sqlite3.Connection | None:
|
|
46
|
+
if not DB_PATH.exists():
|
|
47
|
+
return None
|
|
48
|
+
conn = sqlite3.connect(str(DB_PATH))
|
|
49
|
+
conn.row_factory = sqlite3.Row
|
|
50
|
+
return conn
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@router.get("/persona")
|
|
54
|
+
def get_persona(profile: str = Query("")) -> JSONResponse:
|
|
55
|
+
pid = profile or get_active_profile()
|
|
56
|
+
conn = _conn()
|
|
57
|
+
if conn is None:
|
|
58
|
+
return JSONResponse({"profile": pid, "persona": None})
|
|
59
|
+
try:
|
|
60
|
+
from superlocalmemory.core.progressive_abstraction import ProgressiveAbstraction
|
|
61
|
+
|
|
62
|
+
persona = ProgressiveAbstraction(_ReadDB(conn)).get_persona(pid)
|
|
63
|
+
return JSONResponse({"profile": pid, "persona": persona})
|
|
64
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
65
|
+
logger.debug("persona read failed: %s", exc)
|
|
66
|
+
return JSONResponse({"profile": pid, "persona": None})
|
|
67
|
+
finally:
|
|
68
|
+
conn.close()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@router.get("/communities")
|
|
72
|
+
def get_communities(profile: str = Query("")) -> JSONResponse:
|
|
73
|
+
pid = profile or get_active_profile()
|
|
74
|
+
conn = _conn()
|
|
75
|
+
if conn is None:
|
|
76
|
+
return JSONResponse({"profile": pid, "communities": []})
|
|
77
|
+
try:
|
|
78
|
+
from superlocalmemory.core.community_summary import CommunitySummaryBuilder
|
|
79
|
+
|
|
80
|
+
summaries = CommunitySummaryBuilder(_ReadDB(conn)).get_summaries(pid)
|
|
81
|
+
return JSONResponse({"profile": pid, "communities": summaries})
|
|
82
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
83
|
+
logger.debug("communities read failed: %s", exc)
|
|
84
|
+
return JSONResponse({"profile": pid, "communities": []})
|
|
85
|
+
finally:
|
|
86
|
+
conn.close()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@router.get("/sources")
|
|
90
|
+
def get_sources(
|
|
91
|
+
profile: str = Query(""), node: str = Query("persona"),
|
|
92
|
+
) -> JSONResponse:
|
|
93
|
+
pid = profile or get_active_profile()
|
|
94
|
+
conn = _conn()
|
|
95
|
+
empty: dict[str, Any] = {
|
|
96
|
+
"node_id": node, "node_type": "unknown", "communities": [], "fact_ids": [],
|
|
97
|
+
}
|
|
98
|
+
if conn is None:
|
|
99
|
+
return JSONResponse({"profile": pid, "sources": empty})
|
|
100
|
+
try:
|
|
101
|
+
from superlocalmemory.core.progressive_abstraction import ProgressiveAbstraction
|
|
102
|
+
|
|
103
|
+
node_val: Any = node
|
|
104
|
+
if node != "persona":
|
|
105
|
+
try:
|
|
106
|
+
node_val = int(node)
|
|
107
|
+
except (ValueError, TypeError):
|
|
108
|
+
node_val = node
|
|
109
|
+
sources = ProgressiveAbstraction(_ReadDB(conn)).get_sources(pid, node_val)
|
|
110
|
+
return JSONResponse({"profile": pid, "sources": sources})
|
|
111
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
112
|
+
logger.debug("sources read failed: %s", exc)
|
|
113
|
+
return JSONResponse({"profile": pid, "sources": empty})
|
|
114
|
+
finally:
|
|
115
|
+
conn.close()
|
|
@@ -19,6 +19,12 @@ from .helpers import DB_PATH
|
|
|
19
19
|
logger = logging.getLogger("superlocalmemory.routes.agents")
|
|
20
20
|
router = APIRouter()
|
|
21
21
|
|
|
22
|
+
|
|
23
|
+
def _internal_error(detail: str = "Internal server error") -> HTTPException:
|
|
24
|
+
"""SEC-H-02: log full traceback server-side; return a generic message to the client."""
|
|
25
|
+
logger.exception("agents route error")
|
|
26
|
+
return HTTPException(status_code=500, detail=detail)
|
|
27
|
+
|
|
22
28
|
# Feature flag: V3 trust scorer
|
|
23
29
|
TRUST_AVAILABLE = False
|
|
24
30
|
try:
|
|
@@ -56,8 +62,8 @@ async def get_agents(
|
|
|
56
62
|
"count": len(agents),
|
|
57
63
|
"stats": {"total_agents": len(agents)},
|
|
58
64
|
}
|
|
59
|
-
except Exception
|
|
60
|
-
raise
|
|
65
|
+
except Exception:
|
|
66
|
+
raise _internal_error("Agent registry error")
|
|
61
67
|
|
|
62
68
|
|
|
63
69
|
@router.get("/api/agents/stats")
|
|
@@ -69,8 +75,91 @@ async def get_agent_stats(request: Request):
|
|
|
69
75
|
registry = AgentRegistry(persist_path=_registry_path())
|
|
70
76
|
agents = registry.list_agents()
|
|
71
77
|
return {"total_agents": len(agents)}
|
|
72
|
-
except Exception
|
|
73
|
-
raise
|
|
78
|
+
except Exception:
|
|
79
|
+
raise _internal_error("Agent stats error")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@router.get("/api/agents/memory-activity")
|
|
83
|
+
async def get_agent_memory_activity(
|
|
84
|
+
request: Request,
|
|
85
|
+
limit: int = Query(20, ge=1, le=100),
|
|
86
|
+
):
|
|
87
|
+
"""Per-agent memory attribution for the multi-agent memory view.
|
|
88
|
+
|
|
89
|
+
Reports how many memories each writing agent contributed, when each was
|
|
90
|
+
last active, which ingestion sources they used, and the most recent
|
|
91
|
+
entries — grouped by ``ingestion_operations.trusted_actor_id`` (the agent
|
|
92
|
+
that wrote the memory). Profile-scoped. Uses a direct DB read because the
|
|
93
|
+
dashboard runs without the engine subprocess. Never raises to the client;
|
|
94
|
+
returns empty structures if the operations table is absent.
|
|
95
|
+
"""
|
|
96
|
+
import sqlite3
|
|
97
|
+
|
|
98
|
+
from .helpers import get_active_profile
|
|
99
|
+
|
|
100
|
+
pid = get_active_profile()
|
|
101
|
+
agents: list[dict] = []
|
|
102
|
+
recent: list[dict] = []
|
|
103
|
+
total = 0
|
|
104
|
+
|
|
105
|
+
if DB_PATH.exists():
|
|
106
|
+
conn = sqlite3.connect(str(DB_PATH))
|
|
107
|
+
conn.row_factory = sqlite3.Row
|
|
108
|
+
try:
|
|
109
|
+
try:
|
|
110
|
+
rows = conn.execute(
|
|
111
|
+
"SELECT CASE WHEN trusted_actor_id='' THEN 'unknown' "
|
|
112
|
+
"ELSE trusted_actor_id END AS agent_id, "
|
|
113
|
+
"COUNT(*) AS cnt, MAX(created_at) AS last_active, "
|
|
114
|
+
"GROUP_CONCAT(DISTINCT source_type) AS sources "
|
|
115
|
+
"FROM ingestion_operations WHERE profile_id=? "
|
|
116
|
+
"GROUP BY agent_id ORDER BY cnt DESC, agent_id ASC "
|
|
117
|
+
"LIMIT 500",
|
|
118
|
+
(pid,),
|
|
119
|
+
).fetchall()
|
|
120
|
+
for r in rows:
|
|
121
|
+
agents.append({
|
|
122
|
+
"agent_id": r["agent_id"],
|
|
123
|
+
"count": r["cnt"],
|
|
124
|
+
"last_active": r["last_active"],
|
|
125
|
+
"source_types": (
|
|
126
|
+
[s for s in (r["sources"] or "").split(",") if s]
|
|
127
|
+
),
|
|
128
|
+
})
|
|
129
|
+
total += r["cnt"]
|
|
130
|
+
except sqlite3.OperationalError:
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
rows = conn.execute(
|
|
135
|
+
"SELECT CASE WHEN trusted_actor_id='' THEN 'unknown' "
|
|
136
|
+
"ELSE trusted_actor_id END AS agent_id, "
|
|
137
|
+
"substr(raw_content, 1, 160) AS snippet, "
|
|
138
|
+
"created_at, source_type, session_id "
|
|
139
|
+
"FROM ingestion_operations WHERE profile_id=? "
|
|
140
|
+
"ORDER BY created_at DESC, rowid DESC LIMIT ?",
|
|
141
|
+
(pid, int(limit)),
|
|
142
|
+
).fetchall()
|
|
143
|
+
recent = [{
|
|
144
|
+
"agent_id": r["agent_id"],
|
|
145
|
+
"content": r["snippet"],
|
|
146
|
+
"created_at": r["created_at"],
|
|
147
|
+
"source_type": r["source_type"],
|
|
148
|
+
"session_id": r["session_id"],
|
|
149
|
+
} for r in rows]
|
|
150
|
+
except sqlite3.OperationalError:
|
|
151
|
+
pass
|
|
152
|
+
finally:
|
|
153
|
+
conn.close()
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
"ok": True,
|
|
157
|
+
"profile_id": pid,
|
|
158
|
+
"total_memories": total,
|
|
159
|
+
"agent_count": len(agents),
|
|
160
|
+
"agents": agents,
|
|
161
|
+
"recent": recent,
|
|
162
|
+
}
|
|
74
163
|
|
|
75
164
|
|
|
76
165
|
@router.get("/api/trust/stats")
|
|
@@ -102,38 +191,39 @@ async def get_trust_stats(request: Request):
|
|
|
102
191
|
conn = sqlite3.connect(str(DB_PATH))
|
|
103
192
|
conn.row_factory = sqlite3.Row
|
|
104
193
|
try:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
try:
|
|
115
|
-
# Average trust score
|
|
116
|
-
row = conn.execute(
|
|
117
|
-
"SELECT AVG(trust_score) AS avg_ts FROM trust_scores "
|
|
118
|
-
"WHERE profile_id = ?", (pid,),
|
|
119
|
-
).fetchone()
|
|
120
|
-
if row and row["avg_ts"] is not None:
|
|
121
|
-
avg_trust_score = round(float(row["avg_ts"]), 3)
|
|
122
|
-
except sqlite3.OperationalError:
|
|
123
|
-
pass
|
|
194
|
+
try:
|
|
195
|
+
# Count trust signals
|
|
196
|
+
row = conn.execute(
|
|
197
|
+
"SELECT COUNT(*) AS cnt FROM trust_signals "
|
|
198
|
+
"WHERE profile_id = ?", (pid,),
|
|
199
|
+
).fetchone()
|
|
200
|
+
total_signals = row["cnt"] if row else 0
|
|
201
|
+
except sqlite3.OperationalError:
|
|
202
|
+
pass
|
|
124
203
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
204
|
+
try:
|
|
205
|
+
# Average trust score
|
|
206
|
+
row = conn.execute(
|
|
207
|
+
"SELECT AVG(trust_score) AS avg_ts FROM trust_scores "
|
|
208
|
+
"WHERE profile_id = ?", (pid,),
|
|
209
|
+
).fetchone()
|
|
210
|
+
if row and row["avg_ts"] is not None:
|
|
211
|
+
avg_trust_score = round(float(row["avg_ts"]), 3)
|
|
212
|
+
except sqlite3.OperationalError:
|
|
213
|
+
pass
|
|
135
214
|
|
|
136
|
-
|
|
215
|
+
try:
|
|
216
|
+
# Signal breakdown by type
|
|
217
|
+
rows = conn.execute(
|
|
218
|
+
"SELECT signal_type, COUNT(*) AS cnt "
|
|
219
|
+
"FROM trust_signals WHERE profile_id = ? "
|
|
220
|
+
"GROUP BY signal_type", (pid,),
|
|
221
|
+
).fetchall()
|
|
222
|
+
by_signal_type = {r["signal_type"]: r["cnt"] for r in rows}
|
|
223
|
+
except sqlite3.OperationalError:
|
|
224
|
+
pass
|
|
225
|
+
finally:
|
|
226
|
+
conn.close()
|
|
137
227
|
|
|
138
228
|
# Enforcement status: SLM uses "Silent Collection" by default
|
|
139
229
|
enforcement = "Silent Collection"
|
|
@@ -144,8 +234,8 @@ async def get_trust_stats(request: Request):
|
|
|
144
234
|
"enforcement": enforcement,
|
|
145
235
|
"by_signal_type": by_signal_type,
|
|
146
236
|
}
|
|
147
|
-
except Exception
|
|
148
|
-
raise
|
|
237
|
+
except Exception:
|
|
238
|
+
raise _internal_error("Trust stats error")
|
|
149
239
|
|
|
150
240
|
|
|
151
241
|
@router.get("/api/trust/signals/{agent_id}")
|
|
@@ -167,5 +257,5 @@ async def get_agent_trust_signals(
|
|
|
167
257
|
"signals": signals, "count": len(signals),
|
|
168
258
|
}
|
|
169
259
|
return {"agent_id": agent_id, "signals": [], "count": 0}
|
|
170
|
-
except Exception
|
|
171
|
-
raise
|
|
260
|
+
except Exception:
|
|
261
|
+
raise _internal_error("Trust signals error")
|