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
|
@@ -12,19 +12,34 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
|
12
12
|
"""
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
-
import json
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
import sqlite3
|
|
19
|
+
import threading
|
|
20
|
+
import time
|
|
16
21
|
from contextlib import contextmanager
|
|
17
22
|
from pathlib import Path
|
|
18
23
|
from types import ModuleType
|
|
19
24
|
from typing import Any, Generator
|
|
20
25
|
|
|
21
|
-
from superlocalmemory.storage.write_lock import get_write_lock
|
|
22
26
|
from superlocalmemory.storage.models import (
|
|
23
|
-
AtomicFact,
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
AtomicFact,
|
|
28
|
+
CanonicalEntity,
|
|
29
|
+
ConsolidationAction,
|
|
30
|
+
EdgeType,
|
|
31
|
+
EntityAlias,
|
|
32
|
+
EntityProfile,
|
|
33
|
+
FactType,
|
|
34
|
+
GraphEdge,
|
|
35
|
+
MemoryLifecycle,
|
|
36
|
+
MemoryRecord,
|
|
37
|
+
MemoryScene,
|
|
38
|
+
SignalType,
|
|
39
|
+
TemporalEvent,
|
|
26
40
|
TrustScore,
|
|
27
41
|
)
|
|
42
|
+
from superlocalmemory.storage.write_lock import get_write_lock
|
|
28
43
|
|
|
29
44
|
logger = logging.getLogger(__name__)
|
|
30
45
|
|
|
@@ -559,6 +574,24 @@ class DatabaseManager:
|
|
|
559
574
|
)
|
|
560
575
|
return [self._row_to_fact(r) for r in rows]
|
|
561
576
|
|
|
577
|
+
def _has_archive_status(self) -> bool:
|
|
578
|
+
"""Whether atomic_facts carries the M011 ``archive_status`` column.
|
|
579
|
+
|
|
580
|
+
M011 is a DEFERRED migration, so the column is absent until it runs;
|
|
581
|
+
callers must not filter on a column that may not exist. Cached once True
|
|
582
|
+
(a column never disappears); re-checked while absent so a later deferred
|
|
583
|
+
migration is picked up.
|
|
584
|
+
"""
|
|
585
|
+
if getattr(self, "_archive_col_present", False):
|
|
586
|
+
return True
|
|
587
|
+
present = any(
|
|
588
|
+
dict(row).get("name") == "archive_status"
|
|
589
|
+
for row in self.execute("PRAGMA table_info(atomic_facts)")
|
|
590
|
+
)
|
|
591
|
+
if present:
|
|
592
|
+
self._archive_col_present = True
|
|
593
|
+
return present
|
|
594
|
+
|
|
562
595
|
def get_all_facts(
|
|
563
596
|
self, profile_id: str, limit: int | None = None,
|
|
564
597
|
*,
|
|
@@ -581,8 +614,14 @@ class DatabaseManager:
|
|
|
581
614
|
# hard, env-tunable ceiling even when the caller passes limit=None.
|
|
582
615
|
if limit is None:
|
|
583
616
|
limit = _unbounded_facts_ceiling()
|
|
617
|
+
# Archived facts are not live; never surface them in direct reads.
|
|
618
|
+
archive_clause = (
|
|
619
|
+
" AND COALESCE(archive_status, 'live') != 'archived'"
|
|
620
|
+
if self._has_archive_status()
|
|
621
|
+
else ""
|
|
622
|
+
)
|
|
584
623
|
rows = self.execute(
|
|
585
|
-
f"SELECT * FROM atomic_facts WHERE {where} "
|
|
624
|
+
f"SELECT * FROM atomic_facts WHERE {where}{archive_clause} "
|
|
586
625
|
"ORDER BY created_at DESC LIMIT ?",
|
|
587
626
|
(*params, int(limit)),
|
|
588
627
|
)
|
|
@@ -610,9 +649,14 @@ class DatabaseManager:
|
|
|
610
649
|
include_global=include_global,
|
|
611
650
|
include_shared=include_shared,
|
|
612
651
|
)
|
|
652
|
+
archive_clause = (
|
|
653
|
+
" AND COALESCE(archive_status, 'live') != 'archived'"
|
|
654
|
+
if self._has_archive_status()
|
|
655
|
+
else ""
|
|
656
|
+
)
|
|
613
657
|
rows = self.execute(
|
|
614
|
-
f"SELECT * FROM atomic_facts WHERE {where} AND profile_id != ?
|
|
615
|
-
"ORDER BY created_at DESC",
|
|
658
|
+
f"SELECT * FROM atomic_facts WHERE {where} AND profile_id != ?"
|
|
659
|
+
f"{archive_clause} ORDER BY created_at DESC",
|
|
616
660
|
(*params, profile_id),
|
|
617
661
|
)
|
|
618
662
|
return [self._row_to_fact(r) for r in rows]
|
|
@@ -1039,10 +1083,16 @@ class DatabaseManager:
|
|
|
1039
1083
|
include_shared=include_shared,
|
|
1040
1084
|
prefix="f",
|
|
1041
1085
|
)
|
|
1086
|
+
# Archived facts must not surface via full-text search either.
|
|
1087
|
+
archive_clause = (
|
|
1088
|
+
" AND COALESCE(f.archive_status, 'live') != 'archived'"
|
|
1089
|
+
if self._has_archive_status()
|
|
1090
|
+
else ""
|
|
1091
|
+
)
|
|
1042
1092
|
rows = self.execute(
|
|
1043
1093
|
f"""SELECT f.* FROM atomic_facts_fts AS fts
|
|
1044
1094
|
JOIN atomic_facts AS f ON f.fact_id = fts.fact_id
|
|
1045
|
-
WHERE fts.atomic_facts_fts MATCH ? AND {where}
|
|
1095
|
+
WHERE fts.atomic_facts_fts MATCH ? AND {where}{archive_clause}
|
|
1046
1096
|
ORDER BY fts.rank LIMIT ?""",
|
|
1047
1097
|
(match_expr, *params, limit),
|
|
1048
1098
|
)
|
|
@@ -1104,10 +1154,15 @@ class DatabaseManager:
|
|
|
1104
1154
|
include_global=include_global,
|
|
1105
1155
|
include_shared=include_shared,
|
|
1106
1156
|
)
|
|
1157
|
+
archive_clause = (
|
|
1158
|
+
" AND COALESCE(archive_status, 'live') != 'archived'"
|
|
1159
|
+
if self._has_archive_status()
|
|
1160
|
+
else ""
|
|
1161
|
+
)
|
|
1107
1162
|
placeholders = ",".join("?" for _ in fact_ids)
|
|
1108
1163
|
rows = self.execute(
|
|
1109
1164
|
f"SELECT * FROM atomic_facts WHERE fact_id IN ({placeholders}) "
|
|
1110
|
-
f"AND {where} ORDER BY created_at DESC",
|
|
1165
|
+
f"AND {where}{archive_clause} ORDER BY created_at DESC",
|
|
1111
1166
|
(*fact_ids, *params),
|
|
1112
1167
|
)
|
|
1113
1168
|
return [self._row_to_fact(r) for r in rows]
|
|
@@ -1545,19 +1600,47 @@ class DatabaseManager:
|
|
|
1545
1600
|
self, fact_id: str, invalidated_by: str,
|
|
1546
1601
|
invalidation_reason: str,
|
|
1547
1602
|
) -> None:
|
|
1548
|
-
"""
|
|
1603
|
+
"""Mark a fact as invalidated, preserving bi-temporal independence.
|
|
1604
|
+
|
|
1605
|
+
- valid_until (event-time): when the fact ceased to be true in the
|
|
1606
|
+
real world. Sourced from the fact's referenced_date so the
|
|
1607
|
+
real-world boundary is preserved, not overwritten with wall-clock time.
|
|
1608
|
+
- system_expired_at (transaction-time): when the system learned the
|
|
1609
|
+
fact was invalid — always set to now.
|
|
1610
|
+
|
|
1611
|
+
The two dimensions must remain independent: a fact can be true until
|
|
1612
|
+
2020-06-30 in the real world (valid_until) while the system only
|
|
1613
|
+
discovers this in 2024 (system_expired_at).
|
|
1549
1614
|
|
|
1550
|
-
|
|
1551
|
-
Never deletes the fact (Rule 17: immutability).
|
|
1615
|
+
Never deletes the fact (immutability).
|
|
1552
1616
|
"""
|
|
1553
|
-
from datetime import UTC
|
|
1617
|
+
from datetime import UTC
|
|
1618
|
+
from datetime import datetime as _dt
|
|
1554
1619
|
now = _dt.now(UTC).isoformat()
|
|
1620
|
+
|
|
1621
|
+
# Resolve the event-time boundary from the fact's referenced_date.
|
|
1622
|
+
# Falls back to the current valid_until (which may already be set),
|
|
1623
|
+
# and ultimately to now if neither is available.
|
|
1624
|
+
fact_rows = self.execute(
|
|
1625
|
+
"SELECT referenced_date FROM atomic_facts WHERE fact_id = ?",
|
|
1626
|
+
(fact_id,),
|
|
1627
|
+
)
|
|
1628
|
+
referenced_date = dict(fact_rows[0]).get("referenced_date") if fact_rows else None
|
|
1629
|
+
|
|
1630
|
+
tv_rows = self.execute(
|
|
1631
|
+
"SELECT valid_until FROM fact_temporal_validity WHERE fact_id = ?",
|
|
1632
|
+
(fact_id,),
|
|
1633
|
+
)
|
|
1634
|
+
existing_valid_until = dict(tv_rows[0]).get("valid_until") if tv_rows else None
|
|
1635
|
+
|
|
1636
|
+
valid_until = referenced_date or existing_valid_until or now
|
|
1637
|
+
|
|
1555
1638
|
self.execute(
|
|
1556
1639
|
"UPDATE fact_temporal_validity "
|
|
1557
1640
|
"SET valid_until = ?, system_expired_at = ?, "
|
|
1558
1641
|
" invalidated_by = ?, invalidation_reason = ? "
|
|
1559
1642
|
"WHERE fact_id = ?",
|
|
1560
|
-
(
|
|
1643
|
+
(valid_until, now, invalidated_by, invalidation_reason, fact_id),
|
|
1561
1644
|
)
|
|
1562
1645
|
|
|
1563
1646
|
def get_valid_facts(self, profile_id: str) -> list[str]:
|
|
@@ -1587,7 +1670,10 @@ class DatabaseManager:
|
|
|
1587
1670
|
return [dict(r)["fact_id"] for r in rows]
|
|
1588
1671
|
|
|
1589
1672
|
def get_invalidated_fact_ids(
|
|
1590
|
-
self,
|
|
1673
|
+
self,
|
|
1674
|
+
fact_ids: list[str],
|
|
1675
|
+
profile_id: str,
|
|
1676
|
+
as_of: str | None = None,
|
|
1591
1677
|
) -> set[str]:
|
|
1592
1678
|
"""Return the subset of ``fact_ids`` that are system-invalidated.
|
|
1593
1679
|
|
|
@@ -1596,6 +1682,21 @@ class DatabaseManager:
|
|
|
1596
1682
|
``invalidate_fact_temporal``). Such facts are wrong/outdated and must be
|
|
1597
1683
|
excluded from default retrieval (T1, Phase 4).
|
|
1598
1684
|
|
|
1685
|
+
Phase 4b — bi-temporal as_of:
|
|
1686
|
+
When ``as_of`` is None (default): returns ALL facts with
|
|
1687
|
+
``system_expired_at IS NOT NULL`` — existing behaviour, no regression.
|
|
1688
|
+
|
|
1689
|
+
When ``as_of`` is set (UTC ISO 8601, "+00:00" suffix): returns only
|
|
1690
|
+
facts where ``system_expired_at <= as_of`` (transaction-time boundary
|
|
1691
|
+
inclusive). This means supersessions that occurred AFTER ``as_of`` are
|
|
1692
|
+
excluded — at the historical query point the fact was still valid.
|
|
1693
|
+
|
|
1694
|
+
``as_of`` MUST be UTC-normalized via ``normalize_as_of()`` before this
|
|
1695
|
+
call. The stored ``system_expired_at`` values use Python's
|
|
1696
|
+
``datetime.now(UTC).isoformat()`` format ("...+00:00") so the
|
|
1697
|
+
``normalize_as_of()`` "+00:00" output produces correct lexicographic
|
|
1698
|
+
SQL comparisons.
|
|
1699
|
+
|
|
1599
1700
|
Bounded + indexed: only the supplied candidate ids are queried (never a
|
|
1600
1701
|
full-table scan), keyed on the ``fact_id`` PK with the
|
|
1601
1702
|
``idx_temporal_system_expired`` index covering the predicate. Chunked to
|
|
@@ -1615,17 +1716,118 @@ class DatabaseManager:
|
|
|
1615
1716
|
for start in range(0, len(fact_ids), chunk):
|
|
1616
1717
|
batch = fact_ids[start:start + chunk]
|
|
1617
1718
|
placeholders = ",".join("?" for _ in batch)
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1719
|
+
if as_of is not None:
|
|
1720
|
+
# Transaction-time point-in-time: only supersessions that
|
|
1721
|
+
# occurred AT OR BEFORE as_of contribute to invalidation.
|
|
1722
|
+
# Supersessions after as_of are invisible at this query point.
|
|
1723
|
+
rows = self.execute(
|
|
1724
|
+
f"SELECT fact_id FROM fact_temporal_validity "
|
|
1725
|
+
f"WHERE fact_id IN ({placeholders}) "
|
|
1726
|
+
f" AND profile_id = ? "
|
|
1727
|
+
f" AND system_expired_at IS NOT NULL "
|
|
1728
|
+
f" AND system_expired_at <= ?",
|
|
1729
|
+
(*batch, profile_id, as_of),
|
|
1730
|
+
)
|
|
1731
|
+
else:
|
|
1732
|
+
rows = self.execute(
|
|
1733
|
+
f"SELECT fact_id FROM fact_temporal_validity "
|
|
1734
|
+
f"WHERE fact_id IN ({placeholders}) "
|
|
1735
|
+
f" AND profile_id = ? "
|
|
1736
|
+
f" AND system_expired_at IS NOT NULL",
|
|
1737
|
+
(*batch, profile_id),
|
|
1738
|
+
)
|
|
1625
1739
|
for r in rows:
|
|
1626
1740
|
invalid.add(dict(r)["fact_id"])
|
|
1627
1741
|
return invalid
|
|
1628
1742
|
|
|
1743
|
+
def get_event_time_expired_fact_ids(
|
|
1744
|
+
self,
|
|
1745
|
+
fact_ids: list[str],
|
|
1746
|
+
profile_id: str,
|
|
1747
|
+
as_of: str | None = None,
|
|
1748
|
+
) -> set[str]:
|
|
1749
|
+
"""Return the subset of ``fact_ids`` that are event-time out-of-range.
|
|
1750
|
+
|
|
1751
|
+
Returns fact_ids whose event-time validity window does not encompass
|
|
1752
|
+
``as_of`` (or the current wall-clock time when ``as_of`` is None):
|
|
1753
|
+
|
|
1754
|
+
1. **Already expired** — ``valid_until IS NOT NULL AND valid_until <= ref``
|
|
1755
|
+
where ``ref`` is ``as_of`` when provided or the current UTC time.
|
|
1756
|
+
The ``<=`` implements the half-open interval ``[valid_from, valid_until)``:
|
|
1757
|
+
a fact with ``valid_until == as_of`` has expired at that boundary (Phase 4b fix).
|
|
1758
|
+
2. **Not yet valid** — ``valid_from IS NOT NULL AND valid_from > as_of``
|
|
1759
|
+
(only when ``as_of`` is provided for explicit point-in-time recall).
|
|
1760
|
+
|
|
1761
|
+
Zero-regression guarantee: facts with ``valid_until = NULL`` are
|
|
1762
|
+
assumed open-ended (still valid) and are NEVER returned. Facts with no
|
|
1763
|
+
temporal record at all are NEVER returned (assumed valid). Because almost
|
|
1764
|
+
all existing facts have ``valid_until = NULL``, the default path
|
|
1765
|
+
(``as_of=None``) returns an empty set and causes no demotion.
|
|
1766
|
+
|
|
1767
|
+
Bounded + indexed: only the supplied candidate ids are queried (never a
|
|
1768
|
+
full-table scan), keyed on the ``fact_id`` PK. Chunked to stay under
|
|
1769
|
+
SQLite's ~999 bound-parameter limit (chunk size 900). The
|
|
1770
|
+
``idx_temporal_valid(profile_id, valid_until)`` index assists the
|
|
1771
|
+
``valid_until <`` range predicate after the PK IN-lookup. The
|
|
1772
|
+
``valid_from > as_of`` branch operates on the same bounded PK row set
|
|
1773
|
+
(≤ 900 rows per chunk) — no full-table scan occurs.
|
|
1774
|
+
|
|
1775
|
+
Fail-open: any DB error logs a warning and returns an empty set so
|
|
1776
|
+
retrieval can never break because of a validity lookup failure.
|
|
1777
|
+
|
|
1778
|
+
Args:
|
|
1779
|
+
fact_ids: Candidate fact IDs to check (bounded retrieval pool).
|
|
1780
|
+
profile_id: Current profile — scopes the lookup to one tenant.
|
|
1781
|
+
as_of: Optional ISO 8601 datetime string for point-in-time recall.
|
|
1782
|
+
When set, facts not yet valid at this time are also returned.
|
|
1783
|
+
When None (default), only facts past their ``valid_until`` are
|
|
1784
|
+
returned — the standard current-time path.
|
|
1785
|
+
|
|
1786
|
+
Note:
|
|
1787
|
+
``as_of`` must be in the same ISO 8601 format as the stored
|
|
1788
|
+
``valid_until`` / ``valid_from`` values so SQLite's lexicographic
|
|
1789
|
+
string comparison correctly orders the timestamps.
|
|
1790
|
+
"""
|
|
1791
|
+
if not fact_ids:
|
|
1792
|
+
return set()
|
|
1793
|
+
expired: set[str] = set()
|
|
1794
|
+
try:
|
|
1795
|
+
chunk = 900
|
|
1796
|
+
for start in range(0, len(fact_ids), chunk):
|
|
1797
|
+
batch = fact_ids[start:start + chunk]
|
|
1798
|
+
placeholders = ",".join("?" for _ in batch)
|
|
1799
|
+
if as_of is not None:
|
|
1800
|
+
# Time-travel: expired-before-as_of OR not-yet-started-at-as_of.
|
|
1801
|
+
rows = self.execute(
|
|
1802
|
+
f"SELECT fact_id FROM fact_temporal_validity "
|
|
1803
|
+
f"WHERE fact_id IN ({placeholders}) "
|
|
1804
|
+
f" AND profile_id = ? "
|
|
1805
|
+
f" AND ("
|
|
1806
|
+
f" (valid_until IS NOT NULL AND valid_until <= ?) "
|
|
1807
|
+
f" OR (valid_from IS NOT NULL AND valid_from > ?)"
|
|
1808
|
+
f" )",
|
|
1809
|
+
(*batch, profile_id, as_of, as_of),
|
|
1810
|
+
)
|
|
1811
|
+
else:
|
|
1812
|
+
# Default path: only facts whose valid_until has passed.
|
|
1813
|
+
# idx_temporal_valid(profile_id, valid_until) assists range scan.
|
|
1814
|
+
rows = self.execute(
|
|
1815
|
+
f"SELECT fact_id FROM fact_temporal_validity "
|
|
1816
|
+
f"WHERE fact_id IN ({placeholders}) "
|
|
1817
|
+
f" AND profile_id = ? "
|
|
1818
|
+
f" AND valid_until IS NOT NULL "
|
|
1819
|
+
f" AND valid_until < strftime('%Y-%m-%dT%H:%M:%SZ', 'now')",
|
|
1820
|
+
(*batch, profile_id),
|
|
1821
|
+
)
|
|
1822
|
+
for r in rows:
|
|
1823
|
+
expired.add(dict(r)["fact_id"])
|
|
1824
|
+
except Exception as exc:
|
|
1825
|
+
logger.warning(
|
|
1826
|
+
"Event-time expiry lookup failed (fail-open): %s", exc,
|
|
1827
|
+
)
|
|
1828
|
+
return set()
|
|
1829
|
+
return expired
|
|
1830
|
+
|
|
1629
1831
|
def get_fact_event_times(
|
|
1630
1832
|
self, fact_ids: list[str], profile_id: str,
|
|
1631
1833
|
) -> dict[str, str]:
|
|
@@ -1655,7 +1857,8 @@ class DatabaseManager:
|
|
|
1655
1857
|
f" tv.valid_from, f.created_at) AS event_time "
|
|
1656
1858
|
f"FROM atomic_facts f "
|
|
1657
1859
|
f"LEFT JOIN fact_temporal_validity tv ON f.fact_id = tv.fact_id "
|
|
1658
|
-
f"WHERE f.fact_id IN ({placeholders})
|
|
1860
|
+
f"WHERE f.fact_id IN ({placeholders}) "
|
|
1861
|
+
f"AND (f.profile_id = ? OR f.scope = 'global')",
|
|
1659
1862
|
(*batch, profile_id),
|
|
1660
1863
|
)
|
|
1661
1864
|
for r in rows:
|
|
@@ -1696,6 +1899,108 @@ class DatabaseManager:
|
|
|
1696
1899
|
except Exception as exc: # pragma: no cover — legacy/missing FTS table
|
|
1697
1900
|
logger.debug("upsert_fact_expansion skipped for %s: %s", fact_id, exc)
|
|
1698
1901
|
|
|
1902
|
+
def reset_fact_expansion(self, fact_id: str, alt_keys: str = "") -> None:
|
|
1903
|
+
"""Replace the expansion row unconditionally, keeping it alive with new alt_keys.
|
|
1904
|
+
|
|
1905
|
+
Unlike ``upsert_fact_expansion``, this always inserts (even when
|
|
1906
|
+
``alt_keys`` is empty) so the row survives as a cleared placeholder.
|
|
1907
|
+
Used by update paths that must guarantee the expansion entry exists but
|
|
1908
|
+
holds no stale tokens. Fail-soft: a missing FTS table is a no-op.
|
|
1909
|
+
"""
|
|
1910
|
+
try:
|
|
1911
|
+
self.execute(
|
|
1912
|
+
"DELETE FROM fact_expansion_fts WHERE fact_id = ?", (fact_id,)
|
|
1913
|
+
)
|
|
1914
|
+
self.execute(
|
|
1915
|
+
"INSERT INTO fact_expansion_fts (fact_id, alt_keys) VALUES (?, ?)",
|
|
1916
|
+
(fact_id, alt_keys),
|
|
1917
|
+
)
|
|
1918
|
+
except Exception as exc:
|
|
1919
|
+
logger.debug("reset_fact_expansion skipped for %s: %s", fact_id, exc)
|
|
1920
|
+
|
|
1921
|
+
def update_temporal_event_description(
|
|
1922
|
+
self, fact_id: str, description: str
|
|
1923
|
+
) -> None:
|
|
1924
|
+
"""Update the description column in ``temporal_events`` for a fact.
|
|
1925
|
+
|
|
1926
|
+
Fail-soft: absent table (pre-migration DB) is silently skipped.
|
|
1927
|
+
"""
|
|
1928
|
+
try:
|
|
1929
|
+
self.execute(
|
|
1930
|
+
"UPDATE temporal_events SET description = ? WHERE fact_id = ?",
|
|
1931
|
+
(description, fact_id),
|
|
1932
|
+
)
|
|
1933
|
+
except Exception as exc:
|
|
1934
|
+
logger.debug(
|
|
1935
|
+
"update_temporal_event_description skipped for %s: %s", fact_id, exc
|
|
1936
|
+
)
|
|
1937
|
+
|
|
1938
|
+
def delete_bm25_tokens_for_fact(self, fact_id: str) -> None:
|
|
1939
|
+
"""Delete persisted BM25 tokens for a fact from the ``bm25_tokens`` table."""
|
|
1940
|
+
try:
|
|
1941
|
+
self.execute(
|
|
1942
|
+
"DELETE FROM bm25_tokens WHERE fact_id = ?", (fact_id,)
|
|
1943
|
+
)
|
|
1944
|
+
except Exception as exc:
|
|
1945
|
+
logger.debug("delete_bm25_tokens_for_fact skipped for %s: %s", fact_id, exc)
|
|
1946
|
+
|
|
1947
|
+
def delete_graph_edges_for_fact(self, fact_id: str) -> None:
|
|
1948
|
+
"""Delete all graph edges where this fact is the source or the target."""
|
|
1949
|
+
try:
|
|
1950
|
+
self.execute(
|
|
1951
|
+
"DELETE FROM graph_edges WHERE source_id = ? OR target_id = ?",
|
|
1952
|
+
(fact_id, fact_id),
|
|
1953
|
+
)
|
|
1954
|
+
except Exception as exc:
|
|
1955
|
+
logger.debug("delete_graph_edges_for_fact skipped for %s: %s", fact_id, exc)
|
|
1956
|
+
|
|
1957
|
+
def remove_fact_from_scenes(self, fact_id: str, profile_id: str) -> None:
|
|
1958
|
+
"""Remove a fact_id from every ``memory_scenes`` JSON array in the profile.
|
|
1959
|
+
|
|
1960
|
+
Scenes that become empty after removal are deleted entirely.
|
|
1961
|
+
Fail-soft: any exception is logged and ignored.
|
|
1962
|
+
"""
|
|
1963
|
+
try:
|
|
1964
|
+
scenes = self.get_scenes_for_fact(fact_id, profile_id)
|
|
1965
|
+
for scene in scenes:
|
|
1966
|
+
new_ids = [fid for fid in (scene.fact_ids or []) if fid != fact_id]
|
|
1967
|
+
if new_ids:
|
|
1968
|
+
self.execute(
|
|
1969
|
+
"UPDATE memory_scenes SET fact_ids_json = ? "
|
|
1970
|
+
"WHERE scene_id = ?",
|
|
1971
|
+
(json.dumps(new_ids), scene.scene_id),
|
|
1972
|
+
)
|
|
1973
|
+
else:
|
|
1974
|
+
self.execute(
|
|
1975
|
+
"DELETE FROM memory_scenes WHERE scene_id = ?",
|
|
1976
|
+
(scene.scene_id,),
|
|
1977
|
+
)
|
|
1978
|
+
except Exception as exc:
|
|
1979
|
+
logger.debug("remove_fact_from_scenes skipped for %s: %s", fact_id, exc)
|
|
1980
|
+
|
|
1981
|
+
def delete_memory_for_fact(self, fact_id: str, profile_id: str) -> None:
|
|
1982
|
+
"""Delete the raw ``memories`` record that sourced this fact.
|
|
1983
|
+
|
|
1984
|
+
Reads the ``memory_id`` from ``atomic_facts`` before the fact row is
|
|
1985
|
+
gone, then deletes the memory. Fail-soft: any exception is logged.
|
|
1986
|
+
"""
|
|
1987
|
+
try:
|
|
1988
|
+
rows = self.execute(
|
|
1989
|
+
"SELECT memory_id FROM atomic_facts "
|
|
1990
|
+
"WHERE fact_id = ? AND profile_id = ? LIMIT 1",
|
|
1991
|
+
(fact_id, profile_id),
|
|
1992
|
+
)
|
|
1993
|
+
if rows:
|
|
1994
|
+
memory_id = dict(rows[0]).get("memory_id") or ""
|
|
1995
|
+
if memory_id:
|
|
1996
|
+
self.execute(
|
|
1997
|
+
"DELETE FROM memories "
|
|
1998
|
+
"WHERE memory_id = ? AND profile_id = ?",
|
|
1999
|
+
(memory_id, profile_id),
|
|
2000
|
+
)
|
|
2001
|
+
except Exception as exc:
|
|
2002
|
+
logger.debug("delete_memory_for_fact skipped for %s: %s", fact_id, exc)
|
|
2003
|
+
|
|
1699
2004
|
# ------------------------------------------------------------------
|
|
1700
2005
|
# Phase 5: Core Memory Blocks CRUD (Rule 15)
|
|
1701
2006
|
# ------------------------------------------------------------------
|