superlocalmemory 4.0.10 → 4.1.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/CHANGELOG.md +170 -0
- package/README.md +7 -7
- package/package.json +4 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +4 -4
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +2 -2
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +5 -5
- package/plugin/skills/slm-recall/SKILL.md +102 -15
- package/plugin/skills/slm-remember/SKILL.md +35 -3
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +29 -3
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +16 -8
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-governance/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-loop/SKILL.md +2 -2
- package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
- package/plugin-src/skills/slm-profile/SKILL.md +5 -5
- package/plugin-src/skills/slm-recall/SKILL.md +102 -15
- package/plugin-src/skills/slm-remember/SKILL.md +35 -3
- package/plugin-src/skills/slm-scope/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +29 -3
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +263 -18
- package/src/superlocalmemory/cli/daemon.py +30 -0
- package/src/superlocalmemory/cli/db_migrate.py +71 -1
- package/src/superlocalmemory/cli/gdpr_cmd.py +15 -2
- package/src/superlocalmemory/cli/main.py +24 -2
- package/src/superlocalmemory/code_graph/database.py +44 -0
- package/src/superlocalmemory/compliance/gdpr.py +449 -39
- package/src/superlocalmemory/core/admission.py +231 -11
- package/src/superlocalmemory/core/backend_orchestrator.py +190 -84
- package/src/superlocalmemory/core/config.py +90 -11
- package/src/superlocalmemory/core/consolidation_engine.py +34 -0
- package/src/superlocalmemory/core/engine.py +140 -11
- package/src/superlocalmemory/core/graph_analyzer.py +76 -112
- package/src/superlocalmemory/core/graph_metrics.py +597 -0
- package/src/superlocalmemory/core/graph_pruner.py +121 -0
- package/src/superlocalmemory/core/maintenance_scheduler.py +205 -0
- package/src/superlocalmemory/core/mode_capability.py +111 -0
- package/src/superlocalmemory/core/ollama_validator.py +315 -0
- package/src/superlocalmemory/core/projection_drain.py +380 -0
- package/src/superlocalmemory/core/recall_pipeline.py +390 -3
- package/src/superlocalmemory/core/recall_worker.py +6 -3
- package/src/superlocalmemory/core/scale_autopromote.py +196 -0
- package/src/superlocalmemory/core/scale_engine.py +16 -2
- package/src/superlocalmemory/core/score_contract.py +21 -1
- package/src/superlocalmemory/core/session_identity.py +85 -0
- package/src/superlocalmemory/core/status_contract.py +108 -0
- package/src/superlocalmemory/core/worker_pool.py +4 -4
- package/src/superlocalmemory/core/working_memory.py +288 -0
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +36 -6
- package/src/superlocalmemory/encoding/context_generator.py +1 -1
- package/src/superlocalmemory/encoding/entity_resolver.py +38 -0
- package/src/superlocalmemory/encoding/fact_extractor.py +18 -14
- package/src/superlocalmemory/encoding/prospective_markers.py +262 -0
- package/src/superlocalmemory/encoding/type_router.py +12 -12
- package/src/superlocalmemory/evolution/mutation_generator.py +30 -4
- package/src/superlocalmemory/graph/cozo_adjacency.py +122 -0
- package/src/superlocalmemory/graph/cozo_backend.py +103 -138
- package/src/superlocalmemory/hooks/portable_kit.py +10 -2
- package/src/superlocalmemory/learning/bandit.py +43 -0
- package/src/superlocalmemory/learning/consolidation_worker.py +54 -0
- package/src/superlocalmemory/learning/database.py +60 -3
- package/src/superlocalmemory/learning/entity_compiler.py +21 -58
- package/src/superlocalmemory/learning/feedback.py +3 -1
- package/src/superlocalmemory/learning/outcomes.py +47 -16
- package/src/superlocalmemory/learning/pattern_miner.py +28 -3
- package/src/superlocalmemory/learning/pattern_miner_constants.py +43 -0
- package/src/superlocalmemory/learning/pcos.py +291 -0
- package/src/superlocalmemory/learning/reward_from_outcomes.py +365 -0
- package/src/superlocalmemory/learning/reward_proxy.py +100 -10
- package/src/superlocalmemory/learning/signal_kinds.py +79 -0
- package/src/superlocalmemory/mcp/profiles.py +14 -2
- package/src/superlocalmemory/mcp/tools_active.py +2 -1
- package/src/superlocalmemory/mcp/tools_core.py +31 -3
- package/src/superlocalmemory/mcp/tools_v28.py +20 -1
- package/src/superlocalmemory/parameterization/pattern_extractor.py +14 -1
- package/src/superlocalmemory/parameterization/soft_prompt_generator.py +98 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +64 -3
- package/src/superlocalmemory/retrieval/channel_status.py +117 -0
- package/src/superlocalmemory/retrieval/engine.py +106 -11
- package/src/superlocalmemory/retrieval/entity_channel.py +210 -256
- package/src/superlocalmemory/retrieval/graph_adjacency.py +219 -0
- package/src/superlocalmemory/retrieval/scope_policy.py +20 -0
- package/src/superlocalmemory/retrieval/semantic_channel.py +47 -5
- package/src/superlocalmemory/retrieval/spreading.py +288 -0
- package/src/superlocalmemory/server/api.py +24 -5
- package/src/superlocalmemory/server/bandit_loops.py +17 -1
- package/src/superlocalmemory/server/rbac_enforce.py +26 -6
- package/src/superlocalmemory/server/recall_serializer.py +9 -0
- package/src/superlocalmemory/server/routes/behavioral.py +75 -10
- package/src/superlocalmemory/server/routes/compliance.py +98 -18
- package/src/superlocalmemory/server/routes/config_api.py +186 -4
- package/src/superlocalmemory/server/routes/evolution.py +178 -0
- package/src/superlocalmemory/server/routes/ingest.py +8 -0
- package/src/superlocalmemory/server/routes/learning_telemetry.py +2 -1
- package/src/superlocalmemory/server/routes/memories.py +49 -7
- package/src/superlocalmemory/server/routes/timeline.py +4 -0
- package/src/superlocalmemory/server/routes/v3_api.py +191 -15
- package/src/superlocalmemory/server/ui.py +20 -4
- package/src/superlocalmemory/server/unified_daemon.py +186 -5
- package/src/superlocalmemory/storage/_migration_internals.py +31 -0
- package/src/superlocalmemory/storage/_schema_version.py +24 -3
- package/src/superlocalmemory/storage/database.py +477 -59
- package/src/superlocalmemory/storage/embedding_codec.py +71 -0
- package/src/superlocalmemory/storage/lineage_retention.py +236 -0
- package/src/superlocalmemory/storage/logical_edges.py +43 -2
- package/src/superlocalmemory/storage/migration_runner.py +119 -0
- package/src/superlocalmemory/storage/migrations/M044_play_carries_its_own_evidence.py +127 -0
- package/src/superlocalmemory/storage/migrations/M045_fact_outcome_score.py +158 -0
- package/src/superlocalmemory/storage/migrations/M046_prospective_memory_has_its_own_name.py +620 -0
- package/src/superlocalmemory/storage/migrations/M047_fisher_vectors_are_stored_like_every_other_vector.py +306 -0
- package/src/superlocalmemory/storage/migrations/M048_upcoming_holds_only_what_is_upcoming.py +207 -0
- package/src/superlocalmemory/storage/migrations/M049_a_schema_version_marker_is_one_row.py +201 -0
- package/src/superlocalmemory/storage/migrations.py +18 -2
- package/src/superlocalmemory/storage/models.py +40 -1
- package/src/superlocalmemory/storage/projection_outbox.py +346 -0
- package/src/superlocalmemory/storage/retention_policy.py +860 -0
- package/src/superlocalmemory/storage/schema.py +12 -1
- package/src/superlocalmemory/storage/write_coordinator.py +19 -2
- package/src/superlocalmemory/trust/scorer.py +43 -1
- package/src/superlocalmemory/ui/index.html +9 -18
- package/src/superlocalmemory/ui/js/event-delegation.js +12 -1
- package/src/superlocalmemory/ui/js/od-health.js +28 -6
- package/src/superlocalmemory/ui/js/od-memories.js +19 -0
- package/src/superlocalmemory/ui/js/od-settings.js +87 -1
- package/src/superlocalmemory/ui/js/recall-lab.js +78 -3
|
@@ -1804,6 +1804,41 @@ def _stop_deployment_retention(application) -> bool:
|
|
|
1804
1804
|
return True
|
|
1805
1805
|
|
|
1806
1806
|
|
|
1807
|
+
def _start_embedder_warmup(engine: object) -> "threading.Thread | None":
|
|
1808
|
+
"""Load the embedding model in the background. Never blocks startup.
|
|
1809
|
+
|
|
1810
|
+
Returns the thread so a test can join it; ``None`` when there is nothing to
|
|
1811
|
+
warm. Every failure is "not warmed", never a failed startup: a daemon that
|
|
1812
|
+
cannot embed still serves keyword recall and still stores memories, and the
|
|
1813
|
+
materializer fills the vectors in afterwards either way.
|
|
1814
|
+
"""
|
|
1815
|
+
embedder = getattr(engine, "_embedder", None)
|
|
1816
|
+
if embedder is None or not hasattr(embedder, "embed"):
|
|
1817
|
+
return None
|
|
1818
|
+
|
|
1819
|
+
from superlocalmemory.core.engine import _is_remote_embedder
|
|
1820
|
+
|
|
1821
|
+
if _is_remote_embedder(embedder):
|
|
1822
|
+
# A hosted embedder has no model to load and warming it would spend a
|
|
1823
|
+
# request, and money, on a sentence nobody asked about.
|
|
1824
|
+
return None
|
|
1825
|
+
|
|
1826
|
+
def _warm() -> None:
|
|
1827
|
+
started = time.time()
|
|
1828
|
+
try:
|
|
1829
|
+
embedder.embed("slm embedder warm-up")
|
|
1830
|
+
except Exception as exc: # pragma: no cover — warming is best effort
|
|
1831
|
+
logger.debug("embedder warm-up failed (%s) — writes will defer", exc)
|
|
1832
|
+
return
|
|
1833
|
+
logger.info(
|
|
1834
|
+
"Embedding model warm and ready (%.1fs)", time.time() - started,
|
|
1835
|
+
)
|
|
1836
|
+
|
|
1837
|
+
thread = threading.Thread(target=_warm, daemon=True, name="slm-embed-warmup")
|
|
1838
|
+
thread.start()
|
|
1839
|
+
return thread
|
|
1840
|
+
|
|
1841
|
+
|
|
1807
1842
|
@asynccontextmanager
|
|
1808
1843
|
async def lifespan(application: FastAPI):
|
|
1809
1844
|
"""Initialize engine, workers, and optional services on startup."""
|
|
@@ -2088,6 +2123,40 @@ async def lifespan(application: FastAPI):
|
|
|
2088
2123
|
engine = MemoryEngine(config)
|
|
2089
2124
|
engine.initialize()
|
|
2090
2125
|
|
|
2126
|
+
# Load the embedding model now, off the request path, the way the
|
|
2127
|
+
# cross-encoder is already warmed at startup.
|
|
2128
|
+
#
|
|
2129
|
+
# A write embeds inline so the memory it stores can be found by asking a
|
|
2130
|
+
# question rather than only by quoting its own words, and it gives that
|
|
2131
|
+
# one second before deferring to the materializer. Loading the model
|
|
2132
|
+
# takes 9.9-11.0 s here; once loaded an embed is 42 ms. So on a daemon
|
|
2133
|
+
# that had not embedded yet, the first writes each waited the full second
|
|
2134
|
+
# and stored no vector regardless -- and the model only ever loaded
|
|
2135
|
+
# because some *recall* eventually paid for it. Whoever recalled first
|
|
2136
|
+
# wore the cold start.
|
|
2137
|
+
#
|
|
2138
|
+
# This belongs to the daemon and not to engine wiring: the worker is a
|
|
2139
|
+
# subprocess per engine, so warming from wiring would have every `slm
|
|
2140
|
+
# status` spawn one and load a model it will never use.
|
|
2141
|
+
_start_embedder_warmup(engine)
|
|
2142
|
+
|
|
2143
|
+
# Tell the hook subprocesses that skill evolution is on. The hook reads
|
|
2144
|
+
# this env var as its fast-path signal and nothing ever set it, so the
|
|
2145
|
+
# feature was off for everyone who had switched it on in config: the
|
|
2146
|
+
# hook checked, found nothing, and returned False. Hooks are launched as
|
|
2147
|
+
# children of this process and inherit its environment, which is the
|
|
2148
|
+
# only channel between the two.
|
|
2149
|
+
try:
|
|
2150
|
+
if getattr(getattr(config, "evolution", None), "enabled", False):
|
|
2151
|
+
os.environ["SLM_EVOLUTION_ENABLED"] = "1"
|
|
2152
|
+
else:
|
|
2153
|
+
# Cleared as well as set: a daemon restarted with the setting
|
|
2154
|
+
# turned off must not leave the previous run's answer behind in
|
|
2155
|
+
# an environment the next hook inherits.
|
|
2156
|
+
os.environ.pop("SLM_EVOLUTION_ENABLED", None)
|
|
2157
|
+
except Exception as _evo_exc: # pragma: no cover — never block startup
|
|
2158
|
+
logger.debug("evolution flag not exported: %s", _evo_exc)
|
|
2159
|
+
|
|
2091
2160
|
# Refresh migration state now that the engine is initialised. Any
|
|
2092
2161
|
# schema work the engine's own bootstrap may have applied (e.g.
|
|
2093
2162
|
# runtime-table creation) is captured here so the dashboard and
|
|
@@ -2175,6 +2244,30 @@ async def lifespan(application: FastAPI):
|
|
|
2175
2244
|
"deferred migration runner crashed (non-fatal): %s", _dexc,
|
|
2176
2245
|
)
|
|
2177
2246
|
|
|
2247
|
+
# Move an existing store onto the graph and vector backends on the
|
|
2248
|
+
# first start after an upgrade. They have shipped as required
|
|
2249
|
+
# dependencies since 3.7 and sat unused, because building the
|
|
2250
|
+
# projections was three manual commands almost nobody ran. Runs after
|
|
2251
|
+
# the deferred migrations so it projects the converted store, and never
|
|
2252
|
+
# fatal: if the libraries will not import or the projection does not
|
|
2253
|
+
# match, the daemon serves from SQLite and says so.
|
|
2254
|
+
try:
|
|
2255
|
+
from superlocalmemory.core.scale_autopromote import (
|
|
2256
|
+
auto_promote_scale_backends,
|
|
2257
|
+
)
|
|
2258
|
+
_promotion = auto_promote_scale_backends(config)
|
|
2259
|
+
application.state.scale_autopromotion = _promotion.as_dict()
|
|
2260
|
+
if _promotion.promoted and _promotion.restart_required:
|
|
2261
|
+
logger.info(
|
|
2262
|
+
"graph and vector backends are promoted and serve after the "
|
|
2263
|
+
"next restart",
|
|
2264
|
+
)
|
|
2265
|
+
except Exception as _pexc: # pragma: no cover — defensive
|
|
2266
|
+
logger.warning("automatic backend promotion crashed (non-fatal): %s", _pexc)
|
|
2267
|
+
application.state.scale_autopromotion = {
|
|
2268
|
+
"attempted": True, "promoted": False, "reason": str(_pexc),
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2178
2271
|
# S9-DASH-02: start the outcome-queue worker so recall →
|
|
2179
2272
|
# pending_outcomes is actually produced. Before v3.4.22 this
|
|
2180
2273
|
# producer had zero callers and the closed-loop pipeline was
|
|
@@ -3041,6 +3134,18 @@ async def lifespan(application: FastAPI):
|
|
|
3041
3134
|
await _cancel_fact_entity_association_repair(application)
|
|
3042
3135
|
await _cancel_source_quality_repair(application)
|
|
3043
3136
|
|
|
3137
|
+
# Stop the projection drain. Its queue is durable, so an interrupted pass
|
|
3138
|
+
# costs a repeat of idempotent work and nothing else — but a live worker
|
|
3139
|
+
# writing into RocksDB and Lance while the process tears down around it has
|
|
3140
|
+
# no upside.
|
|
3141
|
+
try:
|
|
3142
|
+
from superlocalmemory.core.backend_orchestrator import get_orchestrator
|
|
3143
|
+
_orch = get_orchestrator()
|
|
3144
|
+
if _orch is not None:
|
|
3145
|
+
_orch.stop()
|
|
3146
|
+
except Exception as exc: # pragma: no cover — defensive
|
|
3147
|
+
logger.warning("projection drain shutdown failed: %s", exc)
|
|
3148
|
+
|
|
3044
3149
|
# Cancel the cross-platform sync loop (H-CONC-2) so adapter file I/O does
|
|
3045
3150
|
# not outlive the daemon.
|
|
3046
3151
|
try:
|
|
@@ -4072,12 +4177,26 @@ def _register_dashboard_routes(application: FastAPI) -> None:
|
|
|
4072
4177
|
# 4.0.10: asset ?v= strings are now derived from file content instead of
|
|
4073
4178
|
# being hand-written literals that tracked nothing. See
|
|
4074
4179
|
# server/asset_versions.py — including what that does and does not fix.
|
|
4075
|
-
|
|
4180
|
+
# Asset versioning is cosmetic. It must never be why this page 500s.
|
|
4181
|
+
#
|
|
4182
|
+
# The import is deferred (house style, keeps startup lean), which means
|
|
4183
|
+
# it resolves at REQUEST time — so when `pip install -e .` replaced the
|
|
4184
|
+
# installed package underneath a running daemon, this route began
|
|
4185
|
+
# answering "Internal Server Error" on the dashboard while every other
|
|
4186
|
+
# endpoint was fine. A stale hand-written version string is a trifle; a
|
|
4187
|
+
# blank page is not. Fall back to the file as written.
|
|
4188
|
+
try:
|
|
4189
|
+
from superlocalmemory.server.asset_versions import render_index
|
|
4076
4190
|
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4191
|
+
return render_index(
|
|
4192
|
+
index_path, UI_DIR, substitutions={"__SLM_VERSION__": _SLM_VERSION},
|
|
4193
|
+
)
|
|
4194
|
+
except Exception as exc: # noqa: BLE001 — serve the page regardless
|
|
4195
|
+
logger.warning(
|
|
4196
|
+
"asset version rewrite unavailable, serving index.html as "
|
|
4197
|
+
"written: %s: %s", type(exc).__name__, exc,
|
|
4198
|
+
)
|
|
4199
|
+
return index_path.read_text().replace("__SLM_VERSION__", _SLM_VERSION)
|
|
4081
4200
|
|
|
4082
4201
|
@application.get("/favicon.ico", include_in_schema=False)
|
|
4083
4202
|
async def favicon():
|
|
@@ -4177,6 +4296,20 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
4177
4296
|
"embedding": embedding_ready,
|
|
4178
4297
|
"recall_health": recall_health.get("recall_healthy") is True,
|
|
4179
4298
|
"migration_failures": migration_failures,
|
|
4299
|
+
# WHY each one failed, not just which. The runner already produces
|
|
4300
|
+
# a precise sentence per migration -- "safe repair did not restore
|
|
4301
|
+
# M043_...", "schema verification failed ... : <sqlite error>" --
|
|
4302
|
+
# and this endpoint computed it and then dropped it on the floor.
|
|
4303
|
+
# A migration recorded ``complete`` in migration_log can still be
|
|
4304
|
+
# reported failed here, because a completed migration is re-checked
|
|
4305
|
+
# by its own verify() on every start; with only a name to go on,
|
|
4306
|
+
# that reads as the health check contradicting the database. It is
|
|
4307
|
+
# not: they are answering different questions, and this is the
|
|
4308
|
+
# sentence that says which. Reported as #125.
|
|
4309
|
+
"migration_failure_reasons": {
|
|
4310
|
+
name: str(migration_details.get(name, "(no detail recorded)"))
|
|
4311
|
+
for name in migration_failures
|
|
4312
|
+
},
|
|
4180
4313
|
}
|
|
4181
4314
|
readiness["retrieval"] = bool(
|
|
4182
4315
|
readiness["embedding"] and readiness["recall_health"]
|
|
@@ -4255,6 +4388,12 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
4255
4388
|
# reveal staleness on its own. Loopback-only, alongside the other
|
|
4256
4389
|
# operational metadata.
|
|
4257
4390
|
"version_integrity": _version_integrity_payload(),
|
|
4391
|
+
# How far behind the second graph store is. A drain that stops
|
|
4392
|
+
# advancing is the failure that does not announce itself: every
|
|
4393
|
+
# other signal here stays green while the graph quietly diverges
|
|
4394
|
+
# from the record, and the only symptom is answers that are subtly
|
|
4395
|
+
# worse. A depth that does not fall is the thing to alert on.
|
|
4396
|
+
"projection": _projection_health(),
|
|
4258
4397
|
}
|
|
4259
4398
|
|
|
4260
4399
|
@application.get("/recall")
|
|
@@ -4909,6 +5048,7 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
4909
5048
|
fact_count = 0
|
|
4910
5049
|
entity_count = 0
|
|
4911
5050
|
edge_count = 0
|
|
5051
|
+
projection_queue_depth = 0
|
|
4912
5052
|
if engine is not None:
|
|
4913
5053
|
try:
|
|
4914
5054
|
fact_count = engine._db.get_fact_count(profile_snapshot.profile_id)
|
|
@@ -4925,6 +5065,11 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
4925
5065
|
edge_count = int(dict(edges[0])["c"]) if edges else 0
|
|
4926
5066
|
except Exception:
|
|
4927
5067
|
logger.debug("daemon status count query failed", exc_info=True)
|
|
5068
|
+
try:
|
|
5069
|
+
from superlocalmemory.storage import projection_outbox
|
|
5070
|
+
projection_queue_depth = projection_outbox.depth(engine._db)
|
|
5071
|
+
except Exception:
|
|
5072
|
+
logger.debug("projection queue depth unavailable", exc_info=True)
|
|
4928
5073
|
db_path = getattr(config, "db_path", None)
|
|
4929
5074
|
db_size_mb = (
|
|
4930
5075
|
round(db_path.stat().st_size / 1024 / 1024, 2)
|
|
@@ -4950,6 +5095,10 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
4950
5095
|
"legacy_port": _LEGACY_PORT,
|
|
4951
5096
|
"profile": profile_snapshot.profile_id,
|
|
4952
5097
|
"profile_generation": profile_snapshot.generation,
|
|
5098
|
+
# Facts stored but not yet in the graph and vector projections. The
|
|
5099
|
+
# CLI and MCP read their own status from here, so this is where the
|
|
5100
|
+
# number has to be for all three surfaces to agree.
|
|
5101
|
+
"projection_queue_depth": projection_queue_depth,
|
|
4953
5102
|
# F2 fix: expose M028 backfill progress so operators can monitor
|
|
4954
5103
|
# the post-upgrade fact/entity association repair state.
|
|
4955
5104
|
"m028_backfill": getattr(
|
|
@@ -5561,6 +5710,38 @@ def _terminalize_orphan_operation(engine, operation_id: str) -> None:
|
|
|
5561
5710
|
)
|
|
5562
5711
|
|
|
5563
5712
|
|
|
5713
|
+
def _projection_health() -> dict:
|
|
5714
|
+
"""Queue depth, stall count, and whether the worker is running.
|
|
5715
|
+
|
|
5716
|
+
Never raises: a health endpoint that fails because one of its fields could
|
|
5717
|
+
not be computed is worse than the missing field.
|
|
5718
|
+
|
|
5719
|
+
Takes no application. It used to accept one and never read it -- the
|
|
5720
|
+
orchestrator is a process singleton -- which made the signature claim a
|
|
5721
|
+
dependency the body did not have, and made a test that handed it a broken
|
|
5722
|
+
application look like it was exercising the failure path when it was only
|
|
5723
|
+
observing whatever the process had already built. Compare
|
|
5724
|
+
``_ops_failure_counts`` directly below, which takes an application because it
|
|
5725
|
+
genuinely reads one.
|
|
5726
|
+
"""
|
|
5727
|
+
try:
|
|
5728
|
+
from superlocalmemory.core.backend_orchestrator import get_orchestrator
|
|
5729
|
+
|
|
5730
|
+
orchestrator = get_orchestrator()
|
|
5731
|
+
if orchestrator is None:
|
|
5732
|
+
return {"available": False}
|
|
5733
|
+
health = dict(orchestrator.outbox_health())
|
|
5734
|
+
health["available"] = True
|
|
5735
|
+
# One boolean an alert can key on without knowing what a healthy depth
|
|
5736
|
+
# looks like on this store.
|
|
5737
|
+
health["behind"] = bool(health.get("depth", 0)) or bool(
|
|
5738
|
+
health.get("stalled", 0)
|
|
5739
|
+
)
|
|
5740
|
+
return health
|
|
5741
|
+
except Exception as exc: # noqa: BLE001
|
|
5742
|
+
return {"available": False, "error": str(exc)[:120]}
|
|
5743
|
+
|
|
5744
|
+
|
|
5564
5745
|
def _ops_failure_counts(engine, application) -> dict:
|
|
5565
5746
|
"""Return operational failure counts for /status and /health.
|
|
5566
5747
|
|
|
@@ -156,6 +156,16 @@ from superlocalmemory.storage.migrations import (
|
|
|
156
156
|
from superlocalmemory.storage.migrations import (
|
|
157
157
|
M043_quarantine_display_summaries as _M043,
|
|
158
158
|
)
|
|
159
|
+
from superlocalmemory.storage.migrations import (
|
|
160
|
+
M044_play_carries_its_own_evidence as _M044,
|
|
161
|
+
)
|
|
162
|
+
from superlocalmemory.storage.migrations import (
|
|
163
|
+
M045_fact_outcome_score as _M045,
|
|
164
|
+
M046_prospective_memory_has_its_own_name as _M046,
|
|
165
|
+
M047_fisher_vectors_are_stored_like_every_other_vector as _M047,
|
|
166
|
+
M048_upcoming_holds_only_what_is_upcoming as _M048,
|
|
167
|
+
M049_a_schema_version_marker_is_one_row as _M049,
|
|
168
|
+
)
|
|
159
169
|
|
|
160
170
|
# Emit under the runner's logger name so operational log filters that key on
|
|
161
171
|
# "superlocalmemory.storage.migration_runner" keep matching after this split.
|
|
@@ -207,6 +217,12 @@ _MODULES = {
|
|
|
207
217
|
_M041.NAME: _M041,
|
|
208
218
|
_M042.NAME: _M042,
|
|
209
219
|
_M043.NAME: _M043,
|
|
220
|
+
_M044.NAME: _M044,
|
|
221
|
+
_M045.NAME: _M045,
|
|
222
|
+
_M046.NAME: _M046,
|
|
223
|
+
_M047.NAME: _M047,
|
|
224
|
+
_M048.NAME: _M048,
|
|
225
|
+
_M049.NAME: _M049,
|
|
210
226
|
}
|
|
211
227
|
|
|
212
228
|
# Exact historical DDL fingerprints whose resulting schema is intentionally
|
|
@@ -245,9 +261,24 @@ def _ddl_hash(ddl: str) -> str:
|
|
|
245
261
|
return hashlib.sha256(ddl.encode("utf-8")).hexdigest()
|
|
246
262
|
|
|
247
263
|
|
|
264
|
+
#: How long a migration waits for a database another process is holding.
|
|
265
|
+
#:
|
|
266
|
+
#: Without this, SQLite raises SQLITE_BUSY the instant a write lock is taken,
|
|
267
|
+
#: and a migration that runs while the daemon happens to be writing is recorded
|
|
268
|
+
#: as failed rather than retried. A migration that rebuilds a table takes an
|
|
269
|
+
#: exclusive lock, so it is exactly the one most likely to collide — and its
|
|
270
|
+
#: failure leaves an upgrade wedged until someone notices.
|
|
271
|
+
#:
|
|
272
|
+
#: Fifteen seconds is longer than any single write this codebase performs and
|
|
273
|
+
#: short enough that a genuinely stuck lock still surfaces as a failure rather
|
|
274
|
+
#: than a hang.
|
|
275
|
+
_MIGRATION_BUSY_TIMEOUT_MS = 15_000
|
|
276
|
+
|
|
277
|
+
|
|
248
278
|
def _connect(db_path: Path) -> sqlite3.Connection:
|
|
249
279
|
# isolation_level=None → we manage transactions explicitly via DDL.
|
|
250
280
|
conn = sqlite3.connect(db_path, isolation_level=None)
|
|
281
|
+
conn.execute(f"PRAGMA busy_timeout = {_MIGRATION_BUSY_TIMEOUT_MS};")
|
|
251
282
|
conn.execute("PRAGMA foreign_keys = OFF;")
|
|
252
283
|
return conn
|
|
253
284
|
|
|
@@ -20,10 +20,31 @@ from __future__ import annotations
|
|
|
20
20
|
import sqlite3
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
|
|
23
|
-
#: Highest schema_version this runner can write. Matches the trailing serial
|
|
24
|
-
#:
|
|
23
|
+
#: Highest schema_version this runner can write. Matches the trailing serial of
|
|
24
|
+
#: the latest migration (M049). Increment when adding new migrations or
|
|
25
25
|
#: table-level breaking changes.
|
|
26
|
-
|
|
26
|
+
#:
|
|
27
|
+
#: This sat at 42 while M043, M044 and M045 shipped, so for three migrations the
|
|
28
|
+
#: ceiling did not move and nothing prevented an older build from opening a
|
|
29
|
+
#: newer store. That was tolerable only because those three were additive: a
|
|
30
|
+
#: build that did not know about a new column or table simply never read it.
|
|
31
|
+
#:
|
|
32
|
+
#: M046 is not additive. It rebuilds ``atomic_facts`` with a constraint that
|
|
33
|
+
#: rejects the value an older build classifies planned events as, so an older
|
|
34
|
+
#: writer against a migrated store fails its INSERT. The ceiling is what turns
|
|
35
|
+
#: that from a lost memory into a refusal to start, which is why it moves here
|
|
36
|
+
#: and why it moves to the trailing serial rather than to 43.
|
|
37
|
+
#:
|
|
38
|
+
#: M049 is additive — a unique index on ``schema_version`` plus the removal of
|
|
39
|
+
#: the duplicate rows that index could not otherwise be created over — so an
|
|
40
|
+
#: older build could in principle read a store it has touched. The ceiling
|
|
41
|
+
#: still moves, because the convention is "trailing serial, always": the cost of
|
|
42
|
+
#: moving it for an additive migration is an older build declining a store it
|
|
43
|
+
#: could have read, and the cost of NOT moving it for one that turns out not to
|
|
44
|
+
#: be additive is a silent bad write. Those are not comparable, and judging
|
|
45
|
+
#: additivity per migration is exactly the judgement that let it fall three
|
|
46
|
+
#: behind.
|
|
47
|
+
SUPPORTED_SCHEMA_VERSION: int = 49
|
|
27
48
|
|
|
28
49
|
|
|
29
50
|
class SchemaVersionError(RuntimeError):
|