superlocalmemory 3.8.13 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ATTRIBUTION.md +4 -4
- package/CHANGELOG.md +113 -121
- package/README.md +65 -63
- package/docs/pi-dev-integration.md +1 -1
- package/package.json +6 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-loop/SKILL.md +99 -0
- package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
- package/plugin-src/skills/slm-profile/SKILL.md +148 -0
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +11 -4
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +125 -11
- package/src/superlocalmemory/cli/daemon.py +5 -1
- package/src/superlocalmemory/cli/main.py +35 -2
- package/src/superlocalmemory/cli/ops_cmd.py +281 -0
- package/src/superlocalmemory/cli/setup_wizard.py +1 -1
- package/src/superlocalmemory/compliance/audit.py +65 -0
- package/src/superlocalmemory/compliance/eu_ai_act.py +27 -57
- package/src/superlocalmemory/compliance/gdpr.py +416 -20
- package/src/superlocalmemory/compliance/retention.py +74 -22
- package/src/superlocalmemory/compliance/scheduler.py +78 -9
- package/src/superlocalmemory/core/actor_context.py +166 -0
- package/src/superlocalmemory/core/admission.py +549 -0
- package/src/superlocalmemory/core/backend_orchestrator.py +23 -10
- package/src/superlocalmemory/core/config.py +202 -24
- package/src/superlocalmemory/core/consolidation_engine.py +13 -13
- package/src/superlocalmemory/core/context_cache.py +28 -0
- package/src/superlocalmemory/core/embeddings.py +64 -2
- package/src/superlocalmemory/core/engine.py +7 -2
- package/src/superlocalmemory/core/engine_ingestion.py +65 -3
- package/src/superlocalmemory/core/engine_wiring.py +36 -9
- package/src/superlocalmemory/core/ingest_policy.py +38 -0
- package/src/superlocalmemory/core/maintenance.py +255 -0
- package/src/superlocalmemory/core/modes.py +40 -13
- package/src/superlocalmemory/core/mutations.py +437 -44
- package/src/superlocalmemory/core/operation_policy.py +92 -0
- package/src/superlocalmemory/core/operation_policy_registry.py +542 -0
- package/src/superlocalmemory/core/operation_request.py +127 -0
- package/src/superlocalmemory/core/ops_remediation.py +542 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -0
- package/src/superlocalmemory/core/remember_runtime.py +202 -4
- package/src/superlocalmemory/core/remote_mode.py +20 -5
- package/src/superlocalmemory/core/store_pipeline.py +150 -0
- package/src/superlocalmemory/core/topic_signature.py +19 -4
- package/src/superlocalmemory/core/transactions/__init__.py +78 -0
- package/src/superlocalmemory/core/transactions/concrete_owners.py +597 -0
- package/src/superlocalmemory/core/transactions/erasure.py +825 -0
- package/src/superlocalmemory/core/transactions/manifest.py +255 -0
- package/src/superlocalmemory/core/transactions/manifest_key.py +155 -0
- package/src/superlocalmemory/core/transactions/obligations.py +272 -0
- package/src/superlocalmemory/core/transactions/owners.py +114 -0
- package/src/superlocalmemory/core/transactions/reconciler.py +285 -0
- package/src/superlocalmemory/core/transactions/service.py +330 -0
- package/src/superlocalmemory/core/worker_pool.py +33 -5
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +70 -28
- package/src/superlocalmemory/encoding/emotional.py +75 -14
- package/src/superlocalmemory/encoding/scene_builder.py +115 -13
- package/src/superlocalmemory/encoding/temporal_parser.py +4 -0
- package/src/superlocalmemory/evolution/blind_verifier.py +11 -4
- package/src/superlocalmemory/evolution/evolution_store.py +244 -4
- package/src/superlocalmemory/evolution/llm_dispatch.py +40 -0
- package/src/superlocalmemory/evolution/model_selection.py +18 -3
- package/src/superlocalmemory/evolution/mutation_generator.py +3 -0
- package/src/superlocalmemory/evolution/skill_activator.py +270 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +281 -59
- package/src/superlocalmemory/evolution/types.py +30 -8
- package/src/superlocalmemory/graph/cozo_backend.py +17 -9
- package/src/superlocalmemory/hooks/auto_invoker.py +2 -1
- package/src/superlocalmemory/hooks/auto_recall.py +64 -30
- package/src/superlocalmemory/hooks/codex_assets.py +14 -1
- package/src/superlocalmemory/infra/backup.py +434 -7
- package/src/superlocalmemory/infra/process_reaper.py +18 -0
- package/src/superlocalmemory/infra/self_heal.py +401 -0
- package/src/superlocalmemory/learning/feedback.py +52 -9
- package/src/superlocalmemory/loops/engine.py +10 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/http_transport.py +30 -331
- package/src/superlocalmemory/mcp/profiles.py +5 -0
- package/src/superlocalmemory/mcp/resources.py +8 -0
- package/src/superlocalmemory/mcp/server.py +51 -4
- package/src/superlocalmemory/mcp/shared.py +19 -0
- package/src/superlocalmemory/mcp/tools_active.py +25 -4
- package/src/superlocalmemory/mcp/tools_code_graph.py +26 -18
- package/src/superlocalmemory/mcp/tools_context.py +50 -8
- package/src/superlocalmemory/mcp/tools_core.py +69 -21
- package/src/superlocalmemory/mcp/tools_evolution.py +9 -2
- package/src/superlocalmemory/mcp/tools_learning.py +21 -10
- package/src/superlocalmemory/mcp/tools_loops.py +29 -18
- package/src/superlocalmemory/mcp/tools_mesh.py +8 -0
- package/src/superlocalmemory/mcp/tools_ops.py +115 -0
- package/src/superlocalmemory/mcp/tools_optimize.py +4 -0
- package/src/superlocalmemory/mcp/tools_v28.py +10 -3
- package/src/superlocalmemory/mcp/tools_v3.py +34 -14
- package/src/superlocalmemory/mcp/tools_v33.py +18 -33
- package/src/superlocalmemory/mesh/broker.py +124 -46
- package/src/superlocalmemory/mesh/broker_security.py +470 -0
- package/src/superlocalmemory/mesh/discovery.py +365 -0
- package/src/superlocalmemory/mesh/lock_protocol.py +313 -0
- package/src/superlocalmemory/mesh/node_identity.py +97 -0
- package/src/superlocalmemory/mesh/outbox_remote.py +429 -0
- package/src/superlocalmemory/mesh/remote_sync.py +511 -28
- package/src/superlocalmemory/mesh/state_sync.py +286 -0
- package/src/superlocalmemory/optimize/config/store.py +45 -0
- package/src/superlocalmemory/parameterization/cross_project.py +12 -0
- package/src/superlocalmemory/parameterization/prompt_injector.py +13 -11
- package/src/superlocalmemory/parameterization/prompt_lifecycle.py +8 -2
- package/src/superlocalmemory/parameterization/workflow_miner.py +17 -0
- package/src/superlocalmemory/retrieval/ann_index.py +5 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +49 -2
- package/src/superlocalmemory/retrieval/engine.py +19 -4
- package/src/superlocalmemory/retrieval/fusion.py +4 -1
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -3
- package/src/superlocalmemory/retrieval/remote_reranker.py +47 -22
- package/src/superlocalmemory/retrieval/reranker.py +32 -1
- package/src/superlocalmemory/retrieval/temporal_channel.py +16 -3
- package/src/superlocalmemory/retrieval/temporal_utils.py +107 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +155 -42
- package/src/superlocalmemory/retrieval/vector_store.py +214 -8
- package/src/superlocalmemory/server/api.py +5 -5
- package/src/superlocalmemory/server/egress_policy.py +258 -0
- package/src/superlocalmemory/server/rbac_enforce.py +32 -0
- package/src/superlocalmemory/server/route_mutations.py +20 -0
- package/src/superlocalmemory/server/routes/compliance.py +153 -7
- package/src/superlocalmemory/server/routes/data_io.py +43 -2
- package/src/superlocalmemory/server/routes/events.py +15 -0
- package/src/superlocalmemory/server/routes/memories.py +56 -3
- package/src/superlocalmemory/server/routes/mesh.py +82 -1
- package/src/superlocalmemory/server/routes/mesh_lock.py +54 -0
- package/src/superlocalmemory/server/routes/mesh_state.py +63 -0
- package/src/superlocalmemory/server/routes/v3_api.py +50 -27
- package/src/superlocalmemory/server/routes/ws.py +86 -0
- package/src/superlocalmemory/server/ui.py +6 -6
- package/src/superlocalmemory/server/unified_daemon.py +942 -119
- package/src/superlocalmemory/storage/_migration_internals.py +568 -0
- package/src/superlocalmemory/storage/_schema_version.py +110 -0
- package/src/superlocalmemory/storage/database.py +329 -24
- package/src/superlocalmemory/storage/embedding_migrator.py +246 -51
- package/src/superlocalmemory/storage/erasure_fence.py +45 -0
- package/src/superlocalmemory/storage/generation_fence.py +63 -0
- package/src/superlocalmemory/storage/migration_runner.py +140 -417
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +40 -0
- package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +148 -0
- package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +58 -0
- package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +113 -0
- package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +107 -0
- package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +162 -0
- package/src/superlocalmemory/storage/migrations/{M033_learning_feedback_channel.py → M038_learning_feedback_channel.py} +3 -3
- package/src/superlocalmemory/storage/migrations/M039_scene_fact_members.py +137 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -2
- package/src/superlocalmemory/storage/schema.py +67 -0
- package/src/superlocalmemory/storage/write_coordinator.py +125 -0
- package/src/superlocalmemory/trust/scorer.py +28 -4
- package/src/superlocalmemory/ui/index.html +14 -3
- package/src/superlocalmemory/ui/js/auto-settings.js +12 -1
- package/src/superlocalmemory/ui/js/brain.js +6 -4
- package/src/superlocalmemory/ui/js/compliance.js +66 -12
- package/src/superlocalmemory/ui/js/dashboard.js +13 -3
- package/src/superlocalmemory/ui/js/feedback.js +8 -2
- package/src/superlocalmemory/ui/js/lifecycle.js +7 -1
- package/src/superlocalmemory/ui/js/modal.js +272 -5
- package/src/superlocalmemory/ui/js/od-backup.js +9 -2
- package/src/superlocalmemory/ui/js/od-compliance-ext.js +301 -0
- package/src/superlocalmemory/ui/js/od-operations.js +154 -23
- package/src/superlocalmemory/ui/js/od-ops-health.js +417 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +35 -21
- package/src/superlocalmemory/ui/js/od-team.js +9 -2
- package/src/superlocalmemory/ui/js/optimize.js +13 -16
- package/src/superlocalmemory/ui/js/profiles.js +7 -3
- package/src/superlocalmemory/ui/js/settings.js +7 -1
- package/src/superlocalmemory/vector/lancedb_backend.py +19 -9
- package/src/superlocalmemory/attribution/mathematical_dna.py +0 -235
- package/src/superlocalmemory/cli/post_install.py +0 -114
- package/src/superlocalmemory/core/clock_monitor.py +0 -45
- package/src/superlocalmemory/core/db_pool.py +0 -80
- package/src/superlocalmemory/core/error_catalog.py +0 -113
- package/src/superlocalmemory/core/loop_watchdog.py +0 -56
- package/src/superlocalmemory/core/priority_queue.py +0 -61
- package/src/superlocalmemory/core/pruning_engine.py +0 -216
- package/src/superlocalmemory/core/queue_dispatcher.py +0 -73
- package/src/superlocalmemory/core/slmignore.py +0 -125
- package/src/superlocalmemory/infra/heartbeat_monitor.py +0 -140
- package/src/superlocalmemory/infra/webhook_dispatcher.py +0 -247
- package/src/superlocalmemory/learning/quantization_scheduler.py +0 -320
- package/src/superlocalmemory/storage/access_control.py +0 -182
|
@@ -283,8 +283,9 @@ class RetrievalConfig:
|
|
|
283
283
|
# It is now read, validated, and — when it disagrees with the backend —
|
|
284
284
|
# reported as a loud configuration error instead of nothing at all.
|
|
285
285
|
cross_encoder_endpoint: str = ""
|
|
286
|
-
# Optional bearer token.
|
|
287
|
-
#
|
|
286
|
+
# Optional bearer token. SLM_CROSS_ENCODER_API_KEY takes precedence.
|
|
287
|
+
# When persisted for backward compatibility, config.json is atomically
|
|
288
|
+
# forced to owner-only mode 0600 by save().
|
|
288
289
|
cross_encoder_api_key: str = ""
|
|
289
290
|
# Per-request read budget for the remote reranker. Recall is interactive,
|
|
290
291
|
# so this stays tight: a slow reranker degrades to fusion scores rather
|
|
@@ -386,6 +387,12 @@ class MathConfig:
|
|
|
386
387
|
|
|
387
388
|
# Hopfield
|
|
388
389
|
|
|
390
|
+
# Ebbinghaus-Langevin coupling in maintenance (Phase 5 — P1-ELC)
|
|
391
|
+
# When True, the maintenance cycle computes the combined Ebbinghaus-Langevin
|
|
392
|
+
# coupled state for each fact and writes back the lifecycle zone.
|
|
393
|
+
# Defaults to False to preserve existing maintenance behaviour.
|
|
394
|
+
ebbinghaus_langevin_coupling_enabled: bool = False
|
|
395
|
+
|
|
389
396
|
# Sheaf (at encoding time, NOT retrieval)
|
|
390
397
|
sheaf_at_encoding: bool = True
|
|
391
398
|
sheaf_contradiction_threshold: float = 0.45
|
|
@@ -744,6 +751,11 @@ class TemporalValidatorConfig:
|
|
|
744
751
|
# Include expired facts in historical queries
|
|
745
752
|
include_expired_in_history: bool = True
|
|
746
753
|
|
|
754
|
+
# Event-time (valid_until) demotion factor, applied like
|
|
755
|
+
# superseded_demotion_factor but for facts whose stated validity window has
|
|
756
|
+
# elapsed. Default matches the prior hardcoded constant (0.5).
|
|
757
|
+
event_time_demotion_factor: float = 0.5
|
|
758
|
+
|
|
747
759
|
|
|
748
760
|
@dataclass(frozen=True)
|
|
749
761
|
class EvolutionConfig:
|
|
@@ -908,7 +920,7 @@ DEPLOYMENT_ENTERPRISE = DeploymentConfig(
|
|
|
908
920
|
def load_deployment_config(
|
|
909
921
|
config_toml_path: Path | None = None,
|
|
910
922
|
) -> DeploymentConfig:
|
|
911
|
-
"""Parse [deployment] from config.toml
|
|
923
|
+
"""Parse [deployment] from config.toml with fail-closed invalid-state handling.
|
|
912
924
|
|
|
913
925
|
config.toml is the installer-written performance config (separate from the
|
|
914
926
|
daemon's config.json managed by SLMConfig). This function reads ONLY the
|
|
@@ -919,8 +931,9 @@ def load_deployment_config(
|
|
|
919
931
|
``~/.superlocalmemory/config.toml`` via the canonical data root.
|
|
920
932
|
|
|
921
933
|
Returns:
|
|
922
|
-
DeploymentConfig — Personal
|
|
923
|
-
|
|
934
|
+
DeploymentConfig — Personal only when the file or deployment section is
|
|
935
|
+
absent. A present but invalid deployment configuration resolves to the
|
|
936
|
+
enterprise preset so startup controls cannot silently downgrade.
|
|
924
937
|
"""
|
|
925
938
|
if config_toml_path is None:
|
|
926
939
|
try:
|
|
@@ -939,40 +952,55 @@ def load_deployment_config(
|
|
|
939
952
|
logger.warning(
|
|
940
953
|
"load_deployment_config: failed to parse %s: %s", config_toml_path, exc
|
|
941
954
|
)
|
|
942
|
-
return
|
|
955
|
+
return DEPLOYMENT_ENTERPRISE
|
|
943
956
|
|
|
944
|
-
|
|
945
|
-
if not dep:
|
|
957
|
+
if "deployment" not in data:
|
|
946
958
|
# No [deployment] section — personal defaults, no behaviour change.
|
|
947
959
|
return DEPLOYMENT_PERSONAL
|
|
960
|
+
dep = data["deployment"]
|
|
961
|
+
if not isinstance(dep, dict):
|
|
962
|
+
logger.warning(
|
|
963
|
+
"load_deployment_config: [deployment] in %s is not a table — "
|
|
964
|
+
"failing closed to enterprise",
|
|
965
|
+
config_toml_path,
|
|
966
|
+
)
|
|
967
|
+
return DEPLOYMENT_ENTERPRISE
|
|
948
968
|
|
|
949
|
-
raw_mode = str(dep.get("mode", "
|
|
969
|
+
raw_mode = str(dep.get("mode", "")).lower()
|
|
950
970
|
if raw_mode not in _VALID_DEPLOYMENT_MODES:
|
|
951
971
|
logger.warning(
|
|
952
|
-
"load_deployment_config: unknown mode %r in %s —
|
|
972
|
+
"load_deployment_config: unknown mode %r in %s — failing closed to enterprise",
|
|
953
973
|
raw_mode, config_toml_path,
|
|
954
974
|
)
|
|
955
|
-
|
|
975
|
+
return DEPLOYMENT_ENTERPRISE
|
|
956
976
|
|
|
957
977
|
# Use the preset for the mode as the base so omitted keys inherit
|
|
958
978
|
# sensible values (enterprise → require_login=True etc.).
|
|
959
979
|
base = DEPLOYMENT_ENTERPRISE if raw_mode == "enterprise" else DEPLOYMENT_PERSONAL
|
|
960
980
|
|
|
981
|
+
def _strict_bool(name: str, default: bool) -> bool:
|
|
982
|
+
value = dep.get(name, default)
|
|
983
|
+
if not isinstance(value, bool):
|
|
984
|
+
raise TypeError(f"{name} must be a TOML boolean")
|
|
985
|
+
return value
|
|
986
|
+
|
|
961
987
|
try:
|
|
962
988
|
return DeploymentConfig(
|
|
963
989
|
mode=raw_mode,
|
|
964
|
-
require_login=
|
|
965
|
-
pii_redaction=
|
|
966
|
-
retention_enabled=
|
|
967
|
-
|
|
990
|
+
require_login=_strict_bool("require_login", base.require_login),
|
|
991
|
+
pii_redaction=_strict_bool("pii_redaction", base.pii_redaction),
|
|
992
|
+
retention_enabled=_strict_bool(
|
|
993
|
+
"retention_enabled", base.retention_enabled
|
|
994
|
+
),
|
|
995
|
+
audit=_strict_bool("audit", base.audit),
|
|
968
996
|
)
|
|
969
997
|
except (ValueError, TypeError) as exc:
|
|
970
998
|
logger.warning(
|
|
971
999
|
"load_deployment_config: invalid values in [deployment] in %s: %s — "
|
|
972
|
-
"
|
|
1000
|
+
"failing closed to enterprise",
|
|
973
1001
|
config_toml_path, exc,
|
|
974
1002
|
)
|
|
975
|
-
return
|
|
1003
|
+
return DEPLOYMENT_ENTERPRISE
|
|
976
1004
|
|
|
977
1005
|
|
|
978
1006
|
# ---------------------------------------------------------------------------
|
|
@@ -1085,6 +1113,13 @@ class SLMConfig:
|
|
|
1085
1113
|
daemon_legacy_port: int = 8767 # Backward-compat redirect port
|
|
1086
1114
|
daemon_enable_legacy_port: bool = True # Set False to disable 8767 redirect
|
|
1087
1115
|
|
|
1116
|
+
# v4: auto-close orphaned application sessions during maintenance.
|
|
1117
|
+
# A session is stale when it has had no new atomic_facts for this many
|
|
1118
|
+
# hours. close_session is only invoked by the MCP tool otherwise, so
|
|
1119
|
+
# without this pass temporal summaries never form for abandoned sessions.
|
|
1120
|
+
session_idle_close_hours: float = 24.0
|
|
1121
|
+
session_idle_close_max_per_pass: int = 50
|
|
1122
|
+
|
|
1088
1123
|
# v3.4.3: Entity compilation
|
|
1089
1124
|
entity_compilation_enabled: bool = True
|
|
1090
1125
|
entity_compilation_retrieval_boost: float = 1.0 # 1.0 = disabled. >1.0 = boost score.
|
|
@@ -1092,6 +1127,11 @@ class SLMConfig:
|
|
|
1092
1127
|
# v3.4.3: Mesh
|
|
1093
1128
|
mesh_enabled: bool = True
|
|
1094
1129
|
|
|
1130
|
+
# Deployment policy overlay. Personal installs remain unchanged; the
|
|
1131
|
+
# daemon upgrades this to True before engine initialization when the
|
|
1132
|
+
# enterprise deployment preset requests PII redaction.
|
|
1133
|
+
pii_redaction: bool = False
|
|
1134
|
+
|
|
1095
1135
|
def __post_init__(self) -> None:
|
|
1096
1136
|
if self.db_path is None:
|
|
1097
1137
|
self.db_path = self.base_dir / DEFAULT_DB_NAME
|
|
@@ -1208,6 +1248,65 @@ class SLMConfig:
|
|
|
1208
1248
|
if k in ForgettingConfig.__dataclass_fields__
|
|
1209
1249
|
})
|
|
1210
1250
|
|
|
1251
|
+
# quantization: nested PolarQuantConfig + QJLConfig are reconstructed
|
|
1252
|
+
# from their serialized dicts; scalar fields are passed through directly.
|
|
1253
|
+
# Unknown subkeys are filtered out for forward-compat safety.
|
|
1254
|
+
qt_raw = data.get("quantization", {})
|
|
1255
|
+
if qt_raw:
|
|
1256
|
+
try:
|
|
1257
|
+
polar_raw = qt_raw.get("polar", {})
|
|
1258
|
+
qjl_raw = qt_raw.get("qjl", {})
|
|
1259
|
+
polar = PolarQuantConfig(**{
|
|
1260
|
+
k: v for k, v in polar_raw.items()
|
|
1261
|
+
if k in PolarQuantConfig.__dataclass_fields__
|
|
1262
|
+
})
|
|
1263
|
+
qjl = QJLConfig(**{
|
|
1264
|
+
k: v for k, v in qjl_raw.items()
|
|
1265
|
+
if k in QJLConfig.__dataclass_fields__
|
|
1266
|
+
})
|
|
1267
|
+
qt_scalars = {
|
|
1268
|
+
k: v for k, v in qt_raw.items()
|
|
1269
|
+
if k in QuantizationConfig.__dataclass_fields__
|
|
1270
|
+
and k not in ("polar", "qjl")
|
|
1271
|
+
}
|
|
1272
|
+
config.quantization = QuantizationConfig(polar=polar, qjl=qjl, **qt_scalars)
|
|
1273
|
+
except (TypeError, ValueError) as exc:
|
|
1274
|
+
logger.warning(
|
|
1275
|
+
"Ignoring invalid quantization config (%s) — using defaults", exc
|
|
1276
|
+
)
|
|
1277
|
+
|
|
1278
|
+
# sagq: valid_bit_widths is serialized as a list by dataclasses.asdict()
|
|
1279
|
+
# and must be coerced back to a tuple to satisfy the field annotation and
|
|
1280
|
+
# preserve identity through a round-trip.
|
|
1281
|
+
sq_raw = data.get("sagq", {})
|
|
1282
|
+
if sq_raw:
|
|
1283
|
+
try:
|
|
1284
|
+
sq_kwargs = {
|
|
1285
|
+
k: v for k, v in sq_raw.items()
|
|
1286
|
+
if k in SAGQConfig.__dataclass_fields__
|
|
1287
|
+
}
|
|
1288
|
+
if "valid_bit_widths" in sq_kwargs:
|
|
1289
|
+
sq_kwargs["valid_bit_widths"] = tuple(sq_kwargs["valid_bit_widths"])
|
|
1290
|
+
config.sagq = SAGQConfig(**sq_kwargs)
|
|
1291
|
+
except (TypeError, ValueError) as exc:
|
|
1292
|
+
logger.warning(
|
|
1293
|
+
"Ignoring invalid sagq config (%s) — using defaults", exc
|
|
1294
|
+
)
|
|
1295
|
+
|
|
1296
|
+
# auto_invoke: dict fields (weights, act_r_weights, mode_a_weights) are
|
|
1297
|
+
# preserved as-is by asdict(); no special reconstruction required.
|
|
1298
|
+
ai_raw = data.get("auto_invoke", {})
|
|
1299
|
+
if ai_raw:
|
|
1300
|
+
try:
|
|
1301
|
+
config.auto_invoke = AutoInvokeConfig(**{
|
|
1302
|
+
k: v for k, v in ai_raw.items()
|
|
1303
|
+
if k in AutoInvokeConfig.__dataclass_fields__
|
|
1304
|
+
})
|
|
1305
|
+
except (TypeError, ValueError) as exc:
|
|
1306
|
+
logger.warning(
|
|
1307
|
+
"Ignoring invalid auto_invoke config (%s) — using defaults", exc
|
|
1308
|
+
)
|
|
1309
|
+
|
|
1211
1310
|
rt = data.get("retrieval", {})
|
|
1212
1311
|
if rt:
|
|
1213
1312
|
# V3.3.2 migration: add ONNX cross-encoder backend field.
|
|
@@ -1227,6 +1326,12 @@ class SLMConfig:
|
|
|
1227
1326
|
# V3.4.3 config fields (additive — missing keys get dataclass defaults)
|
|
1228
1327
|
config.daemon_idle_timeout = data.get("daemon_idle_timeout", 0)
|
|
1229
1328
|
config.daemon_port = data.get("daemon_port", 8765)
|
|
1329
|
+
config.session_idle_close_hours = float(
|
|
1330
|
+
data.get("session_idle_close_hours", 24.0) or 24.0
|
|
1331
|
+
)
|
|
1332
|
+
config.session_idle_close_max_per_pass = int(
|
|
1333
|
+
data.get("session_idle_close_max_per_pass", 50) or 50
|
|
1334
|
+
)
|
|
1230
1335
|
config.daemon_legacy_port = data.get("daemon_legacy_port", 8767)
|
|
1231
1336
|
config.daemon_enable_legacy_port = data.get("daemon_enable_legacy_port", True)
|
|
1232
1337
|
config.entity_compilation_enabled = data.get("entity_compilation_enabled", True)
|
|
@@ -1324,6 +1429,14 @@ class SLMConfig:
|
|
|
1324
1429
|
"Ignoring invalid scope config (%s) — using shared-off defaults", exc
|
|
1325
1430
|
)
|
|
1326
1431
|
|
|
1432
|
+
# Preserve the raw loaded dict so save() can return every key that was
|
|
1433
|
+
# present in the file, including unknown forward-compat keys and nested
|
|
1434
|
+
# sections whose subkeys this version does not model. The attribute is
|
|
1435
|
+
# intentionally private (leading underscore) and is not a declared field,
|
|
1436
|
+
# so dataclasses.asdict() and dataclasses.replace() ignore it — callers
|
|
1437
|
+
# that clone a config via replace() will lose unknown-key preservation,
|
|
1438
|
+
# which is acceptable: the primary contract is load→save round-trip.
|
|
1439
|
+
config._raw_preserved: dict = dict(data) # type: ignore[attr-defined]
|
|
1327
1440
|
return config
|
|
1328
1441
|
|
|
1329
1442
|
def save(
|
|
@@ -1441,17 +1554,82 @@ class SLMConfig:
|
|
|
1441
1554
|
"enabled": self.graph_pruning.enabled,
|
|
1442
1555
|
}
|
|
1443
1556
|
|
|
1444
|
-
#
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1557
|
+
# Daemon, entity-compilation, and mesh scalar settings were loaded by
|
|
1558
|
+
# load() but omitted from earlier versions of this method. Write them
|
|
1559
|
+
# explicitly so a load-then-save is lossless for these fields.
|
|
1560
|
+
data["daemon_idle_timeout"] = self.daemon_idle_timeout
|
|
1561
|
+
data["daemon_port"] = self.daemon_port
|
|
1562
|
+
data["session_idle_close_hours"] = self.session_idle_close_hours
|
|
1563
|
+
data["session_idle_close_max_per_pass"] = self.session_idle_close_max_per_pass
|
|
1564
|
+
data["daemon_legacy_port"] = self.daemon_legacy_port
|
|
1565
|
+
data["daemon_enable_legacy_port"] = self.daemon_enable_legacy_port
|
|
1566
|
+
data["entity_compilation_enabled"] = self.entity_compilation_enabled
|
|
1567
|
+
data["entity_compilation_retrieval_boost"] = self.entity_compilation_retrieval_boost
|
|
1568
|
+
data["mesh_enabled"] = self.mesh_enabled
|
|
1569
|
+
|
|
1570
|
+
# Typed in-memory config sections. Preserve any on-disk subkeys this
|
|
1571
|
+
# version does not model (forward-compat or externally-tuned fields such
|
|
1572
|
+
# as forgetting.half_life_days), then overlay the current in-memory
|
|
1573
|
+
# dataclass values so a programmatic mutation still survives a
|
|
1574
|
+
# save/load cycle while unmodeled subkeys are never dropped.
|
|
1575
|
+
# dataclasses.asdict() recurses into nested dataclasses
|
|
1576
|
+
# (quantization.polar, quantization.qjl).
|
|
1577
|
+
#
|
|
1578
|
+
# embedding_signature: has no typed in-memory model — it is an opaque blob
|
|
1579
|
+
# written by external tooling (see below).
|
|
1580
|
+
for _section, _obj in (
|
|
1581
|
+
("forgetting", self.forgetting),
|
|
1582
|
+
("quantization", self.quantization),
|
|
1583
|
+
("sagq", self.sagq),
|
|
1584
|
+
("auto_invoke", self.auto_invoke),
|
|
1585
|
+
):
|
|
1586
|
+
_base = existing.get(_section)
|
|
1587
|
+
_merged = dict(_base) if isinstance(_base, dict) else {}
|
|
1588
|
+
_merged.update(asdict(_obj))
|
|
1589
|
+
data[_section] = _merged
|
|
1590
|
+
|
|
1591
|
+
# Merge any keys from the last load() that are not covered by the
|
|
1592
|
+
# explicit serialization above. This includes unknown forward-compat
|
|
1593
|
+
# top-level keys and nested sections whose subkeys this version does
|
|
1594
|
+
# not model (e.g. a "health" dict that carries vendor-extension fields).
|
|
1595
|
+
# Explicitly serialized keys always take precedence; preserved keys are
|
|
1596
|
+
# only written when the slot is vacant in the output dict.
|
|
1597
|
+
_raw = getattr(self, "_raw_preserved", None)
|
|
1598
|
+
if _raw:
|
|
1599
|
+
for _k, _v in _raw.items():
|
|
1600
|
+
if _k not in data:
|
|
1601
|
+
data[_k] = _v
|
|
1602
|
+
|
|
1603
|
+
# embedding_signature has no typed in-memory model — it is an opaque blob
|
|
1604
|
+
# written by external tooling. Preserve the on-disk value verbatim so a
|
|
1605
|
+
# load→save cycle does not erase externally written metadata.
|
|
1606
|
+
if "embedding_signature" in existing:
|
|
1607
|
+
data["embedding_signature"] = existing["embedding_signature"]
|
|
1448
1608
|
|
|
1449
1609
|
# Atomic write: a crash mid-write must NOT leave a truncated/corrupt
|
|
1450
1610
|
# config.json (which would make every subsequent `slm` call fail to load).
|
|
1451
1611
|
import os as _os
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1612
|
+
import tempfile as _tempfile
|
|
1613
|
+
|
|
1614
|
+
_fd, _tmp_name = _tempfile.mkstemp(
|
|
1615
|
+
prefix=f".{path.name}.", suffix=".tmp", dir=path.parent,
|
|
1616
|
+
)
|
|
1617
|
+
_tmp = Path(_tmp_name)
|
|
1618
|
+
try:
|
|
1619
|
+
with _os.fdopen(_fd, "w", encoding="utf-8") as _handle:
|
|
1620
|
+
json.dump(data, _handle, indent=2)
|
|
1621
|
+
_handle.write("\n")
|
|
1622
|
+
_handle.flush()
|
|
1623
|
+
_os.fsync(_handle.fileno())
|
|
1624
|
+
_os.chmod(_tmp, 0o600)
|
|
1625
|
+
_os.replace(_tmp, path)
|
|
1626
|
+
_os.chmod(path, 0o600)
|
|
1627
|
+
except BaseException:
|
|
1628
|
+
try:
|
|
1629
|
+
_tmp.unlink(missing_ok=True)
|
|
1630
|
+
except OSError:
|
|
1631
|
+
pass
|
|
1632
|
+
raise
|
|
1455
1633
|
|
|
1456
1634
|
@staticmethod
|
|
1457
1635
|
def provider_presets() -> dict[str, dict[str, str]]:
|
|
@@ -132,13 +132,13 @@ class ConsolidationEngine:
|
|
|
132
132
|
ccq_worker: CCQWorker | None = None,
|
|
133
133
|
) -> None:
|
|
134
134
|
self._db = db
|
|
135
|
-
self.
|
|
135
|
+
self._consolidation_config = config
|
|
136
136
|
self._summarizer = summarizer
|
|
137
137
|
self._behavioral = behavioral_store
|
|
138
138
|
self._auto_linker = auto_linker
|
|
139
139
|
self._graph_analyzer = graph_analyzer
|
|
140
140
|
self._temporal_validator = temporal_validator
|
|
141
|
-
self.
|
|
141
|
+
self._config = slm_config
|
|
142
142
|
self._ccq_worker = ccq_worker
|
|
143
143
|
self._mode = slm_config.mode.value if slm_config else "a"
|
|
144
144
|
self._store_count: int = 0 # For step-count trigger (L7)
|
|
@@ -213,7 +213,7 @@ class ConsolidationEngine:
|
|
|
213
213
|
def mine(self, *a, **kw): return []
|
|
214
214
|
from superlocalmemory.hooks.auto_parameterize import AutoParameterizeHook
|
|
215
215
|
from superlocalmemory.core.config import ParameterizationConfig
|
|
216
|
-
p_config = getattr(self.
|
|
216
|
+
p_config = getattr(self._config, "parameterization", ParameterizationConfig())
|
|
217
217
|
from superlocalmemory.infra.data_root import state_path
|
|
218
218
|
learning_db = str(state_path("learning.db"))
|
|
219
219
|
beh_store = BehavioralPatternStore(learning_db)
|
|
@@ -246,7 +246,7 @@ class ConsolidationEngine:
|
|
|
246
246
|
# Never on recall/remember hot path. Budget: max 3 per cycle.
|
|
247
247
|
try:
|
|
248
248
|
from superlocalmemory.evolution.skill_evolver import SkillEvolver
|
|
249
|
-
evolver = SkillEvolver(self._db.db_path)
|
|
249
|
+
evolver = SkillEvolver(self._db.db_path, self._config)
|
|
250
250
|
results["skill_evolution"] = evolver.run_consolidation_cycle(profile_id)
|
|
251
251
|
except Exception as exc:
|
|
252
252
|
logger.debug("Skill evolution (non-fatal): %s", exc)
|
|
@@ -286,11 +286,11 @@ class ConsolidationEngine:
|
|
|
286
286
|
|
|
287
287
|
Returns True if lightweight consolidation was triggered.
|
|
288
288
|
"""
|
|
289
|
-
if not self.
|
|
289
|
+
if not self._consolidation_config.enabled:
|
|
290
290
|
return False
|
|
291
291
|
|
|
292
292
|
self._store_count += 1
|
|
293
|
-
if self._store_count >= self.
|
|
293
|
+
if self._store_count >= self._consolidation_config.step_count_trigger:
|
|
294
294
|
self._store_count = 0
|
|
295
295
|
self.consolidate(profile_id, lightweight=True)
|
|
296
296
|
# V3.4.2: Queue graph analysis in background (non-blocking)
|
|
@@ -388,7 +388,7 @@ class ConsolidationEngine:
|
|
|
388
388
|
- learned_preferences: opinion facts with confidence >= threshold
|
|
389
389
|
"""
|
|
390
390
|
blocks_compiled = 0
|
|
391
|
-
block_limit = self.
|
|
391
|
+
block_limit = self._consolidation_config.block_char_limit
|
|
392
392
|
|
|
393
393
|
# 1. user_profile: top semantic/opinion facts by access
|
|
394
394
|
user_facts = self._get_top_facts(
|
|
@@ -439,7 +439,7 @@ class ConsolidationEngine:
|
|
|
439
439
|
"GROUP BY f.fact_id "
|
|
440
440
|
"HAVING COUNT(a.log_id) >= ? "
|
|
441
441
|
"ORDER BY COUNT(a.log_id) DESC LIMIT 5",
|
|
442
|
-
(profile_id, self.
|
|
442
|
+
(profile_id, self._consolidation_config.promotion_min_access),
|
|
443
443
|
)
|
|
444
444
|
self._store_core_block(
|
|
445
445
|
profile_id, "active_decisions",
|
|
@@ -454,7 +454,7 @@ class ConsolidationEngine:
|
|
|
454
454
|
"WHERE profile_id = ? AND fact_type = 'opinion' "
|
|
455
455
|
"AND confidence >= ? AND lifecycle = 'active' "
|
|
456
456
|
"ORDER BY confidence DESC LIMIT 5",
|
|
457
|
-
(profile_id, self.
|
|
457
|
+
(profile_id, self._consolidation_config.promotion_min_trust),
|
|
458
458
|
)
|
|
459
459
|
self._store_core_block(
|
|
460
460
|
profile_id, "learned_preferences",
|
|
@@ -491,7 +491,7 @@ class ConsolidationEngine:
|
|
|
491
491
|
summary = self._summarizer.summarize_cluster(fact_dicts)
|
|
492
492
|
self._store_core_block(
|
|
493
493
|
profile_id, block_type,
|
|
494
|
-
summary[:self.
|
|
494
|
+
summary[:self._consolidation_config.block_char_limit],
|
|
495
495
|
[f["fact_id"] for f in facts],
|
|
496
496
|
compiled_by="llm",
|
|
497
497
|
)
|
|
@@ -524,7 +524,7 @@ class ConsolidationEngine:
|
|
|
524
524
|
"WHERE f.profile_id = ? AND f.lifecycle = 'active' "
|
|
525
525
|
"GROUP BY f.fact_id "
|
|
526
526
|
"HAVING COUNT(a.log_id) >= ?",
|
|
527
|
-
(profile_id, self.
|
|
527
|
+
(profile_id, self._consolidation_config.promotion_min_access),
|
|
528
528
|
)
|
|
529
529
|
|
|
530
530
|
promoted = 0
|
|
@@ -537,7 +537,7 @@ class ConsolidationEngine:
|
|
|
537
537
|
continue
|
|
538
538
|
|
|
539
539
|
# Trust check
|
|
540
|
-
if d.get("confidence", 0) < self.
|
|
540
|
+
if d.get("confidence", 0) < self._consolidation_config.promotion_min_trust:
|
|
541
541
|
continue
|
|
542
542
|
|
|
543
543
|
# Promote: active -> warm (lifecycle transition)
|
|
@@ -606,7 +606,7 @@ class ConsolidationEngine:
|
|
|
606
606
|
return {"decayed": 0}
|
|
607
607
|
try:
|
|
608
608
|
decayed = self._auto_linker.decay_unused(
|
|
609
|
-
profile_id, days_threshold=self.
|
|
609
|
+
profile_id, days_threshold=self._consolidation_config.decay_days_threshold,
|
|
610
610
|
)
|
|
611
611
|
return {"decayed": decayed}
|
|
612
612
|
except Exception as exc:
|
|
@@ -478,6 +478,33 @@ def read_entry_fast(
|
|
|
478
478
|
return None
|
|
479
479
|
|
|
480
480
|
|
|
481
|
+
def purge_profile_from_cache_db(db_path: Path, profile_id: str) -> int:
|
|
482
|
+
"""Delete all ``context_entries`` rows for *profile_id* from a cache DB file.
|
|
483
|
+
|
|
484
|
+
Opens the file with a direct write connection (no URI, no read-only flag)
|
|
485
|
+
so it works even when the daemon is not running. Returns the number of rows
|
|
486
|
+
deleted. Never raises — any error returns 0 so callers can remain
|
|
487
|
+
fail-open.
|
|
488
|
+
"""
|
|
489
|
+
try:
|
|
490
|
+
if not Path(db_path).exists():
|
|
491
|
+
return 0
|
|
492
|
+
conn = sqlite3.connect(str(db_path), isolation_level=None, timeout=2.0)
|
|
493
|
+
try:
|
|
494
|
+
cur = conn.execute(
|
|
495
|
+
"DELETE FROM context_entries WHERE profile_id = ?",
|
|
496
|
+
(profile_id,),
|
|
497
|
+
)
|
|
498
|
+
return cur.rowcount
|
|
499
|
+
finally:
|
|
500
|
+
try:
|
|
501
|
+
conn.close()
|
|
502
|
+
except sqlite3.Error:
|
|
503
|
+
pass
|
|
504
|
+
except Exception:
|
|
505
|
+
return 0
|
|
506
|
+
|
|
507
|
+
|
|
481
508
|
__all__ = (
|
|
482
509
|
"CACHE_DB_DEFAULT",
|
|
483
510
|
"CLEANUP_HORIZON_SECONDS",
|
|
@@ -487,5 +514,6 @@ __all__ = (
|
|
|
487
514
|
"MAX_CONTENT_CHARS",
|
|
488
515
|
"SCHEMA_VERSION",
|
|
489
516
|
"TTL_SECONDS",
|
|
517
|
+
"purge_profile_from_cache_db",
|
|
490
518
|
"read_entry_fast",
|
|
491
519
|
)
|
|
@@ -543,7 +543,47 @@ class EmbeddingService:
|
|
|
543
543
|
|
|
544
544
|
@staticmethod
|
|
545
545
|
def _readline_with_timeout(stream, timeout_seconds: float) -> str:
|
|
546
|
-
"""Read a line from stream with a timeout. Returns '' on timeout.
|
|
546
|
+
"""Read a line from stream with a timeout. Returns '' on timeout.
|
|
547
|
+
|
|
548
|
+
Prefer a deadline-driven selector poll of the stream's file descriptor
|
|
549
|
+
(POSIX pipes). That path never spawns a helper thread, so a hung
|
|
550
|
+
embedding worker cannot leak reader threads or pin the pipe FD across
|
|
551
|
+
timeouts. A thread fallback remains only for streams without a usable
|
|
552
|
+
fileno (unit-test mocks) and for Windows, where selectors cannot wait
|
|
553
|
+
on pipes.
|
|
554
|
+
"""
|
|
555
|
+
import selectors
|
|
556
|
+
|
|
557
|
+
timeout_seconds = max(0.0, float(timeout_seconds))
|
|
558
|
+
fd: int | None
|
|
559
|
+
try:
|
|
560
|
+
raw_fd = stream.fileno()
|
|
561
|
+
fd = raw_fd if isinstance(raw_fd, int) else None
|
|
562
|
+
except (AttributeError, OSError, ValueError, TypeError):
|
|
563
|
+
fd = None
|
|
564
|
+
|
|
565
|
+
# Windows select()/selectors only accept sockets, not subprocess pipes.
|
|
566
|
+
if fd is not None and sys.platform != "win32":
|
|
567
|
+
try:
|
|
568
|
+
with selectors.DefaultSelector() as sel:
|
|
569
|
+
sel.register(fd, selectors.EVENT_READ)
|
|
570
|
+
events = sel.select(timeout=timeout_seconds)
|
|
571
|
+
if not events:
|
|
572
|
+
logger.warning(
|
|
573
|
+
"Embedding worker did not respond within %ds",
|
|
574
|
+
timeout_seconds,
|
|
575
|
+
)
|
|
576
|
+
return ""
|
|
577
|
+
# Readable or EOF. Protocol is one JSON line per response;
|
|
578
|
+
# the worker writes a complete line before we are woken.
|
|
579
|
+
line = stream.readline()
|
|
580
|
+
return line if line else ""
|
|
581
|
+
except (OSError, ValueError) as exc:
|
|
582
|
+
# Closed/invalid FD mid-wait — same as empty to the caller
|
|
583
|
+
# (which kills and may respawn the worker).
|
|
584
|
+
logger.debug("Embedding readline selector failed: %s", exc)
|
|
585
|
+
return ""
|
|
586
|
+
|
|
547
587
|
result_container: list[str] = []
|
|
548
588
|
error_container: list[Exception] = []
|
|
549
589
|
|
|
@@ -553,7 +593,10 @@ class EmbeddingService:
|
|
|
553
593
|
except Exception as exc:
|
|
554
594
|
error_container.append(exc)
|
|
555
595
|
|
|
556
|
-
|
|
596
|
+
# Name contains ``_read`` so leak detectors can find abandoned readers.
|
|
597
|
+
reader = threading.Thread(
|
|
598
|
+
target=_read, daemon=True, name="slm_embed_readline_read",
|
|
599
|
+
)
|
|
557
600
|
reader.start()
|
|
558
601
|
reader.join(timeout=timeout_seconds)
|
|
559
602
|
|
|
@@ -561,6 +604,25 @@ class EmbeddingService:
|
|
|
561
604
|
logger.warning(
|
|
562
605
|
"Embedding worker did not respond within %ds", timeout_seconds,
|
|
563
606
|
)
|
|
607
|
+
# Close/shutdown the stream so the blocked readline() returns and
|
|
608
|
+
# the reader thread can exit. Raising alone would leak the thread
|
|
609
|
+
# (and its FD) on Windows pipes and fileno-less mocks.
|
|
610
|
+
for closer_name in ("close", "shutdown"):
|
|
611
|
+
closer = getattr(stream, closer_name, None)
|
|
612
|
+
if not callable(closer):
|
|
613
|
+
continue
|
|
614
|
+
try:
|
|
615
|
+
if closer_name == "shutdown":
|
|
616
|
+
try:
|
|
617
|
+
closer(True) # type: ignore[misc]
|
|
618
|
+
except TypeError:
|
|
619
|
+
closer()
|
|
620
|
+
else:
|
|
621
|
+
closer()
|
|
622
|
+
except Exception:
|
|
623
|
+
pass
|
|
624
|
+
# Bound the join so a stuck closer cannot hang the caller forever.
|
|
625
|
+
reader.join(timeout=min(1.0, max(0.05, timeout_seconds)))
|
|
564
626
|
return ""
|
|
565
627
|
if error_container:
|
|
566
628
|
raise error_container[0]
|
|
@@ -304,6 +304,11 @@ class MemoryEngine:
|
|
|
304
304
|
|
|
305
305
|
self._embedder = init_embedder(self._config)
|
|
306
306
|
|
|
307
|
+
# Rebuild the complete vector projection before VectorStore opens it.
|
|
308
|
+
# This preserves the previous vec0 table until the shadow activation
|
|
309
|
+
# succeeds, including across embedding-dimension changes.
|
|
310
|
+
self._check_embedding_migration()
|
|
311
|
+
|
|
307
312
|
if self._caps.llm_fact_extraction:
|
|
308
313
|
self._llm = LLMBackbone(self._config.llm)
|
|
309
314
|
if not self._llm.is_available():
|
|
@@ -394,8 +399,6 @@ class MemoryEngine:
|
|
|
394
399
|
llm=getattr(self, "_llm", None), # v3.4.7: for CCQ worker
|
|
395
400
|
)
|
|
396
401
|
|
|
397
|
-
self._check_embedding_migration()
|
|
398
|
-
|
|
399
402
|
# Lifecycle/tier evaluation, bounded housekeeping, and backup checks
|
|
400
403
|
# must continue even when optional forgetting/math maintenance is
|
|
401
404
|
# disabled. The scheduler itself gates those optional calculations.
|
|
@@ -703,6 +706,7 @@ class MemoryEngine:
|
|
|
703
706
|
include_global: bool | None = None,
|
|
704
707
|
include_shared: bool | None = None,
|
|
705
708
|
window: str | tuple[str, str] | None = None,
|
|
709
|
+
as_of: str | None = None,
|
|
706
710
|
) -> RecallResponse:
|
|
707
711
|
"""Recall relevant facts for a query.
|
|
708
712
|
|
|
@@ -758,6 +762,7 @@ class MemoryEngine:
|
|
|
758
762
|
include_global=include_global,
|
|
759
763
|
include_shared=include_shared,
|
|
760
764
|
window=window,
|
|
765
|
+
as_of=as_of,
|
|
761
766
|
)
|
|
762
767
|
except Exception:
|
|
763
768
|
# Diagnostics are intentionally not recorded here. A recall is a
|