superlocalmemory 3.7.7 → 3.8.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/.claude-plugin/marketplace.json +1 -1
- package/ATTRIBUTION.md +1 -3
- package/CHANGELOG.md +85 -0
- package/README.md +199 -29
- package/package.json +4 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- 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/agents/slm-governance-advisor.md +80 -0
- package/plugin-src/agents/slm-loop-runner.md +71 -0
- package/plugin-src/agents/slm-memory-advisor.md +10 -5
- package/plugin-src/agents/slm-optimize-advisor.md +9 -3
- package/plugin-src/commands/slm-loop.md +31 -0
- package/plugin-src/hooks/hooks.json +79 -0
- package/plugin-src/manifest.json +7 -2
- package/plugin-src/requirements.txt +1 -1
- package/plugin-src/rules/AGENTS.md +57 -18
- package/plugin-src/rules/CLAUDE.md.fragment +8 -8
- package/plugin-src/scripts/slm-launch +46 -7
- package/plugin-src/settings.json +9 -0
- 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-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +17 -3
- 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 +46 -10
- package/plugin-src/skills/slm-remember/SKILL.md +48 -1
- package/plugin-src/skills/slm-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +24 -1
- package/plugin-src/skills/slm-status/SKILL.md +18 -1
- package/pyproject.toml +1 -2
- 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 +96 -12
- package/src/superlocalmemory/cli/compress_cmd.py +17 -7
- package/src/superlocalmemory/cli/loop_cmd.py +192 -0
- package/src/superlocalmemory/cli/main.py +39 -4
- 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/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 +5 -43
- 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 +17 -2
- package/src/superlocalmemory/core/embeddings.py +12 -1
- package/src/superlocalmemory/core/engine.py +17 -1
- package/src/superlocalmemory/core/engine_ingestion.py +29 -0
- 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/key_expander.py +138 -0
- package/src/superlocalmemory/core/maintenance.py +23 -0
- 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 +2 -0
- 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 +35 -11
- 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/memory_protocol.py +54 -0
- package/src/superlocalmemory/hooks/portable_kit.py +114 -1
- package/src/superlocalmemory/infra/auth_middleware.py +28 -0
- 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/reward_proxy.py +42 -9
- 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 +243 -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 +21 -49
- 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 +50 -5
- package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
- 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 +168 -26
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
- 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 +21 -4
- 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 +34 -10
- package/src/superlocalmemory/server/routes/behavioral.py +13 -12
- package/src/superlocalmemory/server/routes/brain.py +21 -5
- package/src/superlocalmemory/server/routes/chat.py +72 -16
- package/src/superlocalmemory/server/routes/compliance.py +171 -21
- package/src/superlocalmemory/server/routes/config_api.py +436 -0
- package/src/superlocalmemory/server/routes/data_io.py +30 -8
- package/src/superlocalmemory/server/routes/entity.py +9 -4
- package/src/superlocalmemory/server/routes/events.py +24 -8
- package/src/superlocalmemory/server/routes/evolution.py +135 -17
- 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 +14 -14
- package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
- package/src/superlocalmemory/server/routes/memories.py +221 -49
- package/src/superlocalmemory/server/routes/mesh.py +95 -15
- 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 +124 -0
- package/src/superlocalmemory/server/routes/rbac.py +367 -0
- package/src/superlocalmemory/server/routes/stats.py +13 -6
- package/src/superlocalmemory/server/routes/tiers.py +11 -9
- package/src/superlocalmemory/server/routes/v3_api.py +194 -81
- package/src/superlocalmemory/server/routes/ws.py +5 -2
- package/src/superlocalmemory/server/security_middleware.py +12 -5
- package/src/superlocalmemory/server/ui.py +30 -5
- package/src/superlocalmemory/server/unified_daemon.py +431 -75
- package/src/superlocalmemory/server/write_identity.py +38 -8
- package/src/superlocalmemory/storage/database.py +265 -53
- package/src/superlocalmemory/storage/migration_runner.py +53 -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/models.py +4 -0
- package/src/superlocalmemory/storage/schema.py +87 -0
- package/src/superlocalmemory/storage/schema_v32.py +0 -9
- package/src/superlocalmemory/storage/schema_v343.py +24 -12
- 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 +306 -173
- package/src/superlocalmemory/ui/js/brain.js +5 -20
- package/src/superlocalmemory/ui/js/core.js +47 -31
- package/src/superlocalmemory/ui/js/dashboard.js +314 -63
- 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 +780 -0
- package/src/superlocalmemory/ui/js/od-brain.js +779 -0
- package/src/superlocalmemory/ui/js/od-entities.js +579 -0
- package/src/superlocalmemory/ui/js/od-graph.js +593 -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 +887 -0
- package/src/superlocalmemory/ui/js/od-mesh.js +539 -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 +1053 -0
- package/src/superlocalmemory/ui/js/od-shell.js +593 -0
- package/src/superlocalmemory/ui/js/od-skills.js +573 -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 +2 -2
- 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/src/superlocalmemory/learning/behavioral_listener.py +0 -94
|
@@ -20,6 +20,18 @@ logger = logging.getLogger("superlocalmemory.routes.memories")
|
|
|
20
20
|
router = APIRouter()
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
def _internal_error(detail: str = "Internal server error") -> HTTPException:
|
|
24
|
+
"""SEC-H-02: log the full traceback server-side; return a generic message.
|
|
25
|
+
|
|
26
|
+
Returning ``str(e)`` to HTTP clients leaks DB schema (column/constraint
|
|
27
|
+
names), the data-directory filesystem path, and possibly config internals —
|
|
28
|
+
a GDPR Art. 32 gap. Call this only from inside an ``except`` block so
|
|
29
|
+
``logger.exception`` captures the active traceback automatically.
|
|
30
|
+
"""
|
|
31
|
+
logger.exception("memories route error")
|
|
32
|
+
return HTTPException(status_code=500, detail=detail)
|
|
33
|
+
|
|
34
|
+
|
|
23
35
|
def _get_engine(request: Request):
|
|
24
36
|
"""Get V3 engine from app state, initializing lazily on first call."""
|
|
25
37
|
return get_engine_lazy(request.app.state)
|
|
@@ -41,6 +53,15 @@ def _authorize_memory_mutation(
|
|
|
41
53
|
getattr(request.app.state, "daemon_descriptor", None),
|
|
42
54
|
actor_kind="dashboard",
|
|
43
55
|
)
|
|
56
|
+
# RBAC (C3): on top of machine auth, enforce the caller's role on the active
|
|
57
|
+
# profile. delete → DELETE; every other mutation → WRITE. A viewer (or an
|
|
58
|
+
# owner in require_login mode) is rejected here; owner/admin/member pass.
|
|
59
|
+
from superlocalmemory.access.rbac import Permission as _Perm
|
|
60
|
+
from superlocalmemory.server.rbac_enforce import require_permission as _rbac_require
|
|
61
|
+
_rbac_require(
|
|
62
|
+
request,
|
|
63
|
+
_Perm.DELETE if operation == "delete" else _Perm.WRITE,
|
|
64
|
+
)
|
|
44
65
|
engine = _get_engine(request)
|
|
45
66
|
if engine is None:
|
|
46
67
|
raise HTTPException(503, detail="Engine not initialized")
|
|
@@ -253,6 +274,14 @@ async def get_memories(
|
|
|
253
274
|
None,
|
|
254
275
|
description="Named filter: 'high_reward' | 'being_forgotten'",
|
|
255
276
|
),
|
|
277
|
+
scope: Optional[str] = Query(
|
|
278
|
+
None,
|
|
279
|
+
description=(
|
|
280
|
+
"Scope view (v3 only): 'shared' (shared with this profile), "
|
|
281
|
+
"'global' (global memories), 'all' (this profile + global + shared). "
|
|
282
|
+
"Default None = this profile only (unchanged isolation)."
|
|
283
|
+
),
|
|
284
|
+
),
|
|
256
285
|
):
|
|
257
286
|
"""List memories with optional filtering and pagination.
|
|
258
287
|
|
|
@@ -274,15 +303,36 @@ async def get_memories(
|
|
|
274
303
|
use_v3 = _has_table(cursor, 'atomic_facts')
|
|
275
304
|
|
|
276
305
|
if use_v3:
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
306
|
+
# Scope view clause. Default (scope=None) is this-profile-only —
|
|
307
|
+
# identical isolation to before. Other values widen the view to
|
|
308
|
+
# global and/or shared-with-this-profile memories.
|
|
309
|
+
_esc = active_profile.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
|
310
|
+
_shared_pat = f'%"{_esc}"%'
|
|
311
|
+
if scope == "global":
|
|
312
|
+
scope_where = "scope = 'global'"
|
|
313
|
+
scope_params = []
|
|
314
|
+
elif scope == "shared":
|
|
315
|
+
scope_where = "scope = 'shared' AND shared_with LIKE ? ESCAPE '\\'"
|
|
316
|
+
scope_params = [_shared_pat]
|
|
317
|
+
elif scope == "all":
|
|
318
|
+
scope_where = (
|
|
319
|
+
"(profile_id = ? OR scope = 'global' "
|
|
320
|
+
"OR (scope = 'shared' AND shared_with LIKE ? ESCAPE '\\'))"
|
|
321
|
+
)
|
|
322
|
+
scope_params = [active_profile, _shared_pat]
|
|
323
|
+
else:
|
|
324
|
+
scope_where = "profile_id = ?"
|
|
325
|
+
scope_params = [active_profile]
|
|
326
|
+
|
|
327
|
+
query = (
|
|
328
|
+
"SELECT fact_id as id, memory_id, content, fact_type as category, "
|
|
329
|
+
"confidence as importance, access_count, "
|
|
330
|
+
"created_at, created_at as updated_at, "
|
|
331
|
+
"session_id as project_name, scope, shared_with "
|
|
332
|
+
f"FROM atomic_facts WHERE {scope_where}"
|
|
333
|
+
)
|
|
334
|
+
params = list(scope_params)
|
|
335
|
+
count_base = f"SELECT COUNT(*) as total FROM atomic_facts WHERE {scope_where}"
|
|
286
336
|
else:
|
|
287
337
|
query = """
|
|
288
338
|
SELECT id, content, summary, category, project_name, project_path,
|
|
@@ -293,7 +343,8 @@ async def get_memories(
|
|
|
293
343
|
params = [active_profile]
|
|
294
344
|
count_base = "SELECT COUNT(*) as total FROM memories WHERE profile = ?"
|
|
295
345
|
|
|
296
|
-
|
|
346
|
+
# count params must mirror the base WHERE params (scope-aware for v3).
|
|
347
|
+
count_params = list(params)
|
|
297
348
|
|
|
298
349
|
if category:
|
|
299
350
|
if use_v3:
|
|
@@ -396,8 +447,8 @@ async def get_memories(
|
|
|
396
447
|
"has_more": (offset + limit) < total,
|
|
397
448
|
}
|
|
398
449
|
|
|
399
|
-
except Exception
|
|
400
|
-
raise
|
|
450
|
+
except Exception:
|
|
451
|
+
raise _internal_error("Database error")
|
|
401
452
|
|
|
402
453
|
|
|
403
454
|
@router.get("/api/graph")
|
|
@@ -430,8 +481,8 @@ async def get_graph(
|
|
|
430
481
|
},
|
|
431
482
|
}
|
|
432
483
|
|
|
433
|
-
except Exception
|
|
434
|
-
raise
|
|
484
|
+
except Exception:
|
|
485
|
+
raise _internal_error("Graph error")
|
|
435
486
|
|
|
436
487
|
|
|
437
488
|
@router.post("/api/search")
|
|
@@ -455,17 +506,27 @@ async def search_memories(request: Request, body: SearchRequest):
|
|
|
455
506
|
# directly in an async route blocks the ASGI event loop — Chrome detects
|
|
456
507
|
# a stalled connection and aborts with "signal is aborted without reason"
|
|
457
508
|
# before the response arrives. Fix: run in a thread-pool executor so the
|
|
458
|
-
# event loop stays alive to send keepalive frames.
|
|
459
|
-
#
|
|
509
|
+
# event loop stays alive to send keepalive frames.
|
|
510
|
+
# v3.4.64 (regression fix): fast=True skips spreading_activation + agentic
|
|
511
|
+
# LLM rounds (saves ~7s on cold graph traversal). fast=False is the SLOW
|
|
512
|
+
# path that enables both; the earlier comment had this inverted.
|
|
460
513
|
import asyncio
|
|
461
514
|
import time as _time
|
|
462
515
|
engine = _get_engine(request)
|
|
463
516
|
if engine is not None:
|
|
464
|
-
loop = asyncio.
|
|
517
|
+
loop = asyncio.get_running_loop()
|
|
465
518
|
t0 = _time.monotonic()
|
|
519
|
+
# T-window: prefer an explicit ``window`` spec; otherwise derive a
|
|
520
|
+
# range from the legacy date_from/date_to pair when both are set.
|
|
521
|
+
_window = getattr(body, "window", None) or ""
|
|
522
|
+
if not _window and getattr(body, "date_from", None) and getattr(body, "date_to", None):
|
|
523
|
+
_window = f"{body.date_from}..{body.date_to}"
|
|
466
524
|
response = await loop.run_in_executor(
|
|
467
525
|
None,
|
|
468
|
-
lambda: engine.recall(
|
|
526
|
+
lambda: engine.recall(
|
|
527
|
+
body.query, limit=body.limit, fast=True,
|
|
528
|
+
window=_window or None,
|
|
529
|
+
),
|
|
469
530
|
)
|
|
470
531
|
elapsed_ms = round((_time.monotonic() - t0) * 1000, 1)
|
|
471
532
|
from superlocalmemory.server.recall_serializer import (
|
|
@@ -525,8 +586,8 @@ async def search_memories(request: Request, body: SearchRequest):
|
|
|
525
586
|
"no_confident_match": False,
|
|
526
587
|
}
|
|
527
588
|
|
|
528
|
-
except Exception
|
|
529
|
-
raise
|
|
589
|
+
except Exception:
|
|
590
|
+
raise _internal_error("Search error")
|
|
530
591
|
finally:
|
|
531
592
|
end_recall()
|
|
532
593
|
|
|
@@ -611,8 +672,8 @@ async def get_clusters(request: Request):
|
|
|
611
672
|
|
|
612
673
|
conn.close()
|
|
613
674
|
return {"clusters": clusters, "total_clusters": len(clusters), "unclustered_count": unclustered}
|
|
614
|
-
except Exception
|
|
615
|
-
raise
|
|
675
|
+
except Exception:
|
|
676
|
+
raise _internal_error("Cluster error")
|
|
616
677
|
|
|
617
678
|
|
|
618
679
|
@router.get("/api/clusters/{cluster_id}")
|
|
@@ -662,14 +723,30 @@ async def get_cluster_detail(request: Request, cluster_id: str, limit: int = Que
|
|
|
662
723
|
if not members:
|
|
663
724
|
raise HTTPException(status_code=404, detail="Cluster not found")
|
|
664
725
|
# Generate cluster summary
|
|
726
|
+
# v3.7.8: previously routed through WorkerPool.shared(), a subprocess
|
|
727
|
+
# cache never recycled on a profile switch (CRITICAL cross-profile
|
|
728
|
+
# leak — up to 120s stale). Use the daemon's own resident,
|
|
729
|
+
# lease-protected engine's config instead; the summarizer only
|
|
730
|
+
# consumes already profile-filtered `texts` above, so this closes
|
|
731
|
+
# the stale-engine window without changing behavior.
|
|
665
732
|
summary = ""
|
|
666
733
|
try:
|
|
667
|
-
from superlocalmemory.core.worker_pool import WorkerPool
|
|
668
|
-
pool = WorkerPool.shared()
|
|
669
734
|
texts = [m.get("content", "")[:200] for m in members[:10] if m.get("content")]
|
|
670
735
|
if texts:
|
|
671
|
-
|
|
672
|
-
|
|
736
|
+
from superlocalmemory.core.summarizer import Summarizer
|
|
737
|
+
from superlocalmemory.server.routes.helpers import get_engine_lazy
|
|
738
|
+
|
|
739
|
+
# v3.4.64: Do NOT call runtime.operation() synchronously in an
|
|
740
|
+
# async route — if a transition is pending, acquire_operation()
|
|
741
|
+
# blocks the event loop thread, deadlocking the drain. The
|
|
742
|
+
# middleware already holds an operation lease for this request;
|
|
743
|
+
# the engine is stable for the full duration of the handler.
|
|
744
|
+
engine = get_engine_lazy(request.app.state)
|
|
745
|
+
if engine is not None:
|
|
746
|
+
summarizer = Summarizer(engine._config)
|
|
747
|
+
summary = summarizer.summarize_cluster(
|
|
748
|
+
[{"content": t} for t in texts]
|
|
749
|
+
) or ""
|
|
673
750
|
except Exception:
|
|
674
751
|
pass
|
|
675
752
|
|
|
@@ -681,24 +758,60 @@ async def get_cluster_detail(request: Request, cluster_id: str, limit: int = Que
|
|
|
681
758
|
}
|
|
682
759
|
except HTTPException:
|
|
683
760
|
raise
|
|
684
|
-
except Exception
|
|
685
|
-
raise
|
|
761
|
+
except Exception:
|
|
762
|
+
raise _internal_error("Cluster detail error")
|
|
686
763
|
|
|
687
764
|
|
|
688
765
|
@router.get("/api/memories/{memory_id}/facts")
|
|
689
766
|
async def get_memory_facts(request: Request, memory_id: str):
|
|
690
|
-
"""Get original memory text with all its child atomic facts.
|
|
767
|
+
"""Get original memory text with all its child atomic facts.
|
|
768
|
+
|
|
769
|
+
v3.7.8: previously routed through WorkerPool.shared(), a subprocess
|
|
770
|
+
engine cached at process init and never recycled on a profile switch
|
|
771
|
+
(CRITICAL cross-profile leak — served the OLD profile's facts for up to
|
|
772
|
+
120s after a switch). Uses the daemon's own resident, lease-protected
|
|
773
|
+
engine instead, exactly like the /recall route.
|
|
774
|
+
"""
|
|
691
775
|
try:
|
|
692
|
-
from superlocalmemory.
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
776
|
+
from superlocalmemory.server.routes.helpers import get_engine_lazy
|
|
777
|
+
|
|
778
|
+
# v3.4.64: Do NOT call runtime.operation() synchronously in an async
|
|
779
|
+
# route — blocks the event loop when a transition is pending. The
|
|
780
|
+
# middleware already holds an operation lease; the engine and its
|
|
781
|
+
# profile_id are stable for the full duration of this request.
|
|
782
|
+
engine = get_engine_lazy(request.app.state)
|
|
783
|
+
if engine is None:
|
|
784
|
+
raise HTTPException(status_code=503, detail="Engine not initialized")
|
|
785
|
+
active_profile = engine.profile_id
|
|
786
|
+
mem_map = engine._db.get_memory_content_batch(
|
|
787
|
+
[memory_id], active_profile, include_global=True, include_shared=True,
|
|
788
|
+
)
|
|
789
|
+
original = mem_map.get(memory_id, "")
|
|
790
|
+
facts = engine._db.get_facts_by_memory_id(memory_id, active_profile)
|
|
791
|
+
fact_list = [
|
|
792
|
+
{
|
|
793
|
+
"fact_id": f.fact_id,
|
|
794
|
+
"content": f.content,
|
|
795
|
+
"fact_type": (
|
|
796
|
+
f.fact_type.value if hasattr(f.fact_type, "value")
|
|
797
|
+
else str(f.fact_type)
|
|
798
|
+
),
|
|
799
|
+
"confidence": round(f.confidence, 3),
|
|
800
|
+
"created_at": f.created_at,
|
|
801
|
+
}
|
|
802
|
+
for f in facts
|
|
803
|
+
]
|
|
804
|
+
return {
|
|
805
|
+
"ok": True,
|
|
806
|
+
"memory_id": memory_id,
|
|
807
|
+
"original_content": original,
|
|
808
|
+
"facts": fact_list,
|
|
809
|
+
"fact_count": len(fact_list),
|
|
810
|
+
}
|
|
698
811
|
except HTTPException:
|
|
699
812
|
raise
|
|
700
|
-
except Exception
|
|
701
|
-
raise
|
|
813
|
+
except Exception:
|
|
814
|
+
raise _internal_error()
|
|
702
815
|
|
|
703
816
|
|
|
704
817
|
@router.get("/api/memories/{memory_id}/detail")
|
|
@@ -748,8 +861,8 @@ async def get_memory_detail(request: Request, memory_id: str):
|
|
|
748
861
|
}
|
|
749
862
|
except HTTPException:
|
|
750
863
|
raise
|
|
751
|
-
except Exception
|
|
752
|
-
raise
|
|
864
|
+
except Exception:
|
|
865
|
+
raise _internal_error("Detail error")
|
|
753
866
|
|
|
754
867
|
|
|
755
868
|
@router.get("/api/facts/{fact_id}")
|
|
@@ -788,8 +901,8 @@ async def get_fact_detail(request: Request, fact_id: str):
|
|
|
788
901
|
return row
|
|
789
902
|
except HTTPException:
|
|
790
903
|
raise
|
|
791
|
-
except Exception
|
|
792
|
-
raise
|
|
904
|
+
except Exception:
|
|
905
|
+
raise _internal_error("Fact detail error")
|
|
793
906
|
|
|
794
907
|
|
|
795
908
|
@router.delete("/api/memories/{fact_id}")
|
|
@@ -812,8 +925,8 @@ async def delete_memory(request: Request, fact_id: str):
|
|
|
812
925
|
return {"success": True, "deleted": fact_id}
|
|
813
926
|
except HTTPException:
|
|
814
927
|
raise
|
|
815
|
-
except Exception
|
|
816
|
-
raise
|
|
928
|
+
except Exception:
|
|
929
|
+
raise _internal_error("Delete error")
|
|
817
930
|
|
|
818
931
|
|
|
819
932
|
@router.post("/api/memories/{fact_id}/forget")
|
|
@@ -873,8 +986,8 @@ async def forget_memory(request: Request, fact_id: str):
|
|
|
873
986
|
return {"success": True, "fact_id": fact_id, "archived_at": archived_at}
|
|
874
987
|
except HTTPException:
|
|
875
988
|
raise
|
|
876
|
-
except Exception
|
|
877
|
-
raise
|
|
989
|
+
except Exception:
|
|
990
|
+
raise _internal_error("Forget error")
|
|
878
991
|
|
|
879
992
|
|
|
880
993
|
@router.post("/api/memories/{fact_id}/merge")
|
|
@@ -942,8 +1055,8 @@ async def merge_memory(request: Request, fact_id: str):
|
|
|
942
1055
|
}
|
|
943
1056
|
except HTTPException:
|
|
944
1057
|
raise
|
|
945
|
-
except Exception
|
|
946
|
-
raise
|
|
1058
|
+
except Exception:
|
|
1059
|
+
raise _internal_error("Merge error")
|
|
947
1060
|
|
|
948
1061
|
|
|
949
1062
|
@router.patch("/api/memories/{fact_id}")
|
|
@@ -975,5 +1088,64 @@ async def edit_memory(request: Request, fact_id: str):
|
|
|
975
1088
|
return {"success": True, "fact_id": fact_id, "content": new_content}
|
|
976
1089
|
except HTTPException:
|
|
977
1090
|
raise
|
|
978
|
-
except Exception
|
|
979
|
-
raise
|
|
1091
|
+
except Exception:
|
|
1092
|
+
raise _internal_error("Edit error")
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
_VALID_SCOPES = ("personal", "shared", "global")
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
@router.patch("/api/memories/{fact_id}/scope")
|
|
1099
|
+
async def set_memory_scope(request: Request, fact_id: str):
|
|
1100
|
+
"""Set a memory's scope (personal | shared | global) + shared_with.
|
|
1101
|
+
|
|
1102
|
+
Body: {"scope": "shared", "shared_with": "alice,bob"}. shared_with accepts a
|
|
1103
|
+
comma-separated string or a list; it is stored as a JSON array so the
|
|
1104
|
+
_scope_where LIKE match works. Mutation-authorized, and the fact must
|
|
1105
|
+
belong to the active profile (a caller cannot re-scope another profile's
|
|
1106
|
+
fact). This is the write side of multi-scope sharing from the dashboard.
|
|
1107
|
+
"""
|
|
1108
|
+
import json as _json
|
|
1109
|
+
try:
|
|
1110
|
+
body = await request.json()
|
|
1111
|
+
scope = (body.get("scope") or "").strip().lower()
|
|
1112
|
+
if scope not in _VALID_SCOPES:
|
|
1113
|
+
raise HTTPException(400, detail=f"scope must be one of {_VALID_SCOPES}")
|
|
1114
|
+
|
|
1115
|
+
raw_shared = body.get("shared_with", [])
|
|
1116
|
+
if isinstance(raw_shared, str):
|
|
1117
|
+
shared_list = [s.strip() for s in raw_shared.split(",") if s.strip()]
|
|
1118
|
+
elif isinstance(raw_shared, list):
|
|
1119
|
+
shared_list = [str(s).strip() for s in raw_shared if str(s).strip()]
|
|
1120
|
+
else:
|
|
1121
|
+
shared_list = []
|
|
1122
|
+
if scope == "shared" and not shared_list:
|
|
1123
|
+
raise HTTPException(
|
|
1124
|
+
400, detail="shared scope requires at least one profile in shared_with")
|
|
1125
|
+
# global/personal never carry a shared_with list.
|
|
1126
|
+
if scope != "shared":
|
|
1127
|
+
shared_list = []
|
|
1128
|
+
|
|
1129
|
+
engine, active_profile, _ctx = _authorize_memory_mutation(
|
|
1130
|
+
request, "update", fact_id, run_pre_hook=False,
|
|
1131
|
+
)
|
|
1132
|
+
# Ownership check: the fact must belong to the active profile.
|
|
1133
|
+
rows = engine._db.execute(
|
|
1134
|
+
"SELECT 1 FROM atomic_facts WHERE fact_id = ? AND profile_id = ?",
|
|
1135
|
+
(fact_id, active_profile),
|
|
1136
|
+
)
|
|
1137
|
+
if not rows:
|
|
1138
|
+
raise HTTPException(404, detail="Memory not found in this profile")
|
|
1139
|
+
|
|
1140
|
+
engine._db.update_fact(fact_id, {
|
|
1141
|
+
"scope": scope,
|
|
1142
|
+
"shared_with": _json.dumps(shared_list),
|
|
1143
|
+
}, profile_id=active_profile)
|
|
1144
|
+
return {
|
|
1145
|
+
"success": True, "fact_id": fact_id, "scope": scope,
|
|
1146
|
+
"shared_with": shared_list, "active_profile": active_profile,
|
|
1147
|
+
}
|
|
1148
|
+
except HTTPException:
|
|
1149
|
+
raise
|
|
1150
|
+
except Exception:
|
|
1151
|
+
raise _internal_error("Scope update error")
|
|
@@ -11,6 +11,7 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
+
import asyncio
|
|
14
15
|
import re
|
|
15
16
|
from typing import Optional
|
|
16
17
|
|
|
@@ -120,6 +121,19 @@ def _get_broker(request: Request):
|
|
|
120
121
|
return broker
|
|
121
122
|
|
|
122
123
|
|
|
124
|
+
def _active_profile() -> str:
|
|
125
|
+
"""Resolve the tenant (profile) for this mesh request.
|
|
126
|
+
|
|
127
|
+
The mesh is a per-tenant coordination bus: peers, messages, shared state,
|
|
128
|
+
and locks are all scoped to the active profile so one tenant never sees
|
|
129
|
+
another's coordination traffic. Resolved from the request ContextVar set by
|
|
130
|
+
ProfileRuntimeMiddleware (falls back to the configured active profile).
|
|
131
|
+
"""
|
|
132
|
+
from superlocalmemory.server.routes.helpers import get_active_profile
|
|
133
|
+
|
|
134
|
+
return get_active_profile()
|
|
135
|
+
|
|
136
|
+
|
|
123
137
|
_SECRET_KEY = re.compile(
|
|
124
138
|
r"(?:^|[_\-.])(api[_\-.]?key|secret|token|password|credential)(?:$|[_\-.])",
|
|
125
139
|
re.IGNORECASE,
|
|
@@ -146,29 +160,79 @@ async def register(req: RegisterRequest, request: Request):
|
|
|
146
160
|
raise HTTPException(400, detail="session_id required")
|
|
147
161
|
return broker.register_peer(
|
|
148
162
|
req.session_id, req.summary, req.host, req.port,
|
|
149
|
-
req.project_path, req.agent_type,
|
|
163
|
+
req.project_path, req.agent_type, profile_id=_active_profile(),
|
|
150
164
|
)
|
|
151
165
|
|
|
152
166
|
|
|
153
167
|
@router.post("/deregister")
|
|
154
168
|
async def deregister(req: DeregisterRequest, request: Request):
|
|
155
169
|
broker = _get_broker(request)
|
|
156
|
-
result = broker.deregister_peer(req.peer_id)
|
|
170
|
+
result = broker.deregister_peer(req.peer_id, profile_id=_active_profile())
|
|
157
171
|
if not result.get("ok"):
|
|
158
172
|
raise HTTPException(404, detail=result.get("error", "peer not found"))
|
|
159
173
|
return result
|
|
160
174
|
|
|
161
175
|
|
|
176
|
+
def _peer_activity_counts(request: Request, session_ids: list[str]) -> dict:
|
|
177
|
+
"""Per-session {tool_count, memory_count} for the active profile.
|
|
178
|
+
|
|
179
|
+
Each mesh peer IS an agent session, so its activity is the tool events it
|
|
180
|
+
logged and the memories it contributed under the current profile. Batched
|
|
181
|
+
to a single grouped query per table. Fail-soft: returns {} on any error so
|
|
182
|
+
the peer list still renders.
|
|
183
|
+
"""
|
|
184
|
+
if not session_ids:
|
|
185
|
+
return {}
|
|
186
|
+
try:
|
|
187
|
+
from superlocalmemory.server.routes.helpers import (
|
|
188
|
+
get_engine_lazy, get_active_profile,
|
|
189
|
+
)
|
|
190
|
+
engine = get_engine_lazy(request.app.state)
|
|
191
|
+
if engine is None:
|
|
192
|
+
return {}
|
|
193
|
+
profile = get_active_profile()
|
|
194
|
+
placeholders = ",".join("?" * len(session_ids))
|
|
195
|
+
params = [profile, *session_ids]
|
|
196
|
+
counts: dict[str, dict] = {sid: {"tool_count": 0, "memory_count": 0}
|
|
197
|
+
for sid in session_ids}
|
|
198
|
+
for table, key in (("tool_events", "tool_count"),
|
|
199
|
+
("memories", "memory_count")):
|
|
200
|
+
try:
|
|
201
|
+
rows = engine._db.execute(
|
|
202
|
+
f"SELECT session_id, COUNT(*) AS n FROM {table} "
|
|
203
|
+
f"WHERE profile_id = ? AND session_id IN ({placeholders}) "
|
|
204
|
+
"GROUP BY session_id",
|
|
205
|
+
tuple(params),
|
|
206
|
+
)
|
|
207
|
+
for r in rows:
|
|
208
|
+
d = dict(r)
|
|
209
|
+
sid = d.get("session_id")
|
|
210
|
+
if sid in counts:
|
|
211
|
+
counts[sid][key] = d.get("n", 0)
|
|
212
|
+
except Exception:
|
|
213
|
+
continue
|
|
214
|
+
return counts
|
|
215
|
+
except Exception:
|
|
216
|
+
return {}
|
|
217
|
+
|
|
218
|
+
|
|
162
219
|
@router.get("/peers")
|
|
163
220
|
async def peers(request: Request):
|
|
164
221
|
broker = _get_broker(request)
|
|
165
|
-
|
|
222
|
+
peer_list = broker.list_all_peers(_active_profile())
|
|
223
|
+
session_ids = [p.get("session_id") for p in peer_list if p.get("session_id")]
|
|
224
|
+
counts = _peer_activity_counts(request, session_ids)
|
|
225
|
+
for p in peer_list:
|
|
226
|
+
c = counts.get(p.get("session_id"), {})
|
|
227
|
+
p["tool_count"] = c.get("tool_count", 0)
|
|
228
|
+
p["memory_count"] = c.get("memory_count", 0)
|
|
229
|
+
return {"peers": peer_list}
|
|
166
230
|
|
|
167
231
|
|
|
168
232
|
@router.post("/heartbeat")
|
|
169
233
|
async def heartbeat(req: HeartbeatRequest, request: Request):
|
|
170
234
|
broker = _get_broker(request)
|
|
171
|
-
result = broker.heartbeat(req.peer_id)
|
|
235
|
+
result = broker.heartbeat(req.peer_id, profile_id=_active_profile())
|
|
172
236
|
if not result.get("ok"):
|
|
173
237
|
raise HTTPException(404, detail=result.get("error", "peer not found"))
|
|
174
238
|
return result
|
|
@@ -177,7 +241,8 @@ async def heartbeat(req: HeartbeatRequest, request: Request):
|
|
|
177
241
|
@router.post("/summary")
|
|
178
242
|
async def summary(req: SummaryRequest, request: Request):
|
|
179
243
|
broker = _get_broker(request)
|
|
180
|
-
result = broker.update_summary(req.peer_id, req.summary
|
|
244
|
+
result = broker.update_summary(req.peer_id, req.summary,
|
|
245
|
+
profile_id=_active_profile())
|
|
181
246
|
if not result.get("ok"):
|
|
182
247
|
raise HTTPException(404, detail=result.get("error", "peer not found"))
|
|
183
248
|
return result
|
|
@@ -189,7 +254,17 @@ async def send(req: SendRequest, request: Request):
|
|
|
189
254
|
to_target = req.to_peer or req.to # v3.4.6: accept both field names
|
|
190
255
|
if not to_target:
|
|
191
256
|
raise HTTPException(400, detail="'to' or 'to_peer' required")
|
|
192
|
-
|
|
257
|
+
# Resolve the tenant on the event loop (the request ContextVar is set here);
|
|
258
|
+
# a worker thread would not inherit it.
|
|
259
|
+
profile = _active_profile()
|
|
260
|
+
# send_message may make a blocking httpx call (up to 10s) when delivering
|
|
261
|
+
# to a remote peer. Offload to a worker thread so a slow/dead peer network
|
|
262
|
+
# never stalls the daemon event loop for all other users. The broker opens
|
|
263
|
+
# a fresh SQLite connection per call, so this is thread-safe.
|
|
264
|
+
result = await asyncio.to_thread(
|
|
265
|
+
broker.send_message, req.from_peer, to_target, req.content,
|
|
266
|
+
req.type, "", profile,
|
|
267
|
+
)
|
|
193
268
|
if not result.get("ok"):
|
|
194
269
|
status = 413 if "too large" in result.get("error", "") else 404
|
|
195
270
|
raise HTTPException(status, detail=result.get("error", ""))
|
|
@@ -199,27 +274,30 @@ async def send(req: SendRequest, request: Request):
|
|
|
199
274
|
@router.get("/inbox/{peer_id}")
|
|
200
275
|
async def inbox(peer_id: str, request: Request, project_path: str = ""):
|
|
201
276
|
broker = _get_broker(request)
|
|
202
|
-
return {"messages": broker.get_inbox(peer_id, project_path
|
|
277
|
+
return {"messages": broker.get_inbox(peer_id, project_path,
|
|
278
|
+
profile_id=_active_profile())}
|
|
203
279
|
|
|
204
280
|
|
|
205
281
|
@router.post("/inbox/{peer_id}/read")
|
|
206
282
|
async def mark_read(peer_id: str, req: ReadRequest, request: Request):
|
|
207
283
|
broker = _get_broker(request)
|
|
208
|
-
return broker.mark_read(peer_id, req.message_ids
|
|
284
|
+
return broker.mark_read(peer_id, req.message_ids,
|
|
285
|
+
profile_id=_active_profile())
|
|
209
286
|
|
|
210
287
|
|
|
211
288
|
@router.get("/pending/{peer_id}")
|
|
212
289
|
async def pending(peer_id: str, request: Request, project_path: str = ""):
|
|
213
290
|
"""Get pending broadcast/project messages for this peer."""
|
|
214
291
|
broker = _get_broker(request)
|
|
215
|
-
messages = broker.get_pending(peer_id, project_path
|
|
292
|
+
messages = broker.get_pending(peer_id, project_path,
|
|
293
|
+
profile_id=_active_profile())
|
|
216
294
|
return {"messages": messages, "count": len(messages)}
|
|
217
295
|
|
|
218
296
|
|
|
219
297
|
@router.get("/state")
|
|
220
298
|
async def state_all(request: Request):
|
|
221
299
|
broker = _get_broker(request)
|
|
222
|
-
return {"state": broker.get_state()}
|
|
300
|
+
return {"state": broker.get_state(profile_id=_active_profile())}
|
|
223
301
|
|
|
224
302
|
|
|
225
303
|
@router.post("/state")
|
|
@@ -228,13 +306,14 @@ async def state_set(req: StateSetRequest, request: Request):
|
|
|
228
306
|
if not req.key:
|
|
229
307
|
raise HTTPException(400, detail="key required")
|
|
230
308
|
_reject_secret_state(req.key, req.value)
|
|
231
|
-
return broker.set_state(req.key, req.value, req.set_by
|
|
309
|
+
return broker.set_state(req.key, req.value, req.set_by,
|
|
310
|
+
profile_id=_active_profile())
|
|
232
311
|
|
|
233
312
|
|
|
234
313
|
@router.get("/state/{key}")
|
|
235
314
|
async def state_get(key: str, request: Request):
|
|
236
315
|
broker = _get_broker(request)
|
|
237
|
-
result = broker.get_state_key(key)
|
|
316
|
+
result = broker.get_state_key(key, profile_id=_active_profile())
|
|
238
317
|
if result is None:
|
|
239
318
|
raise HTTPException(404, detail="key not found")
|
|
240
319
|
return result
|
|
@@ -247,16 +326,17 @@ async def lock(req: LockRequest, request: Request):
|
|
|
247
326
|
raise HTTPException(400, detail="file_path and locked_by required")
|
|
248
327
|
if req.action not in ("acquire", "release", "query"):
|
|
249
328
|
raise HTTPException(400, detail="action must be acquire, release, or query")
|
|
250
|
-
return broker.lock_action(req.file_path, req.locked_by, req.action
|
|
329
|
+
return broker.lock_action(req.file_path, req.locked_by, req.action,
|
|
330
|
+
profile_id=_active_profile())
|
|
251
331
|
|
|
252
332
|
|
|
253
333
|
@router.get("/events")
|
|
254
334
|
async def events(request: Request):
|
|
255
335
|
broker = _get_broker(request)
|
|
256
|
-
return {"events": broker.get_events()}
|
|
336
|
+
return {"events": broker.get_events(profile_id=_active_profile())}
|
|
257
337
|
|
|
258
338
|
|
|
259
339
|
@router.get("/status")
|
|
260
340
|
async def status(request: Request):
|
|
261
341
|
broker = _get_broker(request)
|
|
262
|
-
return broker.get_status()
|
|
342
|
+
return broker.get_status(profile_id=_active_profile())
|
|
@@ -71,6 +71,36 @@ async def put_config(body: ConfigUpdateRequest) -> dict[str, Any]:
|
|
|
71
71
|
raise HTTPException(status_code=500, detail=str(exc))
|
|
72
72
|
|
|
73
73
|
|
|
74
|
+
@router.get("/status")
|
|
75
|
+
async def get_status() -> dict[str, Any]:
|
|
76
|
+
"""Concise enabled/disabled summary of each optimize sub-feature (M3).
|
|
77
|
+
|
|
78
|
+
REST-convention companion to `slm optimize status` (which reads config
|
|
79
|
+
locally). Returns a per-feature on/off view, the compression mode, and a
|
|
80
|
+
top-level `healthy` flag so external pollers have a stable endpoint.
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
from superlocalmemory.optimize.config import get_shared_store
|
|
84
|
+
cfg = get_shared_store().get()
|
|
85
|
+
return {
|
|
86
|
+
"healthy": True,
|
|
87
|
+
"enabled": cfg.enabled,
|
|
88
|
+
"features": {
|
|
89
|
+
"proxy": cfg.proxy_enabled,
|
|
90
|
+
"cache": cfg.cache_enabled,
|
|
91
|
+
"compression": cfg.compress_enabled,
|
|
92
|
+
"semantic_cache": cfg.semantic_enabled,
|
|
93
|
+
},
|
|
94
|
+
"compress_mode": cfg.compress_mode,
|
|
95
|
+
"config_version": cfg.config_version,
|
|
96
|
+
}
|
|
97
|
+
except Exception as exc:
|
|
98
|
+
logger.warning("GET /api/optimize/status failed: %s", exc)
|
|
99
|
+
# Fail-open: report unhealthy rather than 500 so a poller can
|
|
100
|
+
# distinguish "daemon up but optimize degraded" from "daemon down".
|
|
101
|
+
return {"healthy": False, "error": "internal error"}
|
|
102
|
+
|
|
103
|
+
|
|
74
104
|
@router.get("/savings")
|
|
75
105
|
async def get_savings() -> dict[str, Any]:
|
|
76
106
|
"""Return savings estimates from live metrics + CacheDB.
|
|
@@ -91,7 +121,9 @@ async def get_savings() -> dict[str, Any]:
|
|
|
91
121
|
)
|
|
92
122
|
|
|
93
123
|
# F-03 fix: use module-level singleton instead of per-request instantiation
|
|
94
|
-
|
|
124
|
+
# DASH-V6 fix: attribute may EXIST but be None; getattr default only
|
|
125
|
+
# covers the missing case, so coerce None -> default to avoid .lower() crash.
|
|
126
|
+
active_model = getattr(cfg, "active_model", None) or "anthropic"
|
|
95
127
|
# Determine provider from model
|
|
96
128
|
provider = "anthropic"
|
|
97
129
|
for key in ("anthropic", "openai", "gemini"):
|