superlocalmemory 3.7.8 → 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 +69 -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 -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 +94 -10
- 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/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 +8 -1
- 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 +4 -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 +10 -5
- 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 +182 -57
- 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 +183 -69
- 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 +384 -56
- 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_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
|
@@ -13,11 +13,29 @@ from __future__ import annotations
|
|
|
13
13
|
import hashlib
|
|
14
14
|
import hmac
|
|
15
15
|
import os
|
|
16
|
+
import sys
|
|
16
17
|
from typing import Any
|
|
17
18
|
|
|
18
19
|
from fastapi import HTTPException, Request
|
|
19
20
|
|
|
20
21
|
|
|
22
|
+
# H-04 (3.7.9): the test-client auth bypass must be impossible in a real daemon
|
|
23
|
+
# even if SLM_TEST_ISOLATION=1 leaks into a production environment. Anchor it to
|
|
24
|
+
# pytest being present in the running interpreter — a production daemon process
|
|
25
|
+
# never imports pytest — decided once at import time so it cannot be toggled per
|
|
26
|
+
# request at runtime.
|
|
27
|
+
_TEST_ISOLATION_ALLOWED = (
|
|
28
|
+
os.environ.get("SLM_TEST_ISOLATION") == "1" and "pytest" in sys.modules
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# H-01 (3.7.9): by default a same-user loopback process is a trusted principal
|
|
32
|
+
# and may write without a credential. Operators who want stricter isolation
|
|
33
|
+
# (e.g. a shared machine, or binding beyond loopback) set
|
|
34
|
+
# SLM_REQUIRE_CREDENTIALS=1 so that even loopback must present a daemon
|
|
35
|
+
# capability, install token, or API key. Default OFF — unchanged behaviour.
|
|
36
|
+
_REQUIRE_CREDENTIALS = os.environ.get("SLM_REQUIRE_CREDENTIALS") == "1"
|
|
37
|
+
|
|
38
|
+
|
|
21
39
|
def _header(request: Request, name: str) -> str:
|
|
22
40
|
headers = request.headers
|
|
23
41
|
value = headers.get(name, "")
|
|
@@ -60,9 +78,21 @@ def require_write_actor(
|
|
|
60
78
|
from superlocalmemory.core.security_primitives import verify_install_token
|
|
61
79
|
|
|
62
80
|
if verify_install_token(_header(request, "X-Install-Token")):
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
81
|
+
# The install token is embedded in the dashboard JS served over HTTP, so
|
|
82
|
+
# a LAN observer can read it. Accept it ONLY from a loopback caller (the
|
|
83
|
+
# local dashboard/CLI). A non-loopback holder must use an API key — this
|
|
84
|
+
# closes LAN privilege escalation via a leaked install token. In-process
|
|
85
|
+
# tests (testclient) keep the bypass only under SLM_TEST_ISOLATION.
|
|
86
|
+
client_host = request.client.host if request.client else ""
|
|
87
|
+
is_test_client = client_host == "testclient" and _TEST_ISOLATION_ALLOWED
|
|
88
|
+
# SEC-L-02: do NOT treat an empty client_host as loopback here — a
|
|
89
|
+
# missing peer address (e.g. behind a proxy that strips it) must not be
|
|
90
|
+
# trusted just because an install token is presented. require_http_mutation_actor
|
|
91
|
+
# already excludes "" from its loopback set; keep the two paths consistent.
|
|
92
|
+
if is_test_client or client_host in ("127.0.0.1", "::1", "localhost"):
|
|
93
|
+
from superlocalmemory.core.engine_ingestion import local_trusted_actor_id
|
|
94
|
+
|
|
95
|
+
return local_trusted_actor_id(actor_kind)
|
|
66
96
|
|
|
67
97
|
from superlocalmemory.infra.auth_middleware import verify_api_key
|
|
68
98
|
|
|
@@ -103,11 +133,11 @@ def require_http_mutation_actor(
|
|
|
103
133
|
)
|
|
104
134
|
|
|
105
135
|
client_host = request.client.host if request.client else ""
|
|
106
|
-
is_test_client =
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if
|
|
136
|
+
is_test_client = client_host == "testclient" and _TEST_ISOLATION_ALLOWED
|
|
137
|
+
loopback = client_host in ("127.0.0.1", "::1", "localhost")
|
|
138
|
+
# H-01: the loopback trusted-actor bypass is suppressed when the operator
|
|
139
|
+
# opts into SLM_REQUIRE_CREDENTIALS; in-process tests keep the bypass.
|
|
140
|
+
if is_test_client or (loopback and not _REQUIRE_CREDENTIALS):
|
|
111
141
|
from superlocalmemory.core.engine_ingestion import local_trusted_actor_id
|
|
112
142
|
|
|
113
143
|
return local_trusted_actor_id(actor_kind)
|
|
@@ -70,6 +70,17 @@ _MAX_RETRIES = _env_int("SLM_DB_MAX_RETRIES", 5) # retry on SQLIT
|
|
|
70
70
|
_RETRY_BASE_DELAY = _env_float("SLM_DB_RETRY_BASE_DELAY", 0.1) # backoff base (s)
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
def _unbounded_facts_ceiling() -> int:
|
|
74
|
+
"""Hard upper bound applied when a fact fetch is called with limit=None, so
|
|
75
|
+
a large tenant can never materialize the whole table into memory. Tunable
|
|
76
|
+
via SLM_MAX_FACTS_UNBOUNDED (default 50_000)."""
|
|
77
|
+
import os as _os
|
|
78
|
+
try:
|
|
79
|
+
return max(1, int(_os.environ.get("SLM_MAX_FACTS_UNBOUNDED", "50000")))
|
|
80
|
+
except (TypeError, ValueError):
|
|
81
|
+
return 50000
|
|
82
|
+
|
|
83
|
+
|
|
73
84
|
def _scope_where(
|
|
74
85
|
profile_id: str,
|
|
75
86
|
*,
|
|
@@ -147,6 +158,14 @@ class DatabaseManager:
|
|
|
147
158
|
conn.commit()
|
|
148
159
|
finally:
|
|
149
160
|
conn.close()
|
|
161
|
+
# C4 encryption-at-rest defense-in-depth: sensitive DB files shipped
|
|
162
|
+
# world-readable (0644). Restrict to owner-only now that the file (and
|
|
163
|
+
# its WAL sidecars) exist. Best-effort, never blocks DB open.
|
|
164
|
+
try:
|
|
165
|
+
from superlocalmemory.core.security_primitives import harden_db_perms
|
|
166
|
+
harden_db_perms(self.db_path)
|
|
167
|
+
except Exception:
|
|
168
|
+
pass
|
|
150
169
|
|
|
151
170
|
def initialize(self, schema_module: ModuleType) -> None:
|
|
152
171
|
"""Create all tables. *schema_module* must expose ``create_all_tables(conn)``."""
|
|
@@ -431,17 +450,16 @@ class DatabaseManager:
|
|
|
431
450
|
include_global=include_global,
|
|
432
451
|
include_shared=include_shared,
|
|
433
452
|
)
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
)
|
|
453
|
+
# memory-bounding-02 + perf M-04: an unbounded fetch materializes the
|
|
454
|
+
# whole table (hundreds of MB with embeddings at 50k+ facts). Apply a
|
|
455
|
+
# hard, env-tunable ceiling even when the caller passes limit=None.
|
|
456
|
+
if limit is None:
|
|
457
|
+
limit = _unbounded_facts_ceiling()
|
|
458
|
+
rows = self.execute(
|
|
459
|
+
f"SELECT * FROM atomic_facts WHERE {where} "
|
|
460
|
+
"ORDER BY created_at DESC LIMIT ?",
|
|
461
|
+
(*params, int(limit)),
|
|
462
|
+
)
|
|
445
463
|
return [self._row_to_fact(r) for r in rows]
|
|
446
464
|
|
|
447
465
|
def get_external_visible_facts(
|
|
@@ -525,10 +543,19 @@ class DatabaseManager:
|
|
|
525
543
|
"source_turn_ids_json", "session_id", "embedding",
|
|
526
544
|
"fisher_mean", "fisher_variance", "lifecycle", "langevin_position",
|
|
527
545
|
"emotional_valence", "emotional_arousal", "signal_type",
|
|
546
|
+
# Multi-scope (M016): allow re-scoping a fact after creation so a memory
|
|
547
|
+
# can be shared with a team or made global from the dashboard.
|
|
548
|
+
"scope", "shared_with",
|
|
528
549
|
})
|
|
529
550
|
|
|
530
|
-
def update_fact(self, fact_id: str, updates: dict[str, Any]
|
|
531
|
-
|
|
551
|
+
def update_fact(self, fact_id: str, updates: dict[str, Any],
|
|
552
|
+
profile_id: str | None = None) -> None:
|
|
553
|
+
"""Partial update on a fact. JSON-serializes list/dict values.
|
|
554
|
+
|
|
555
|
+
Tenant safety: when ``profile_id`` is supplied the UPDATE is constrained
|
|
556
|
+
to that tenant, so a fact_id belonging to another profile cannot be
|
|
557
|
+
mutated. Authorized routes always pass it.
|
|
558
|
+
"""
|
|
532
559
|
if not updates:
|
|
533
560
|
raise ValueError("updates dict must not be empty")
|
|
534
561
|
bad_keys = set(updates) - self._UPDATABLE_FACT_COLUMNS
|
|
@@ -543,21 +570,44 @@ class DatabaseManager:
|
|
|
543
570
|
else:
|
|
544
571
|
clean[k] = v
|
|
545
572
|
set_clause = ", ".join(f"{k} = ?" for k in clean)
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
573
|
+
if profile_id is not None:
|
|
574
|
+
self.execute(
|
|
575
|
+
f"UPDATE atomic_facts SET {set_clause} WHERE fact_id = ? AND profile_id = ?",
|
|
576
|
+
(*clean.values(), fact_id, profile_id),
|
|
577
|
+
)
|
|
578
|
+
else:
|
|
579
|
+
self.execute(
|
|
580
|
+
f"UPDATE atomic_facts SET {set_clause} WHERE fact_id = ?",
|
|
581
|
+
(*clean.values(), fact_id),
|
|
582
|
+
)
|
|
550
583
|
|
|
551
|
-
def delete_fact(self, fact_id: str) -> None:
|
|
584
|
+
def delete_fact(self, fact_id: str, profile_id: str | None = None) -> None:
|
|
552
585
|
"""Hard-delete a fact.
|
|
553
586
|
|
|
554
587
|
DatabaseManager connections enforce FKs (PRAGMA foreign_keys=ON), so
|
|
555
588
|
embedding_metadata / fact_retention / edges cascade. The explicit
|
|
556
589
|
embedding_metadata delete below is belt-and-suspenders for the case a
|
|
557
590
|
future caller routes through a connection without FK enforcement.
|
|
591
|
+
|
|
592
|
+
Tenant safety: when ``profile_id`` is supplied the delete is constrained
|
|
593
|
+
to that tenant (the fact must belong to it), so a fact_id from another
|
|
594
|
+
profile cannot be destroyed. Authorized routes always pass it.
|
|
558
595
|
"""
|
|
596
|
+
if profile_id is not None:
|
|
597
|
+
row = self.execute(
|
|
598
|
+
"SELECT 1 FROM atomic_facts WHERE fact_id = ? AND profile_id = ?",
|
|
599
|
+
(fact_id, profile_id),
|
|
600
|
+
)
|
|
601
|
+
if not row:
|
|
602
|
+
return # not this tenant's fact — no-op
|
|
559
603
|
self.execute("DELETE FROM embedding_metadata WHERE fact_id = ?", (fact_id,))
|
|
560
|
-
|
|
604
|
+
if profile_id is not None:
|
|
605
|
+
self.execute(
|
|
606
|
+
"DELETE FROM atomic_facts WHERE fact_id = ? AND profile_id = ?",
|
|
607
|
+
(fact_id, profile_id),
|
|
608
|
+
)
|
|
609
|
+
else:
|
|
610
|
+
self.execute("DELETE FROM atomic_facts WHERE fact_id = ?", (fact_id,))
|
|
561
611
|
|
|
562
612
|
def gc_orphaned_embedding_metadata(self) -> int:
|
|
563
613
|
"""Remove embedding_metadata rows whose parent atomic_fact is gone.
|
|
@@ -626,35 +676,55 @@ class DatabaseManager:
|
|
|
626
676
|
fact_count=d["fact_count"],
|
|
627
677
|
)
|
|
628
678
|
|
|
629
|
-
def store_alias(self, alias: EntityAlias) -> str:
|
|
630
|
-
"""Persist an entity alias. Returns alias_id.
|
|
679
|
+
def store_alias(self, alias: EntityAlias, profile_id: str) -> str:
|
|
680
|
+
"""Persist an entity alias under a profile. Returns alias_id.
|
|
681
|
+
|
|
682
|
+
profile_id scopes the alias so the same entity_id appearing in two
|
|
683
|
+
profiles never shares aliases.
|
|
684
|
+
"""
|
|
631
685
|
self.execute(
|
|
632
686
|
"INSERT OR REPLACE INTO entity_aliases "
|
|
633
|
-
"(alias_id, entity_id, alias, confidence, source)
|
|
634
|
-
(
|
|
687
|
+
"(alias_id, profile_id, entity_id, alias, confidence, source) "
|
|
688
|
+
"VALUES (?,?,?,?,?,?)",
|
|
689
|
+
(alias.alias_id, profile_id, alias.entity_id, alias.alias,
|
|
635
690
|
alias.confidence, alias.source),
|
|
636
691
|
)
|
|
637
692
|
return alias.alias_id
|
|
638
693
|
|
|
639
|
-
def get_aliases_for_entity(
|
|
640
|
-
|
|
694
|
+
def get_aliases_for_entity(
|
|
695
|
+
self, entity_id: str, profile_id: str,
|
|
696
|
+
) -> list[EntityAlias]:
|
|
697
|
+
"""All aliases for a canonical entity within one profile."""
|
|
641
698
|
rows = self.execute(
|
|
642
|
-
"SELECT * FROM entity_aliases WHERE entity_id = ?",
|
|
699
|
+
"SELECT * FROM entity_aliases WHERE entity_id = ? AND profile_id = ?",
|
|
700
|
+
(entity_id, profile_id),
|
|
643
701
|
)
|
|
644
702
|
return [
|
|
645
703
|
EntityAlias(**{k: dict(r)[k] for k in ("alias_id", "entity_id", "alias", "confidence", "source")})
|
|
646
704
|
for r in rows
|
|
647
705
|
]
|
|
648
706
|
|
|
649
|
-
def get_memory_content_batch(
|
|
650
|
-
|
|
707
|
+
def get_memory_content_batch(
|
|
708
|
+
self, memory_ids: list[str], profile_id: str,
|
|
709
|
+
include_global: bool = False, include_shared: bool = False,
|
|
710
|
+
) -> dict[str, str]:
|
|
711
|
+
"""Batch-fetch original memory text. Returns {memory_id: content}.
|
|
712
|
+
|
|
713
|
+
C4 hardening: this exposes raw memory *content* and is reachable from
|
|
714
|
+
HTTP routes, so it is strictly tenant-scoped — a memory_id belonging to
|
|
715
|
+
another profile is never resolved. Widen only via the scope flags.
|
|
716
|
+
"""
|
|
651
717
|
if not memory_ids:
|
|
652
718
|
return {}
|
|
653
719
|
unique_ids = list(set(memory_ids))
|
|
654
720
|
ph = ','.join('?' * len(unique_ids))
|
|
721
|
+
where, sparams = _scope_where(
|
|
722
|
+
profile_id, include_global=include_global, include_shared=include_shared,
|
|
723
|
+
)
|
|
655
724
|
rows = self.execute(
|
|
656
|
-
f"SELECT memory_id, content FROM memories
|
|
657
|
-
|
|
725
|
+
f"SELECT memory_id, content FROM memories "
|
|
726
|
+
f"WHERE memory_id IN ({ph}) AND {where}",
|
|
727
|
+
(*unique_ids, *sparams),
|
|
658
728
|
)
|
|
659
729
|
return {dict(r)["memory_id"]: dict(r)["content"] for r in rows}
|
|
660
730
|
|
|
@@ -876,11 +946,23 @@ class DatabaseManager:
|
|
|
876
946
|
# Phase 0.6: Missing methods (BLOCKER / CRITICAL / HIGH)
|
|
877
947
|
# ------------------------------------------------------------------
|
|
878
948
|
|
|
879
|
-
def get_fact(self, fact_id: str) -> AtomicFact | None:
|
|
880
|
-
"""Get a single fact by ID.
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
949
|
+
def get_fact(self, fact_id: str, profile_id: str | None = None) -> AtomicFact | None:
|
|
950
|
+
"""Get a single fact by ID.
|
|
951
|
+
|
|
952
|
+
C4 defense-in-depth: when ``profile_id`` is provided the lookup is
|
|
953
|
+
tenant-scoped so a fact_id from another profile cannot resolve. Left
|
|
954
|
+
optional (fact_id is a random UUID sourced from already-scoped queries)
|
|
955
|
+
to avoid destabilizing the core store/consolidation write path.
|
|
956
|
+
"""
|
|
957
|
+
if profile_id is not None:
|
|
958
|
+
rows = self.execute(
|
|
959
|
+
"SELECT * FROM atomic_facts WHERE fact_id = ? AND profile_id = ?",
|
|
960
|
+
(fact_id, profile_id),
|
|
961
|
+
)
|
|
962
|
+
else:
|
|
963
|
+
rows = self.execute(
|
|
964
|
+
"SELECT * FROM atomic_facts WHERE fact_id = ?", (fact_id,),
|
|
965
|
+
)
|
|
884
966
|
return self._row_to_fact(rows[0]) if rows else None
|
|
885
967
|
|
|
886
968
|
def get_facts_by_ids(
|
|
@@ -1017,12 +1099,17 @@ class DatabaseManager:
|
|
|
1017
1099
|
out.setdefault(fid, []).append(scene)
|
|
1018
1100
|
return out
|
|
1019
1101
|
|
|
1020
|
-
def increment_entity_fact_count(self, entity_id: str) -> None:
|
|
1021
|
-
"""Atomically increment fact_count for a canonical entity.
|
|
1102
|
+
def increment_entity_fact_count(self, entity_id: str, profile_id: str = "default") -> None:
|
|
1103
|
+
"""Atomically increment fact_count for a canonical entity scoped to profile.
|
|
1104
|
+
|
|
1105
|
+
L-01 fix: the original query had no profile_id guard; any entity_id match
|
|
1106
|
+
would be updated regardless of owner. The AND profile_id = ? clause prevents
|
|
1107
|
+
cross-profile mutations via shared entity_id values.
|
|
1108
|
+
"""
|
|
1022
1109
|
self.execute(
|
|
1023
1110
|
"UPDATE canonical_entities SET fact_count = fact_count + 1 "
|
|
1024
|
-
"WHERE entity_id = ?",
|
|
1025
|
-
(entity_id,),
|
|
1111
|
+
"WHERE entity_id = ? AND profile_id = ?",
|
|
1112
|
+
(entity_id, profile_id),
|
|
1026
1113
|
)
|
|
1027
1114
|
|
|
1028
1115
|
def store_trust_score(self, ts: TrustScore) -> str:
|
|
@@ -1121,11 +1208,17 @@ class DatabaseManager:
|
|
|
1121
1208
|
(fact_id, profile_id, contextual_description, keywords, generated_by),
|
|
1122
1209
|
)
|
|
1123
1210
|
|
|
1124
|
-
def get_fact_context(self, fact_id: str) -> dict | None:
|
|
1125
|
-
"""Get contextual description for a fact."""
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1211
|
+
def get_fact_context(self, fact_id: str, profile_id: str | None = None) -> dict | None:
|
|
1212
|
+
"""Get contextual description for a fact (C4: optionally tenant-scoped)."""
|
|
1213
|
+
if profile_id is not None:
|
|
1214
|
+
rows = self.execute(
|
|
1215
|
+
"SELECT * FROM fact_context WHERE fact_id = ? AND profile_id = ?",
|
|
1216
|
+
(fact_id, profile_id),
|
|
1217
|
+
)
|
|
1218
|
+
else:
|
|
1219
|
+
rows = self.execute(
|
|
1220
|
+
"SELECT * FROM fact_context WHERE fact_id = ?", (fact_id,),
|
|
1221
|
+
)
|
|
1129
1222
|
return dict(rows[0]) if rows else None
|
|
1130
1223
|
|
|
1131
1224
|
def get_all_fact_contexts(self, profile_id: str) -> list[dict]:
|
|
@@ -1277,12 +1370,18 @@ class DatabaseManager:
|
|
|
1277
1370
|
(fact_id, profile_id, valid_from, valid_until),
|
|
1278
1371
|
)
|
|
1279
1372
|
|
|
1280
|
-
def get_temporal_validity(self, fact_id: str) -> dict | None:
|
|
1281
|
-
"""Get temporal validity record for a fact."""
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1373
|
+
def get_temporal_validity(self, fact_id: str, profile_id: str | None = None) -> dict | None:
|
|
1374
|
+
"""Get temporal validity record for a fact (C4: optionally tenant-scoped)."""
|
|
1375
|
+
if profile_id is not None:
|
|
1376
|
+
rows = self.execute(
|
|
1377
|
+
"SELECT * FROM fact_temporal_validity WHERE fact_id = ? AND profile_id = ?",
|
|
1378
|
+
(fact_id, profile_id),
|
|
1379
|
+
)
|
|
1380
|
+
else:
|
|
1381
|
+
rows = self.execute(
|
|
1382
|
+
"SELECT * FROM fact_temporal_validity WHERE fact_id = ?",
|
|
1383
|
+
(fact_id,),
|
|
1384
|
+
)
|
|
1286
1385
|
return dict(rows[0]) if rows else None
|
|
1287
1386
|
|
|
1288
1387
|
def get_all_temporal_validity(self, profile_id: str) -> list[dict]:
|
|
@@ -1316,18 +1415,106 @@ class DatabaseManager:
|
|
|
1316
1415
|
"""Get fact_ids that are currently valid (not expired).
|
|
1317
1416
|
|
|
1318
1417
|
Returns facts that either have no temporal record (assumed valid)
|
|
1319
|
-
or
|
|
1418
|
+
or whose temporal record satisfies:
|
|
1419
|
+
- valid_until IS NULL (open-ended) OR valid_until > now() (still in window)
|
|
1420
|
+
- system_expired_at IS NULL (not system-invalidated)
|
|
1421
|
+
|
|
1422
|
+
M-02 fix: the original query used ``tv.valid_until IS NULL`` which
|
|
1423
|
+
incorrectly excluded future-dated (still valid) facts. The correct
|
|
1424
|
+
predicate is a date comparison against the current timestamp.
|
|
1320
1425
|
"""
|
|
1321
1426
|
rows = self.execute(
|
|
1322
1427
|
"SELECT f.fact_id FROM atomic_facts f "
|
|
1323
1428
|
"LEFT JOIN fact_temporal_validity tv ON f.fact_id = tv.fact_id "
|
|
1324
1429
|
"WHERE f.profile_id = ? "
|
|
1325
|
-
" AND (tv.fact_id IS NULL
|
|
1326
|
-
"
|
|
1430
|
+
" AND (tv.fact_id IS NULL "
|
|
1431
|
+
" OR ( "
|
|
1432
|
+
" (tv.valid_until IS NULL "
|
|
1433
|
+
" OR tv.valid_until > strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) "
|
|
1434
|
+
" AND tv.system_expired_at IS NULL "
|
|
1435
|
+
" ))",
|
|
1327
1436
|
(profile_id,),
|
|
1328
1437
|
)
|
|
1329
1438
|
return [dict(r)["fact_id"] for r in rows]
|
|
1330
1439
|
|
|
1440
|
+
def get_invalidated_fact_ids(
|
|
1441
|
+
self, fact_ids: list[str], profile_id: str,
|
|
1442
|
+
) -> set[str]:
|
|
1443
|
+
"""Return the subset of ``fact_ids`` that are system-invalidated.
|
|
1444
|
+
|
|
1445
|
+
A fact is system-invalidated when ``system_expired_at`` is set — i.e.
|
|
1446
|
+
it was superseded/contradicted by a newer fact (see
|
|
1447
|
+
``invalidate_fact_temporal``). Such facts are wrong/outdated and must be
|
|
1448
|
+
excluded from default retrieval (T1, Phase 4).
|
|
1449
|
+
|
|
1450
|
+
Bounded + indexed: only the supplied candidate ids are queried (never a
|
|
1451
|
+
full-table scan), keyed on the ``fact_id`` PK with the
|
|
1452
|
+
``idx_temporal_system_expired`` index covering the predicate. Chunked to
|
|
1453
|
+
stay well under SQLite's ~999 bound-parameter limit. Facts with no
|
|
1454
|
+
temporal record — or a record whose ``system_expired_at`` is NULL — are
|
|
1455
|
+
NOT returned (treated as valid), so existing DBs need no backfill.
|
|
1456
|
+
|
|
1457
|
+
Event-time expiry (``valid_until`` in the past) is intentionally NOT
|
|
1458
|
+
applied here: it is query-scoped (historical queries legitimately want
|
|
1459
|
+
expired facts, per ``include_expired_in_history``) and handled by the
|
|
1460
|
+
time-window path, not by this blanket admission filter.
|
|
1461
|
+
"""
|
|
1462
|
+
if not fact_ids:
|
|
1463
|
+
return set()
|
|
1464
|
+
invalid: set[str] = set()
|
|
1465
|
+
chunk = 900
|
|
1466
|
+
for start in range(0, len(fact_ids), chunk):
|
|
1467
|
+
batch = fact_ids[start:start + chunk]
|
|
1468
|
+
placeholders = ",".join("?" for _ in batch)
|
|
1469
|
+
rows = self.execute(
|
|
1470
|
+
f"SELECT fact_id FROM fact_temporal_validity "
|
|
1471
|
+
f"WHERE fact_id IN ({placeholders}) "
|
|
1472
|
+
f" AND profile_id = ? "
|
|
1473
|
+
f" AND system_expired_at IS NOT NULL",
|
|
1474
|
+
(*batch, profile_id),
|
|
1475
|
+
)
|
|
1476
|
+
for r in rows:
|
|
1477
|
+
invalid.add(dict(r)["fact_id"])
|
|
1478
|
+
return invalid
|
|
1479
|
+
|
|
1480
|
+
def get_fact_event_times(
|
|
1481
|
+
self, fact_ids: list[str], profile_id: str,
|
|
1482
|
+
) -> dict[str, str]:
|
|
1483
|
+
"""Map each candidate fact_id to its best-available event time.
|
|
1484
|
+
|
|
1485
|
+
Priority (most specific first): ``referenced_date`` (the date the fact
|
|
1486
|
+
is *about*) → ``observation_date`` (when it was observed) →
|
|
1487
|
+
``valid_from`` (bi-temporal event start) → ``created_at`` (storage
|
|
1488
|
+
time, always present). Used by time-window recall to prune candidates
|
|
1489
|
+
by when the underlying event happened, falling back to capture time for
|
|
1490
|
+
undated facts.
|
|
1491
|
+
|
|
1492
|
+
Bounded + indexed (candidate ids only, ``fact_id`` PK), chunked under
|
|
1493
|
+
SQLite's bound-parameter limit. Facts absent from the result (unknown
|
|
1494
|
+
id / wrong profile) are simply omitted.
|
|
1495
|
+
"""
|
|
1496
|
+
if not fact_ids:
|
|
1497
|
+
return {}
|
|
1498
|
+
out: dict[str, str] = {}
|
|
1499
|
+
chunk = 900
|
|
1500
|
+
for start in range(0, len(fact_ids), chunk):
|
|
1501
|
+
batch = fact_ids[start:start + chunk]
|
|
1502
|
+
placeholders = ",".join("?" for _ in batch)
|
|
1503
|
+
rows = self.execute(
|
|
1504
|
+
f"SELECT f.fact_id AS fact_id, "
|
|
1505
|
+
f"COALESCE(f.referenced_date, f.observation_date, "
|
|
1506
|
+
f" tv.valid_from, f.created_at) AS event_time "
|
|
1507
|
+
f"FROM atomic_facts f "
|
|
1508
|
+
f"LEFT JOIN fact_temporal_validity tv ON f.fact_id = tv.fact_id "
|
|
1509
|
+
f"WHERE f.fact_id IN ({placeholders}) AND f.profile_id = ?",
|
|
1510
|
+
(*batch, profile_id),
|
|
1511
|
+
)
|
|
1512
|
+
for r in rows:
|
|
1513
|
+
d = dict(r)
|
|
1514
|
+
if d.get("event_time"):
|
|
1515
|
+
out[d["fact_id"]] = d["event_time"]
|
|
1516
|
+
return out
|
|
1517
|
+
|
|
1331
1518
|
def delete_temporal_validity(self, fact_id: str) -> None:
|
|
1332
1519
|
"""Delete temporal validity record (for testing/rollback only)."""
|
|
1333
1520
|
self.execute(
|
|
@@ -1335,6 +1522,31 @@ class DatabaseManager:
|
|
|
1335
1522
|
(fact_id,),
|
|
1336
1523
|
)
|
|
1337
1524
|
|
|
1525
|
+
# ------------------------------------------------------------------
|
|
1526
|
+
# Phase 4 (T3b): fact-augmented key expansion (BM25 alt-keys)
|
|
1527
|
+
# ------------------------------------------------------------------
|
|
1528
|
+
|
|
1529
|
+
def upsert_fact_expansion(self, fact_id: str, alt_keys: str) -> None:
|
|
1530
|
+
"""Store/replace a fact's alternate keys in ``fact_expansion_fts``.
|
|
1531
|
+
|
|
1532
|
+
Standalone FTS5 (no external-content triggers), so we replace by hand:
|
|
1533
|
+
delete any prior row for the fact, then insert the new alt-keys. An
|
|
1534
|
+
empty/blank ``alt_keys`` clears the fact's expansion entry. Fail-soft:
|
|
1535
|
+
a missing FTS table (legacy DB) never breaks the write path.
|
|
1536
|
+
"""
|
|
1537
|
+
try:
|
|
1538
|
+
self.execute(
|
|
1539
|
+
"DELETE FROM fact_expansion_fts WHERE fact_id = ?", (fact_id,)
|
|
1540
|
+
)
|
|
1541
|
+
if alt_keys and alt_keys.strip():
|
|
1542
|
+
self.execute(
|
|
1543
|
+
"INSERT INTO fact_expansion_fts (fact_id, alt_keys) "
|
|
1544
|
+
"VALUES (?, ?)",
|
|
1545
|
+
(fact_id, alt_keys.strip()),
|
|
1546
|
+
)
|
|
1547
|
+
except Exception as exc: # pragma: no cover — legacy/missing FTS table
|
|
1548
|
+
logger.debug("upsert_fact_expansion skipped for %s: %s", fact_id, exc)
|
|
1549
|
+
|
|
1338
1550
|
# ------------------------------------------------------------------
|
|
1339
1551
|
# Phase 5: Core Memory Blocks CRUD (Rule 15)
|
|
1340
1552
|
# ------------------------------------------------------------------
|
|
@@ -92,6 +92,27 @@ from superlocalmemory.storage.migrations import (
|
|
|
92
92
|
from superlocalmemory.storage.migrations import (
|
|
93
93
|
M020_model_state_integrity as _M020,
|
|
94
94
|
)
|
|
95
|
+
from superlocalmemory.storage.migrations import (
|
|
96
|
+
M021_ingestion_log_profile as _M021,
|
|
97
|
+
)
|
|
98
|
+
from superlocalmemory.storage.migrations import (
|
|
99
|
+
M022_entity_aliases_profile as _M022,
|
|
100
|
+
)
|
|
101
|
+
from superlocalmemory.storage.migrations import (
|
|
102
|
+
M023_mesh_profile_isolation as _M023,
|
|
103
|
+
)
|
|
104
|
+
from superlocalmemory.storage.migrations import (
|
|
105
|
+
M024_rbac_users_roles as _M024,
|
|
106
|
+
)
|
|
107
|
+
from superlocalmemory.storage.migrations import (
|
|
108
|
+
M025_perf_indexes as _M025,
|
|
109
|
+
)
|
|
110
|
+
from superlocalmemory.storage.migrations import (
|
|
111
|
+
M026_rbac_memberships_fk as _M026,
|
|
112
|
+
)
|
|
113
|
+
from superlocalmemory.storage.migrations import (
|
|
114
|
+
M027_transferable_patterns_profile as _M027,
|
|
115
|
+
)
|
|
95
116
|
|
|
96
117
|
# Map migration name → module (used for the optional ``verify(conn)`` hook
|
|
97
118
|
# that lets the runner detect "already applied" state when an idempotent
|
|
@@ -116,6 +137,13 @@ _MODULES = {
|
|
|
116
137
|
_M018.NAME: _M018,
|
|
117
138
|
_M019.NAME: _M019,
|
|
118
139
|
_M020.NAME: _M020,
|
|
140
|
+
_M021.NAME: _M021,
|
|
141
|
+
_M022.NAME: _M022,
|
|
142
|
+
_M023.NAME: _M023,
|
|
143
|
+
_M024.NAME: _M024,
|
|
144
|
+
_M025.NAME: _M025,
|
|
145
|
+
_M026.NAME: _M026,
|
|
146
|
+
_M027.NAME: _M027,
|
|
119
147
|
}
|
|
120
148
|
|
|
121
149
|
logger = logging.getLogger(__name__)
|
|
@@ -171,6 +199,9 @@ MIGRATIONS: list[Migration] = [
|
|
|
171
199
|
Migration(name=_M007.NAME, db_target="memory", ddl=_M007.DDL),
|
|
172
200
|
# M018 is additive and independent of runtime-bootstrapped tables.
|
|
173
201
|
Migration(name=_M018.NAME, db_target="memory", ddl=_M018.DDL),
|
|
202
|
+
# M024 creates RBAC tables (users / memberships / sessions). Independent
|
|
203
|
+
# brand-new tables, so it runs pre-engine-init.
|
|
204
|
+
Migration(name=_M024.NAME, db_target="memory", ddl=_M024.DDL),
|
|
174
205
|
Migration(name=_M019.NAME, db_target="memory", ddl=_M019.DDL,
|
|
175
206
|
dependencies=(_M018.NAME,)),
|
|
176
207
|
# M006 + M011 are deliberately NOT here — see DEFERRED_MIGRATIONS below.
|
|
@@ -203,6 +234,28 @@ DEFERRED_MIGRATIONS: list[Migration] = [
|
|
|
203
234
|
Migration(name=_M016.NAME, db_target="memory", ddl=_M016.DDL),
|
|
204
235
|
# M017 adds scope to the engine-bootstrapped CCQ consolidation table.
|
|
205
236
|
Migration(name=_M017.NAME, db_target="memory", ddl=_M017.DDL),
|
|
237
|
+
# M021 rebuilds ingestion_log with a profile-scoped dedup constraint.
|
|
238
|
+
# Deferred: ingestion_log is created at engine init (apply_v343_schema).
|
|
239
|
+
Migration(name=_M021.NAME, db_target="memory", ddl=_M021.DDL),
|
|
240
|
+
# M022 adds profile_id to entity_aliases, backfilled from the parent entity.
|
|
241
|
+
# Deferred: entity_aliases is created at engine init (create_all_tables).
|
|
242
|
+
Migration(name=_M022.NAME, db_target="memory", ddl=_M022.DDL),
|
|
243
|
+
# M023 profile-scopes every mesh coordination table (peers/messages/state/
|
|
244
|
+
# locks/events). Deferred: mesh tables are created at engine init
|
|
245
|
+
# (apply_v343_schema), same as M021.
|
|
246
|
+
Migration(name=_M023.NAME, db_target="memory", ddl=_M023.DDL),
|
|
247
|
+
# M025 adds hot-path perf indexes (atomic_facts dedup, mesh cleanup/list).
|
|
248
|
+
Migration(name=_M025.NAME, db_target="memory", ddl=_M025.DDL),
|
|
249
|
+
# M026 rebuilds rbac_memberships with a profiles FK (ON DELETE CASCADE) so
|
|
250
|
+
# deleting a profile cascade-purges its role grants (SEC-H-01 defense-in-
|
|
251
|
+
# depth). Deferred: the FK target `profiles` is created at engine init.
|
|
252
|
+
Migration(name=_M026.NAME, db_target="memory", ddl=_M026.DDL),
|
|
253
|
+
# M027 rebuilds transferable_patterns with profile_id + UNIQUE(profile_id,
|
|
254
|
+
# pattern_type, key) to prevent cross-profile preference contamination (H-01,
|
|
255
|
+
# cycle-3 audit). Deferred: CrossProjectAggregator creates the table on first
|
|
256
|
+
# consolidation run, not during engine init or apply_all. apply() is a no-op
|
|
257
|
+
# when the table is absent (first install after the schema change).
|
|
258
|
+
Migration(name=_M027.NAME, db_target="learning", ddl=_M027.DDL),
|
|
206
259
|
]
|
|
207
260
|
|
|
208
261
|
|