superlocalmemory 4.0.4 → 4.0.5
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/CHANGELOG.md +33 -0
- package/README.md +18 -13
- package/ide/configs/codex-mcp.toml +2 -2
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.mcp.json +1 -0
- 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 +3 -2
- 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 +2 -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 +6 -5
- package/plugin-src/skills/slm-graph/SKILL.md +2 -1
- package/plugin-src/skills/slm-profile/SKILL.md +1 -0
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/brain/__init__.py +5 -0
- package/src/superlocalmemory/brain/truth.py +348 -0
- package/src/superlocalmemory/cli/commands.py +82 -25
- package/src/superlocalmemory/cli/main.py +12 -0
- package/src/superlocalmemory/core/context_cache.py +58 -1
- package/src/superlocalmemory/core/mutations.py +155 -25
- package/src/superlocalmemory/core/recall_pipeline.py +6 -10
- package/src/superlocalmemory/core/remember_runtime.py +271 -2
- package/src/superlocalmemory/core/store_pipeline.py +100 -38
- package/src/superlocalmemory/encoding/consolidator.py +17 -47
- package/src/superlocalmemory/encoding/temporal_validator.py +14 -18
- package/src/superlocalmemory/hooks/user_prompt_hook.py +1 -1
- package/src/superlocalmemory/integrations/bounded_loops_mcp.py +4 -3
- package/src/superlocalmemory/mcp/profiles.py +19 -7
- package/src/superlocalmemory/mcp/server.py +4 -2
- package/src/superlocalmemory/mcp/tools_brain.py +54 -10
- package/src/superlocalmemory/mcp/tools_core.py +88 -3
- package/src/superlocalmemory/retrieval/engine.py +7 -10
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +119 -19
- package/src/superlocalmemory/server/routes/brain.py +15 -0
- package/src/superlocalmemory/server/routes/memories.py +129 -3
- package/src/superlocalmemory/storage/_migration_internals.py +4 -0
- package/src/superlocalmemory/storage/_schema_version.py +2 -2
- package/src/superlocalmemory/storage/correction_cases.py +670 -0
- package/src/superlocalmemory/storage/database.py +194 -24
- package/src/superlocalmemory/storage/migration_runner.py +7 -0
- package/src/superlocalmemory/storage/migrations/M042_correction_case_ledger.py +245 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +2 -0
- package/src/superlocalmemory/storage/write_coordinator.py +4 -0
- package/src/superlocalmemory/ui/js/brain.js +43 -7
- package/src/superlocalmemory/ui/js/od-brain.js +44 -28
|
@@ -569,6 +569,62 @@ class DatabaseManager:
|
|
|
569
569
|
created_at=d["created_at"],
|
|
570
570
|
)
|
|
571
571
|
|
|
572
|
+
def insert_fact_immutable(self, fact: AtomicFact) -> str:
|
|
573
|
+
"""Insert one known-new fact without content deduplication or replacement.
|
|
574
|
+
|
|
575
|
+
Reviewed correction successors require a caller-chosen immutable
|
|
576
|
+
identity. Unlike normal remember ingestion, equal content must not
|
|
577
|
+
reinforce an existing row, and an occupied ID must abort the enclosing
|
|
578
|
+
transaction rather than overwrite history.
|
|
579
|
+
"""
|
|
580
|
+
scope = getattr(fact, "scope", None) or "personal"
|
|
581
|
+
shared = _jd(getattr(fact, "shared_with", None))
|
|
582
|
+
self.execute(
|
|
583
|
+
"""INSERT INTO atomic_facts
|
|
584
|
+
(fact_id, memory_id, profile_id, content, fact_type,
|
|
585
|
+
entities_json, canonical_entities_json,
|
|
586
|
+
observation_date, referenced_date, interval_start, interval_end,
|
|
587
|
+
confidence, importance, evidence_count, access_count,
|
|
588
|
+
source_turn_ids_json, session_id,
|
|
589
|
+
embedding, fisher_mean, fisher_variance,
|
|
590
|
+
lifecycle, langevin_position,
|
|
591
|
+
emotional_valence, emotional_arousal, signal_type, created_at,
|
|
592
|
+
scope, shared_with)
|
|
593
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
594
|
+
(
|
|
595
|
+
fact.fact_id,
|
|
596
|
+
fact.memory_id,
|
|
597
|
+
fact.profile_id,
|
|
598
|
+
fact.content,
|
|
599
|
+
fact.fact_type.value,
|
|
600
|
+
json.dumps(fact.entities),
|
|
601
|
+
json.dumps(fact.canonical_entities),
|
|
602
|
+
fact.observation_date,
|
|
603
|
+
fact.referenced_date,
|
|
604
|
+
fact.interval_start,
|
|
605
|
+
fact.interval_end,
|
|
606
|
+
fact.confidence,
|
|
607
|
+
fact.importance,
|
|
608
|
+
fact.evidence_count,
|
|
609
|
+
fact.access_count,
|
|
610
|
+
json.dumps(fact.source_turn_ids),
|
|
611
|
+
fact.session_id,
|
|
612
|
+
_jd(fact.embedding),
|
|
613
|
+
_jd(fact.fisher_mean),
|
|
614
|
+
_jd(fact.fisher_variance),
|
|
615
|
+
fact.lifecycle.value,
|
|
616
|
+
_jd(fact.langevin_position),
|
|
617
|
+
fact.emotional_valence,
|
|
618
|
+
fact.emotional_arousal,
|
|
619
|
+
fact.signal_type.value,
|
|
620
|
+
fact.created_at,
|
|
621
|
+
scope,
|
|
622
|
+
shared,
|
|
623
|
+
),
|
|
624
|
+
)
|
|
625
|
+
self.store_temporal_validity(fact.fact_id, fact.profile_id)
|
|
626
|
+
return fact.fact_id
|
|
627
|
+
|
|
572
628
|
def set_pinned(self, fact_id: str, pinned: bool) -> None:
|
|
573
629
|
"""Set or clear the pinned flag on a fact (v3.4.65 core-memory)."""
|
|
574
630
|
self.execute(
|
|
@@ -604,7 +660,23 @@ class DatabaseManager:
|
|
|
604
660
|
"ORDER BY importance DESC",
|
|
605
661
|
(*params,),
|
|
606
662
|
)
|
|
607
|
-
|
|
663
|
+
facts = [self._row_to_fact(r) for r in rows]
|
|
664
|
+
if not facts:
|
|
665
|
+
return facts
|
|
666
|
+
try:
|
|
667
|
+
blocked = self.get_nonapplied_correction_successor_ids(
|
|
668
|
+
[fact.fact_id for fact in facts],
|
|
669
|
+
profile_id,
|
|
670
|
+
include_global=include_global,
|
|
671
|
+
include_shared=include_shared,
|
|
672
|
+
)
|
|
673
|
+
except Exception as exc:
|
|
674
|
+
logger.warning("Pinned correction admission lookup failed: %s", exc)
|
|
675
|
+
return []
|
|
676
|
+
if not isinstance(blocked, set):
|
|
677
|
+
logger.warning("Pinned correction admission returned malformed data")
|
|
678
|
+
return []
|
|
679
|
+
return [fact for fact in facts if fact.fact_id not in blocked]
|
|
608
680
|
|
|
609
681
|
def _has_archive_status(self) -> bool:
|
|
610
682
|
"""Whether atomic_facts carries the M011 ``archive_status`` column.
|
|
@@ -1647,12 +1719,14 @@ class DatabaseManager:
|
|
|
1647
1719
|
def invalidate_fact_temporal(
|
|
1648
1720
|
self, fact_id: str, invalidated_by: str,
|
|
1649
1721
|
invalidation_reason: str,
|
|
1722
|
+
*,
|
|
1723
|
+
event_valid_until: str | None = None,
|
|
1650
1724
|
) -> None:
|
|
1651
1725
|
"""Mark a fact as invalidated, preserving bi-temporal independence.
|
|
1652
1726
|
|
|
1653
|
-
- valid_until (event-time):
|
|
1654
|
-
real
|
|
1655
|
-
|
|
1727
|
+
- valid_until (event-time): changed only when a reviewer supplies an
|
|
1728
|
+
independently validated real-world boundary. Review time, source
|
|
1729
|
+
timestamps, and a detector's conclusion are not a valid substitute.
|
|
1656
1730
|
- system_expired_at (transaction-time): when the system learned the
|
|
1657
1731
|
fact was invalid — always set to now.
|
|
1658
1732
|
|
|
@@ -1666,29 +1740,12 @@ class DatabaseManager:
|
|
|
1666
1740
|
from datetime import datetime as _dt
|
|
1667
1741
|
now = _dt.now(UTC).isoformat()
|
|
1668
1742
|
|
|
1669
|
-
# Resolve the event-time boundary from the fact's referenced_date.
|
|
1670
|
-
# Falls back to the current valid_until (which may already be set),
|
|
1671
|
-
# and ultimately to now if neither is available.
|
|
1672
|
-
fact_rows = self.execute(
|
|
1673
|
-
"SELECT referenced_date FROM atomic_facts WHERE fact_id = ?",
|
|
1674
|
-
(fact_id,),
|
|
1675
|
-
)
|
|
1676
|
-
referenced_date = dict(fact_rows[0]).get("referenced_date") if fact_rows else None
|
|
1677
|
-
|
|
1678
|
-
tv_rows = self.execute(
|
|
1679
|
-
"SELECT valid_until FROM fact_temporal_validity WHERE fact_id = ?",
|
|
1680
|
-
(fact_id,),
|
|
1681
|
-
)
|
|
1682
|
-
existing_valid_until = dict(tv_rows[0]).get("valid_until") if tv_rows else None
|
|
1683
|
-
|
|
1684
|
-
valid_until = referenced_date or existing_valid_until or now
|
|
1685
|
-
|
|
1686
1743
|
self.execute(
|
|
1687
1744
|
"UPDATE fact_temporal_validity "
|
|
1688
|
-
"SET valid_until = ?, system_expired_at = ?, "
|
|
1745
|
+
"SET valid_until = COALESCE(?, valid_until), system_expired_at = ?, "
|
|
1689
1746
|
" invalidated_by = ?, invalidation_reason = ? "
|
|
1690
|
-
"WHERE fact_id = ?",
|
|
1691
|
-
(
|
|
1747
|
+
"WHERE fact_id = ? AND system_expired_at IS NULL",
|
|
1748
|
+
(event_valid_until, now, invalidated_by, invalidation_reason, fact_id),
|
|
1692
1749
|
)
|
|
1693
1750
|
|
|
1694
1751
|
def get_valid_facts(self, profile_id: str) -> list[str]:
|
|
@@ -1804,6 +1861,119 @@ class DatabaseManager:
|
|
|
1804
1861
|
invalid.add(dict(r)["fact_id"])
|
|
1805
1862
|
return invalid
|
|
1806
1863
|
|
|
1864
|
+
def get_nonapplied_correction_successor_ids(
|
|
1865
|
+
self,
|
|
1866
|
+
fact_ids: list[str],
|
|
1867
|
+
profile_id: str,
|
|
1868
|
+
*,
|
|
1869
|
+
include_global: bool = False,
|
|
1870
|
+
include_shared: bool = False,
|
|
1871
|
+
) -> set[str]:
|
|
1872
|
+
"""Return candidate successors that are not current review truth.
|
|
1873
|
+
|
|
1874
|
+
M042 is optional for older databases. Its absence is safe because
|
|
1875
|
+
canonical proposal never commits a successor unless the same
|
|
1876
|
+
transaction also writes M042. Once present, a read failure must reach
|
|
1877
|
+
the retrieval fail-closed boundary rather than be converted to empty.
|
|
1878
|
+
"""
|
|
1879
|
+
if not fact_ids:
|
|
1880
|
+
return set()
|
|
1881
|
+
present = self.execute(
|
|
1882
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='correction_cases'"
|
|
1883
|
+
)
|
|
1884
|
+
if not present:
|
|
1885
|
+
return set()
|
|
1886
|
+
scope_where, scope_params = _scope_where(
|
|
1887
|
+
profile_id,
|
|
1888
|
+
include_global=include_global,
|
|
1889
|
+
include_shared=include_shared,
|
|
1890
|
+
prefix="f",
|
|
1891
|
+
)
|
|
1892
|
+
inadmissible: set[str] = set()
|
|
1893
|
+
for start in range(0, len(fact_ids), 900):
|
|
1894
|
+
batch = fact_ids[start:start + 900]
|
|
1895
|
+
placeholders = ",".join("?" for _ in batch)
|
|
1896
|
+
rows = self.execute(
|
|
1897
|
+
"SELECT c.successor_fact_id FROM correction_cases c "
|
|
1898
|
+
"JOIN atomic_facts f ON f.fact_id=c.successor_fact_id "
|
|
1899
|
+
f"WHERE c.successor_fact_id IN ({placeholders}) AND {scope_where} "
|
|
1900
|
+
"AND c.profile_id=f.profile_id "
|
|
1901
|
+
"AND c.status IN ('proposed', 'rejected', 'rolled_back')",
|
|
1902
|
+
(*batch, *scope_params),
|
|
1903
|
+
)
|
|
1904
|
+
inadmissible.update(str(row["successor_fact_id"]) for row in rows)
|
|
1905
|
+
return inadmissible
|
|
1906
|
+
|
|
1907
|
+
def get_correction_inadmissible_fact_ids(
|
|
1908
|
+
self,
|
|
1909
|
+
fact_ids: list[str],
|
|
1910
|
+
profile_id: str,
|
|
1911
|
+
as_of: str | None = None,
|
|
1912
|
+
*,
|
|
1913
|
+
include_global: bool = False,
|
|
1914
|
+
include_shared: bool = False,
|
|
1915
|
+
) -> set[str]:
|
|
1916
|
+
"""Return current-lifecycle exclusions with one bounded SQLite read.
|
|
1917
|
+
|
|
1918
|
+
Recall needs both sides of reviewed correction truth: an expired
|
|
1919
|
+
predecessor and a successor whose case is not applied. The older
|
|
1920
|
+
public helpers preserve their focused contracts, but invoking them
|
|
1921
|
+
consecutively opened two SQLite connections on every candidate stage.
|
|
1922
|
+
This read-model helper uses one connection and one UNION query while
|
|
1923
|
+
retaining the same profile/scope and historical ``as_of`` semantics.
|
|
1924
|
+
It is intentionally read-only and does not cache lifecycle state.
|
|
1925
|
+
"""
|
|
1926
|
+
if not fact_ids:
|
|
1927
|
+
return set()
|
|
1928
|
+
scope_where, scope_params = _scope_where(
|
|
1929
|
+
profile_id,
|
|
1930
|
+
include_global=include_global,
|
|
1931
|
+
include_shared=include_shared,
|
|
1932
|
+
prefix="f",
|
|
1933
|
+
)
|
|
1934
|
+
inadmissible: set[str] = set()
|
|
1935
|
+
with self.raw_connection() as conn:
|
|
1936
|
+
correction_table = conn.execute(
|
|
1937
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='correction_cases'"
|
|
1938
|
+
).fetchone()
|
|
1939
|
+
for start in range(0, len(fact_ids), 900):
|
|
1940
|
+
batch = fact_ids[start:start + 900]
|
|
1941
|
+
placeholders = ",".join("?" for _ in batch)
|
|
1942
|
+
temporal_conditions = (
|
|
1943
|
+
"AND tv.system_expired_at IS NOT NULL "
|
|
1944
|
+
+ ("AND tv.system_expired_at <= ?" if as_of is not None else "")
|
|
1945
|
+
)
|
|
1946
|
+
temporal_sql = (
|
|
1947
|
+
"SELECT tv.fact_id AS fact_id "
|
|
1948
|
+
"FROM fact_temporal_validity tv "
|
|
1949
|
+
"JOIN atomic_facts f ON f.fact_id=tv.fact_id "
|
|
1950
|
+
f"WHERE tv.fact_id IN ({placeholders}) AND {scope_where} "
|
|
1951
|
+
"AND tv.profile_id=f.profile_id "
|
|
1952
|
+
f"{temporal_conditions}"
|
|
1953
|
+
)
|
|
1954
|
+
temporal_params: tuple[Any, ...] = (
|
|
1955
|
+
*batch,
|
|
1956
|
+
*scope_params,
|
|
1957
|
+
*((as_of,) if as_of is not None else ()),
|
|
1958
|
+
)
|
|
1959
|
+
if correction_table is None:
|
|
1960
|
+
rows = conn.execute(temporal_sql, temporal_params).fetchall()
|
|
1961
|
+
else:
|
|
1962
|
+
correction_sql = (
|
|
1963
|
+
"SELECT c.successor_fact_id AS fact_id "
|
|
1964
|
+
"FROM correction_cases c "
|
|
1965
|
+
"JOIN atomic_facts f ON f.fact_id=c.successor_fact_id "
|
|
1966
|
+
f"WHERE c.successor_fact_id IN ({placeholders}) AND {scope_where} "
|
|
1967
|
+
"AND c.profile_id=f.profile_id "
|
|
1968
|
+
"AND c.status IN ('proposed', 'rejected', 'rolled_back')"
|
|
1969
|
+
)
|
|
1970
|
+
rows = conn.execute(
|
|
1971
|
+
f"{temporal_sql} UNION {correction_sql}",
|
|
1972
|
+
(*temporal_params, *batch, *scope_params),
|
|
1973
|
+
).fetchall()
|
|
1974
|
+
inadmissible.update(str(row["fact_id"]) for row in rows)
|
|
1975
|
+
return inadmissible
|
|
1976
|
+
|
|
1807
1977
|
def get_strict_temporal_inadmissible_fact_ids(
|
|
1808
1978
|
self,
|
|
1809
1979
|
fact_ids: list[str],
|
|
@@ -157,6 +157,9 @@ from superlocalmemory.storage.migrations import (
|
|
|
157
157
|
from superlocalmemory.storage.migrations import (
|
|
158
158
|
M041_external_evidence_receipts as _M041,
|
|
159
159
|
)
|
|
160
|
+
from superlocalmemory.storage.migrations import (
|
|
161
|
+
M042_correction_case_ledger as _M042,
|
|
162
|
+
)
|
|
160
163
|
from superlocalmemory.storage._schema_version import (
|
|
161
164
|
SUPPORTED_SCHEMA_VERSION,
|
|
162
165
|
SchemaVersionError,
|
|
@@ -239,6 +242,10 @@ MIGRATIONS: list[Migration] = [
|
|
|
239
242
|
dependencies=(_M003.NAME,)),
|
|
240
243
|
Migration(name=_M041.NAME, db_target="learning", ddl=_M041.DDL,
|
|
241
244
|
dependencies=(_M040.NAME,)),
|
|
245
|
+
# Review-gated correction metadata is self-contained in memory.db. It
|
|
246
|
+
# contains identifiers only and does not alter temporal fact state.
|
|
247
|
+
Migration(name=_M042.NAME, db_target="memory", ddl=_M042.DDL,
|
|
248
|
+
dependencies=(_M032.NAME,)),
|
|
242
249
|
# M006 + M011 are deliberately NOT here — see DEFERRED_MIGRATIONS below.
|
|
243
250
|
]
|
|
244
251
|
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""M042 — review-gated correction cases in ``memory.db``.
|
|
2
|
+
|
|
3
|
+
The ledger contains identifiers and lifecycle metadata only. It never stores
|
|
4
|
+
fact text. The saved predecessor temporal tuple lets a reviewed rollback
|
|
5
|
+
restore the exact lifecycle state without deleting fact history.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sqlite3
|
|
11
|
+
|
|
12
|
+
NAME = "M042_correction_case_ledger"
|
|
13
|
+
DB_TARGET = "memory"
|
|
14
|
+
|
|
15
|
+
DDL = """
|
|
16
|
+
BEGIN IMMEDIATE;
|
|
17
|
+
CREATE TABLE IF NOT EXISTS correction_cases (
|
|
18
|
+
case_id TEXT PRIMARY KEY,
|
|
19
|
+
profile_id TEXT NOT NULL,
|
|
20
|
+
scope TEXT NOT NULL CHECK (scope IN ('personal', 'project', 'shared', 'global')),
|
|
21
|
+
predecessor_fact_id TEXT NOT NULL,
|
|
22
|
+
successor_fact_id TEXT NOT NULL,
|
|
23
|
+
reason_code TEXT NOT NULL,
|
|
24
|
+
status TEXT NOT NULL CHECK (status IN ('proposed', 'applied', 'rejected', 'rolled_back')),
|
|
25
|
+
version INTEGER NOT NULL CHECK (version >= 0),
|
|
26
|
+
idempotency_key TEXT NOT NULL,
|
|
27
|
+
proposed_by_actor_id TEXT NOT NULL,
|
|
28
|
+
proposed_by_actor_kind TEXT NOT NULL,
|
|
29
|
+
proposed_by_trust_tier TEXT NOT NULL,
|
|
30
|
+
created_at TEXT NOT NULL,
|
|
31
|
+
updated_at TEXT NOT NULL,
|
|
32
|
+
reviewed_by_actor_id TEXT,
|
|
33
|
+
reviewed_at TEXT,
|
|
34
|
+
applied_at TEXT,
|
|
35
|
+
system_effective_at TEXT,
|
|
36
|
+
event_valid_from TEXT,
|
|
37
|
+
event_valid_until TEXT,
|
|
38
|
+
predecessor_temporal_existed INTEGER,
|
|
39
|
+
predecessor_valid_from TEXT,
|
|
40
|
+
predecessor_valid_until TEXT,
|
|
41
|
+
predecessor_system_created_at TEXT,
|
|
42
|
+
predecessor_system_expired_at TEXT,
|
|
43
|
+
predecessor_invalidated_by TEXT,
|
|
44
|
+
predecessor_invalidation_reason TEXT,
|
|
45
|
+
UNIQUE (profile_id, idempotency_key),
|
|
46
|
+
FOREIGN KEY (predecessor_fact_id) REFERENCES atomic_facts(fact_id) ON DELETE RESTRICT,
|
|
47
|
+
FOREIGN KEY (successor_fact_id) REFERENCES atomic_facts(fact_id) ON DELETE RESTRICT
|
|
48
|
+
);
|
|
49
|
+
CREATE TABLE IF NOT EXISTS correction_events (
|
|
50
|
+
event_id TEXT PRIMARY KEY,
|
|
51
|
+
case_id TEXT NOT NULL,
|
|
52
|
+
profile_id TEXT NOT NULL,
|
|
53
|
+
scope TEXT NOT NULL CHECK (scope IN ('personal', 'project', 'shared', 'global')),
|
|
54
|
+
event_type TEXT NOT NULL
|
|
55
|
+
CHECK (event_type IN ('proposed', 'applied', 'rejected', 'rolled_back')),
|
|
56
|
+
operation_id TEXT NOT NULL,
|
|
57
|
+
actor_id TEXT NOT NULL,
|
|
58
|
+
actor_kind TEXT NOT NULL,
|
|
59
|
+
actor_trust_tier TEXT NOT NULL,
|
|
60
|
+
expected_version INTEGER,
|
|
61
|
+
resulting_version INTEGER NOT NULL CHECK (resulting_version >= 0),
|
|
62
|
+
system_occurred_at TEXT NOT NULL,
|
|
63
|
+
event_valid_from TEXT,
|
|
64
|
+
event_valid_until TEXT,
|
|
65
|
+
UNIQUE (case_id, operation_id),
|
|
66
|
+
FOREIGN KEY (case_id) REFERENCES correction_cases(case_id) ON DELETE RESTRICT
|
|
67
|
+
);
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_correction_cases_profile_status
|
|
69
|
+
ON correction_cases (profile_id, status, updated_at DESC);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_correction_events_case_sequence
|
|
71
|
+
ON correction_events (case_id, system_occurred_at ASC);
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_correction_cases_successor_admission
|
|
73
|
+
ON correction_cases (profile_id, successor_fact_id, status);
|
|
74
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_correction_cases_active_predecessor
|
|
75
|
+
ON correction_cases (profile_id, predecessor_fact_id)
|
|
76
|
+
WHERE status IN ('proposed', 'applied');
|
|
77
|
+
COMMIT;
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
_TABLES = frozenset({"correction_cases", "correction_events"})
|
|
81
|
+
_FORBIDDEN_RAW_COLUMNS = frozenset({"content", "fact_text", "raw_text", "query"})
|
|
82
|
+
_REQUIRED_CASE_COLUMNS = frozenset(
|
|
83
|
+
{
|
|
84
|
+
"case_id",
|
|
85
|
+
"profile_id",
|
|
86
|
+
"scope",
|
|
87
|
+
"predecessor_fact_id",
|
|
88
|
+
"successor_fact_id",
|
|
89
|
+
"reason_code",
|
|
90
|
+
"status",
|
|
91
|
+
"version",
|
|
92
|
+
"idempotency_key",
|
|
93
|
+
"proposed_by_actor_id",
|
|
94
|
+
"proposed_by_actor_kind",
|
|
95
|
+
"proposed_by_trust_tier",
|
|
96
|
+
"created_at",
|
|
97
|
+
"updated_at",
|
|
98
|
+
"reviewed_by_actor_id",
|
|
99
|
+
"reviewed_at",
|
|
100
|
+
"applied_at",
|
|
101
|
+
"system_effective_at",
|
|
102
|
+
"event_valid_from",
|
|
103
|
+
"event_valid_until",
|
|
104
|
+
"predecessor_temporal_existed",
|
|
105
|
+
"predecessor_valid_from",
|
|
106
|
+
"predecessor_valid_until",
|
|
107
|
+
"predecessor_system_created_at",
|
|
108
|
+
"predecessor_system_expired_at",
|
|
109
|
+
"predecessor_invalidated_by",
|
|
110
|
+
"predecessor_invalidation_reason",
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
_REQUIRED_EVENT_COLUMNS = frozenset(
|
|
114
|
+
{
|
|
115
|
+
"event_id",
|
|
116
|
+
"case_id",
|
|
117
|
+
"profile_id",
|
|
118
|
+
"scope",
|
|
119
|
+
"event_type",
|
|
120
|
+
"operation_id",
|
|
121
|
+
"actor_id",
|
|
122
|
+
"actor_kind",
|
|
123
|
+
"actor_trust_tier",
|
|
124
|
+
"expected_version",
|
|
125
|
+
"resulting_version",
|
|
126
|
+
"system_occurred_at",
|
|
127
|
+
"event_valid_from",
|
|
128
|
+
"event_valid_until",
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
_INDEX_SPECS = {
|
|
132
|
+
"idx_correction_cases_profile_status": (
|
|
133
|
+
"correction_cases",
|
|
134
|
+
("profile_id", "status", "updated_at"),
|
|
135
|
+
False,
|
|
136
|
+
None,
|
|
137
|
+
),
|
|
138
|
+
"idx_correction_events_case_sequence": (
|
|
139
|
+
"correction_events",
|
|
140
|
+
("case_id", "system_occurred_at"),
|
|
141
|
+
False,
|
|
142
|
+
None,
|
|
143
|
+
),
|
|
144
|
+
"idx_correction_cases_successor_admission": (
|
|
145
|
+
"correction_cases",
|
|
146
|
+
("profile_id", "successor_fact_id", "status"),
|
|
147
|
+
False,
|
|
148
|
+
None,
|
|
149
|
+
),
|
|
150
|
+
"uq_correction_cases_active_predecessor": (
|
|
151
|
+
"correction_cases",
|
|
152
|
+
("profile_id", "predecessor_fact_id"),
|
|
153
|
+
True,
|
|
154
|
+
"wherestatusin('proposed','applied')",
|
|
155
|
+
),
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def apply(conn: sqlite3.Connection) -> None:
|
|
160
|
+
"""Install the additive review ledger atomically and idempotently."""
|
|
161
|
+
if any(_table_exists(conn, table) for table in _TABLES) and not verify(conn):
|
|
162
|
+
raise sqlite3.OperationalError("M042 correction ledger is malformed; refusing rebuild")
|
|
163
|
+
conn.executescript(DDL)
|
|
164
|
+
if not verify(conn):
|
|
165
|
+
raise sqlite3.OperationalError("M042 correction ledger did not reach its end-state")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def verify(conn: sqlite3.Connection) -> bool:
|
|
169
|
+
if not all(_table_exists(conn, table) for table in _TABLES):
|
|
170
|
+
return False
|
|
171
|
+
case_columns = _columns(conn, "correction_cases")
|
|
172
|
+
event_columns = _columns(conn, "correction_events")
|
|
173
|
+
if (
|
|
174
|
+
set(case_columns) != _REQUIRED_CASE_COLUMNS
|
|
175
|
+
or set(event_columns) != _REQUIRED_EVENT_COLUMNS
|
|
176
|
+
or _FORBIDDEN_RAW_COLUMNS & (set(case_columns) | set(event_columns))
|
|
177
|
+
):
|
|
178
|
+
return False
|
|
179
|
+
if not _required_checks_present(conn):
|
|
180
|
+
return False
|
|
181
|
+
return all(_index_matches(conn, name, *spec) for name, spec in _INDEX_SPECS.items())
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _table_exists(conn: sqlite3.Connection, table: str) -> bool:
|
|
185
|
+
return (
|
|
186
|
+
conn.execute(
|
|
187
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (table,)
|
|
188
|
+
).fetchone()
|
|
189
|
+
is not None
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _columns(conn: sqlite3.Connection, table: str) -> tuple[str, ...]:
|
|
194
|
+
return tuple(row[1] for row in conn.execute(f"PRAGMA table_info({table})"))
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _required_checks_present(conn: sqlite3.Connection) -> bool:
|
|
198
|
+
statements: dict[str, str] = {}
|
|
199
|
+
for table in ("correction_cases", "correction_events"):
|
|
200
|
+
row = conn.execute(
|
|
201
|
+
"SELECT sql FROM sqlite_master WHERE type='table' AND name=?", (table,)
|
|
202
|
+
).fetchone()
|
|
203
|
+
if row is None or row[0] is None:
|
|
204
|
+
return False
|
|
205
|
+
statements[table] = "".join(str(row[0]).lower().split())
|
|
206
|
+
case_sql = statements["correction_cases"]
|
|
207
|
+
event_sql = statements["correction_events"]
|
|
208
|
+
scope_check = "check(scopein('personal','project','shared','global'))"
|
|
209
|
+
return (
|
|
210
|
+
scope_check in case_sql
|
|
211
|
+
and scope_check in event_sql
|
|
212
|
+
and "check(statusin('proposed','applied','rejected','rolled_back'))" in case_sql
|
|
213
|
+
and "check(event_typein('proposed','applied','rejected','rolled_back'))" in event_sql
|
|
214
|
+
and "unique(profile_id,idempotency_key)" in case_sql
|
|
215
|
+
and "unique(case_id,operation_id)" in event_sql
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _index_matches(
|
|
220
|
+
conn: sqlite3.Connection,
|
|
221
|
+
name: str,
|
|
222
|
+
table: str,
|
|
223
|
+
columns: tuple[str, ...],
|
|
224
|
+
unique: bool,
|
|
225
|
+
where_clause: str | None,
|
|
226
|
+
) -> bool:
|
|
227
|
+
"""Verify an index's table, ordered columns, uniqueness, and partial predicate."""
|
|
228
|
+
row = conn.execute(
|
|
229
|
+
"SELECT tbl_name, sql FROM sqlite_master WHERE type='index' AND name=?", (name,)
|
|
230
|
+
).fetchone()
|
|
231
|
+
if row is None or row[0] != table:
|
|
232
|
+
return False
|
|
233
|
+
index_rows = conn.execute(f"PRAGMA index_xinfo({name})").fetchall()
|
|
234
|
+
indexed_columns = tuple(
|
|
235
|
+
entry[2] for entry in index_rows if entry[5] == 1 and entry[2] is not None
|
|
236
|
+
)
|
|
237
|
+
if indexed_columns != columns:
|
|
238
|
+
return False
|
|
239
|
+
index_list = conn.execute(f"PRAGMA index_list({table})").fetchall()
|
|
240
|
+
indexed = next((entry for entry in index_list if entry[1] == name), None)
|
|
241
|
+
if indexed is None or bool(indexed[2]) is not unique:
|
|
242
|
+
return False
|
|
243
|
+
if where_clause is None:
|
|
244
|
+
return True
|
|
245
|
+
return row[1] is not None and where_clause in "".join(str(row[1]).lower().split())
|
|
@@ -32,6 +32,7 @@ from . import (
|
|
|
32
32
|
M039_scene_fact_members,
|
|
33
33
|
M040_agent_experience_receipts,
|
|
34
34
|
M041_external_evidence_receipts,
|
|
35
|
+
M042_correction_case_ledger,
|
|
35
36
|
)
|
|
36
37
|
|
|
37
38
|
# ---------------------------------------------------------------------------
|
|
@@ -89,6 +90,7 @@ __all__ = (
|
|
|
89
90
|
"M039_scene_fact_members",
|
|
90
91
|
"M040_agent_experience_receipts",
|
|
91
92
|
"M041_external_evidence_receipts",
|
|
93
|
+
"M042_correction_case_ledger",
|
|
92
94
|
# Legacy re-exports (backward compat):
|
|
93
95
|
"CURRENT_SCHEMA_VERSION",
|
|
94
96
|
"get_schema_version",
|
|
@@ -96,6 +96,10 @@ class CommandKind(StrEnum):
|
|
|
96
96
|
ADMISSION = "admission"
|
|
97
97
|
DELETE_FACT = "delete_fact"
|
|
98
98
|
UPDATE_FACT = "update_fact"
|
|
99
|
+
PROPOSE_CORRECTION = "propose_correction"
|
|
100
|
+
APPLY_CORRECTION = "apply_correction"
|
|
101
|
+
REJECT_CORRECTION = "reject_correction"
|
|
102
|
+
ROLLBACK_CORRECTION = "rollback_correction"
|
|
99
103
|
ARCHIVE_FACT = "archive_fact"
|
|
100
104
|
MERGE_FACT = "merge_fact"
|
|
101
105
|
SET_FACT_SCOPE = "set_fact_scope"
|
|
@@ -1135,7 +1135,15 @@
|
|
|
1135
1135
|
// --------------------------------------------------------------------
|
|
1136
1136
|
function cardLivingBrain(snapshot) {
|
|
1137
1137
|
const data = snapshot || {};
|
|
1138
|
-
|
|
1138
|
+
// v4.0.5: BrainTruth is the canonical, unavailable-aware read model.
|
|
1139
|
+
// Keep legacy fields below as fallbacks while old daemon versions remain
|
|
1140
|
+
// in use during a rolling local upgrade.
|
|
1141
|
+
const truth = data.brain_truth || {};
|
|
1142
|
+
const memoryActivity = truth.memory_activity || {};
|
|
1143
|
+
const feedback = truth.feedback || data.feedback || {};
|
|
1144
|
+
const experience = truth.agent_experience || {};
|
|
1145
|
+
const externalEvidence = truth.external_evidence || {};
|
|
1146
|
+
const correctionQuality = truth.correction_quality || {};
|
|
1139
1147
|
const clients = (data.connected_clients || {}).clients || [];
|
|
1140
1148
|
const quality = data.source_quality || {};
|
|
1141
1149
|
const graph = data.graph || {};
|
|
@@ -1143,9 +1151,18 @@
|
|
|
1143
1151
|
wrap.appendChild(EL('h4', {text: 'Living Brain'}));
|
|
1144
1152
|
wrap.appendChild(EL('p', {
|
|
1145
1153
|
className: 'brain-help',
|
|
1146
|
-
text: 'A local evidence view of
|
|
1154
|
+
text: 'A local evidence view of memory activity, feedback, and reviewed quality. Evidence and observations do not change recall, ranking, or model routing.',
|
|
1147
1155
|
}));
|
|
1148
1156
|
|
|
1157
|
+
function countOrUnavailable(section, key, label) {
|
|
1158
|
+
if (!section || section.availability === 'unavailable') {
|
|
1159
|
+
const reason = section && section.reason ? ': ' + section.reason : '';
|
|
1160
|
+
return 'Unavailable' + reason;
|
|
1161
|
+
}
|
|
1162
|
+
const value = section[key];
|
|
1163
|
+
return value == null ? 'No data yet' : String(value) + (label ? ' ' + label : '');
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1149
1166
|
const clientText = clients.length
|
|
1150
1167
|
? clients.map((client) => {
|
|
1151
1168
|
const seconds = Number(client.last_seen_seconds_ago || 0);
|
|
@@ -1153,10 +1170,29 @@
|
|
|
1153
1170
|
}).join(' · ')
|
|
1154
1171
|
: 'No client activity in the last 5 minutes';
|
|
1155
1172
|
const grid = EL('div', {className: 'brain-stat-grid'});
|
|
1173
|
+
grid.appendChild(statRow(
|
|
1174
|
+
'Control plane',
|
|
1175
|
+
truth.control_plane === 'observation_only' ? 'Observation only' : 'Legacy view',
|
|
1176
|
+
));
|
|
1156
1177
|
grid.appendChild(statRow('Recent clients', clientText));
|
|
1157
|
-
grid.appendChild(statRow('
|
|
1158
|
-
|
|
1159
|
-
|
|
1178
|
+
grid.appendChild(statRow('Memory activity', countOrUnavailable(
|
|
1179
|
+
memoryActivity, 'facts_total', 'facts',
|
|
1180
|
+
)));
|
|
1181
|
+
grid.appendChild(statRow('Feedback signals', countOrUnavailable(
|
|
1182
|
+
feedback, 'signals_total', 'signals',
|
|
1183
|
+
)));
|
|
1184
|
+
grid.appendChild(statRow('Claimed evidence', countOrUnavailable(
|
|
1185
|
+
experience, 'claimed_experiences_total', 'receipts',
|
|
1186
|
+
)));
|
|
1187
|
+
grid.appendChild(statRow('Independently verified evidence', countOrUnavailable(
|
|
1188
|
+
experience, 'independently_verified_experiences_total', 'receipts',
|
|
1189
|
+
)));
|
|
1190
|
+
grid.appendChild(statRow('External observations', countOrUnavailable(
|
|
1191
|
+
externalEvidence, 'receipts_total', 'receipts',
|
|
1192
|
+
)));
|
|
1193
|
+
grid.appendChild(statRow('Correction quality', countOrUnavailable(
|
|
1194
|
+
correctionQuality, 'cases_total', 'review cases',
|
|
1195
|
+
)));
|
|
1160
1196
|
grid.appendChild(statRow(
|
|
1161
1197
|
'Observed source quality',
|
|
1162
1198
|
quality.mean_quality == null ? 'No evidence yet' : Number(quality.mean_quality).toFixed(3),
|
|
@@ -1175,8 +1211,8 @@
|
|
|
1175
1211
|
: 'No feedback signals recorded yet. Report outcomes or use memory feedback to start the loop.',
|
|
1176
1212
|
}));
|
|
1177
1213
|
wrap.appendChild(badge(
|
|
1178
|
-
data.is_real ? 'real' : 'stub',
|
|
1179
|
-
data.source || 'local evidence',
|
|
1214
|
+
truth.contract ? 'real' : (data.is_real ? 'real' : 'stub'),
|
|
1215
|
+
truth.contract || data.source || 'local evidence',
|
|
1180
1216
|
));
|
|
1181
1217
|
return wrap;
|
|
1182
1218
|
}
|