superlocalmemory 4.0.3 → 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 +55 -0
- package/README.md +19 -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 +5 -4
- 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 +7 -6
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-governance/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +3 -2
- package/plugin-src/skills/slm-loop/SKILL.md +1 -1
- package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
- package/plugin-src/skills/slm-profile/SKILL.md +5 -4
- 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 +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/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 +185 -0
- package/src/superlocalmemory/learning/database.py +2 -1
- package/src/superlocalmemory/mcp/profiles.py +25 -7
- package/src/superlocalmemory/mcp/server.py +7 -2
- package/src/superlocalmemory/mcp/tools_brain.py +138 -9
- 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 +21 -1
- package/src/superlocalmemory/server/routes/memories.py +129 -3
- package/src/superlocalmemory/storage/_migration_internals.py +8 -0
- package/src/superlocalmemory/storage/_schema_version.py +2 -2
- package/src/superlocalmemory/storage/agent_experience.py +26 -4
- package/src/superlocalmemory/storage/correction_cases.py +670 -0
- package/src/superlocalmemory/storage/database.py +194 -24
- package/src/superlocalmemory/storage/external_evidence.py +359 -0
- package/src/superlocalmemory/storage/migration_runner.py +12 -0
- package/src/superlocalmemory/storage/migrations/M041_external_evidence_receipts.py +189 -0
- package/src/superlocalmemory/storage/migrations/M042_correction_case_ledger.py +245 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -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 -19
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""M041 — typed, observation-only external evidence in ``learning.db``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sqlite3
|
|
6
|
+
|
|
7
|
+
NAME = "M041_external_evidence_receipts"
|
|
8
|
+
DB_TARGET = "learning"
|
|
9
|
+
|
|
10
|
+
DDL = """
|
|
11
|
+
BEGIN IMMEDIATE;
|
|
12
|
+
CREATE TABLE IF NOT EXISTS external_evidence_receipts (
|
|
13
|
+
profile_id TEXT NOT NULL,
|
|
14
|
+
contract_id TEXT NOT NULL,
|
|
15
|
+
workspace_id TEXT NOT NULL,
|
|
16
|
+
run_ref TEXT NOT NULL,
|
|
17
|
+
run_id TEXT NOT NULL,
|
|
18
|
+
outcome TEXT NOT NULL,
|
|
19
|
+
run_state TEXT NOT NULL,
|
|
20
|
+
demonstration INTEGER NOT NULL CHECK (demonstration IN (0, 1)),
|
|
21
|
+
eligible_for_learning INTEGER NOT NULL CHECK (eligible_for_learning IN (0, 1)),
|
|
22
|
+
terminal_at TEXT NOT NULL,
|
|
23
|
+
graph_digest TEXT NOT NULL,
|
|
24
|
+
plan_digest TEXT NOT NULL,
|
|
25
|
+
policy_digest TEXT NOT NULL,
|
|
26
|
+
receipt_sequence INTEGER NOT NULL,
|
|
27
|
+
receipt_head_digest TEXT NOT NULL,
|
|
28
|
+
receipt_trust TEXT NOT NULL,
|
|
29
|
+
nodes_json TEXT NOT NULL,
|
|
30
|
+
artifact_digests_json TEXT NOT NULL,
|
|
31
|
+
payload_sha256 TEXT NOT NULL,
|
|
32
|
+
observed_at TEXT NOT NULL,
|
|
33
|
+
PRIMARY KEY (profile_id, contract_id, workspace_id, run_ref)
|
|
34
|
+
);
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_external_evidence_profile_terminal
|
|
36
|
+
ON external_evidence_receipts (profile_id, terminal_at DESC);
|
|
37
|
+
CREATE INDEX IF NOT EXISTS idx_external_evidence_profile_workspace
|
|
38
|
+
ON external_evidence_receipts (profile_id, workspace_id, terminal_at DESC);
|
|
39
|
+
COMMIT;
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
_TABLE = "external_evidence_receipts"
|
|
43
|
+
_COLUMNS = (
|
|
44
|
+
"profile_id",
|
|
45
|
+
"contract_id",
|
|
46
|
+
"workspace_id",
|
|
47
|
+
"run_ref",
|
|
48
|
+
"run_id",
|
|
49
|
+
"outcome",
|
|
50
|
+
"run_state",
|
|
51
|
+
"demonstration",
|
|
52
|
+
"eligible_for_learning",
|
|
53
|
+
"terminal_at",
|
|
54
|
+
"graph_digest",
|
|
55
|
+
"plan_digest",
|
|
56
|
+
"policy_digest",
|
|
57
|
+
"receipt_sequence",
|
|
58
|
+
"receipt_head_digest",
|
|
59
|
+
"receipt_trust",
|
|
60
|
+
"nodes_json",
|
|
61
|
+
"artifact_digests_json",
|
|
62
|
+
"payload_sha256",
|
|
63
|
+
"observed_at",
|
|
64
|
+
)
|
|
65
|
+
_TYPES = (
|
|
66
|
+
"TEXT",
|
|
67
|
+
"TEXT",
|
|
68
|
+
"TEXT",
|
|
69
|
+
"TEXT",
|
|
70
|
+
"TEXT",
|
|
71
|
+
"TEXT",
|
|
72
|
+
"TEXT",
|
|
73
|
+
"INTEGER",
|
|
74
|
+
"INTEGER",
|
|
75
|
+
"TEXT",
|
|
76
|
+
"TEXT",
|
|
77
|
+
"TEXT",
|
|
78
|
+
"TEXT",
|
|
79
|
+
"INTEGER",
|
|
80
|
+
"TEXT",
|
|
81
|
+
"TEXT",
|
|
82
|
+
"TEXT",
|
|
83
|
+
"TEXT",
|
|
84
|
+
"TEXT",
|
|
85
|
+
"TEXT",
|
|
86
|
+
)
|
|
87
|
+
_PRIMARY_KEY = ("profile_id", "contract_id", "workspace_id", "run_ref")
|
|
88
|
+
_INDEXES = {
|
|
89
|
+
"idx_external_evidence_profile_terminal": (
|
|
90
|
+
"external_evidence_receipts", (("profile_id", False), ("terminal_at", True)),
|
|
91
|
+
),
|
|
92
|
+
"idx_external_evidence_profile_workspace": (
|
|
93
|
+
"external_evidence_receipts",
|
|
94
|
+
(("profile_id", False), ("workspace_id", False), ("terminal_at", True)),
|
|
95
|
+
),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def apply(conn: sqlite3.Connection) -> None:
|
|
100
|
+
"""Install the additive receipt table atomically and idempotently."""
|
|
101
|
+
if _table_exists(conn) and not _table_is_valid(conn):
|
|
102
|
+
raise sqlite3.OperationalError(
|
|
103
|
+
"M041 external evidence table is malformed; refusing rebuild"
|
|
104
|
+
)
|
|
105
|
+
conn.executescript(DDL)
|
|
106
|
+
if not verify(conn):
|
|
107
|
+
repair(conn)
|
|
108
|
+
if not verify(conn):
|
|
109
|
+
raise sqlite3.OperationalError("M041 external evidence schema did not reach its end-state")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def repair(conn: sqlite3.Connection) -> None:
|
|
113
|
+
"""Restore only M041's derived indexes without touching stored evidence."""
|
|
114
|
+
if _table_exists(conn) and not _table_is_valid(conn):
|
|
115
|
+
raise sqlite3.OperationalError(
|
|
116
|
+
"M041 external evidence table is malformed; refusing rebuild"
|
|
117
|
+
)
|
|
118
|
+
if not _table_exists(conn):
|
|
119
|
+
apply(conn)
|
|
120
|
+
return
|
|
121
|
+
drops = "\n".join(f"DROP INDEX IF EXISTS {name};" for name in _INDEXES)
|
|
122
|
+
creates = "\n".join(
|
|
123
|
+
f"CREATE INDEX {name} ON {table} ({_index_sql_columns(columns)});"
|
|
124
|
+
for name, (table, columns) in _INDEXES.items()
|
|
125
|
+
)
|
|
126
|
+
conn.executescript(f"BEGIN IMMEDIATE;\n{drops}\n{creates}\nCOMMIT;")
|
|
127
|
+
if not verify(conn):
|
|
128
|
+
raise sqlite3.OperationalError("M041 index repair did not restore required end-state")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def verify(conn: sqlite3.Connection) -> bool:
|
|
132
|
+
if not _table_is_valid(conn):
|
|
133
|
+
return False
|
|
134
|
+
for name, (table, columns) in _INDEXES.items():
|
|
135
|
+
row = conn.execute(
|
|
136
|
+
"SELECT tbl_name FROM sqlite_master WHERE type='index' AND name=?", (name,)
|
|
137
|
+
).fetchone()
|
|
138
|
+
if row is None or row[0] != table:
|
|
139
|
+
return False
|
|
140
|
+
actual = tuple(
|
|
141
|
+
(item[2], bool(item[3]))
|
|
142
|
+
for item in conn.execute(f"PRAGMA index_xinfo({name})")
|
|
143
|
+
if item[5] and item[2] is not None
|
|
144
|
+
)
|
|
145
|
+
if actual != columns:
|
|
146
|
+
return False
|
|
147
|
+
return True
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _table_exists(conn: sqlite3.Connection) -> bool:
|
|
151
|
+
return (
|
|
152
|
+
conn.execute(
|
|
153
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (_TABLE,)
|
|
154
|
+
).fetchone()
|
|
155
|
+
is not None
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _table_is_valid(conn: sqlite3.Connection) -> bool:
|
|
160
|
+
if not _table_exists(conn):
|
|
161
|
+
return False
|
|
162
|
+
info = conn.execute(f"PRAGMA table_info({_TABLE})").fetchall()
|
|
163
|
+
columns = tuple(row[1] for row in info)
|
|
164
|
+
types = tuple(str(row[2]).upper() for row in info)
|
|
165
|
+
primary_key = tuple(row[1] for row in sorted(info, key=lambda row: row[5]) if row[5])
|
|
166
|
+
not_null = {row[1] for row in info if row[3] or row[5]}
|
|
167
|
+
return (
|
|
168
|
+
columns == _COLUMNS
|
|
169
|
+
and types == _TYPES
|
|
170
|
+
and primary_key == _PRIMARY_KEY
|
|
171
|
+
and not_null == set(_COLUMNS)
|
|
172
|
+
and conn.execute(f"PRAGMA foreign_key_list({_TABLE})").fetchone() is None
|
|
173
|
+
and _required_checks_present(conn)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _required_checks_present(conn: sqlite3.Connection) -> bool:
|
|
178
|
+
row = conn.execute(
|
|
179
|
+
"SELECT sql FROM sqlite_master WHERE type='table' AND name=?", (_TABLE,)
|
|
180
|
+
).fetchone()
|
|
181
|
+
sql = "" if row is None or row[0] is None else "".join(str(row[0]).lower().split())
|
|
182
|
+
return (
|
|
183
|
+
"check(demonstrationin(0,1))" in sql
|
|
184
|
+
and "check(eligible_for_learningin(0,1))" in sql
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _index_sql_columns(columns: tuple[tuple[str, bool], ...]) -> str:
|
|
189
|
+
return ", ".join(column + (" DESC" if desc else "") for column, desc in columns)
|
|
@@ -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())
|
|
@@ -31,6 +31,8 @@ from . import (
|
|
|
31
31
|
M038_learning_feedback_channel,
|
|
32
32
|
M039_scene_fact_members,
|
|
33
33
|
M040_agent_experience_receipts,
|
|
34
|
+
M041_external_evidence_receipts,
|
|
35
|
+
M042_correction_case_ledger,
|
|
34
36
|
)
|
|
35
37
|
|
|
36
38
|
# ---------------------------------------------------------------------------
|
|
@@ -87,6 +89,8 @@ __all__ = (
|
|
|
87
89
|
"M038_learning_feedback_channel",
|
|
88
90
|
"M039_scene_fact_members",
|
|
89
91
|
"M040_agent_experience_receipts",
|
|
92
|
+
"M041_external_evidence_receipts",
|
|
93
|
+
"M042_correction_case_ledger",
|
|
90
94
|
# Legacy re-exports (backward compat):
|
|
91
95
|
"CURRENT_SCHEMA_VERSION",
|
|
92
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
|
}
|
|
@@ -227,9 +227,23 @@
|
|
|
227
227
|
var healthColor = healthStatus === 'HEALTHY' ? 'var(--ok)'
|
|
228
228
|
: healthStatus === 'ACTIVE' ? 'var(--cyan)' : undefined;
|
|
229
229
|
var pCount = ((beh.patterns) || []).length;
|
|
230
|
-
|
|
230
|
+
// BrainTruth is the portable V4.0.5 source of truth. Legacy sections
|
|
231
|
+
// remain a rolling-upgrade fallback only.
|
|
232
|
+
var truth = (living && living.brain_truth) || {};
|
|
233
|
+
var memoryActivity = truth.memory_activity || {};
|
|
234
|
+
var feedback = truth.feedback || (living && living.feedback) || {};
|
|
235
|
+
var experience = truth.agent_experience || (living && living.agent_experience) || {};
|
|
236
|
+
var externalEvidence = truth.external_evidence || experience.external_graph_evidence || {};
|
|
237
|
+
var correctionQuality = truth.correction_quality || {};
|
|
231
238
|
var graph = (living && living.graph) || {};
|
|
232
|
-
|
|
239
|
+
|
|
240
|
+
function truthCount(section, key, unit) {
|
|
241
|
+
if (!section || section.availability === 'unavailable') {
|
|
242
|
+
return 'Unavailable' + (section && section.reason ? ': ' + section.reason : '');
|
|
243
|
+
}
|
|
244
|
+
var value = section[key];
|
|
245
|
+
return value == null ? 'No data yet' : String(value) + (unit ? ' ' + unit : '');
|
|
246
|
+
}
|
|
233
247
|
|
|
234
248
|
// KPI strip
|
|
235
249
|
var strip = EL('div', { className: 'kpi-strip', style: 'margin-bottom:16px' });
|
|
@@ -313,11 +327,14 @@
|
|
|
313
327
|
['Models trained', String(stats.models_trained || 0)],
|
|
314
328
|
['Verified active models', String(stats.models_active_verified || 0)],
|
|
315
329
|
['Sources tracked', String(stats.tracked_sources || 0)],
|
|
316
|
-
['
|
|
317
|
-
['
|
|
318
|
-
['Claimed evidence
|
|
319
|
-
['
|
|
320
|
-
|
|
330
|
+
['Memory activity', truthCount(memoryActivity, 'facts_total', 'facts')],
|
|
331
|
+
['Feedback signals', truthCount(feedback, 'signals_total', 'signals')],
|
|
332
|
+
['Claimed evidence', truthCount(experience, 'claimed_experiences_total', 'receipts')],
|
|
333
|
+
['Independently verified evidence', truthCount(
|
|
334
|
+
experience, 'independently_verified_experiences_total', 'receipts',
|
|
335
|
+
)],
|
|
336
|
+
['External observations', truthCount(externalEvidence, 'receipts_total', 'receipts')],
|
|
337
|
+
['Correction quality', truthCount(correctionQuality, 'cases_total', 'review cases')],
|
|
321
338
|
['Graph evidence', String(graph.fact_nodes || 0) + ' nodes · ' +
|
|
322
339
|
String(graph.association_edges || 0) + ' edges'],
|
|
323
340
|
].forEach(function (row) {
|
|
@@ -351,17 +368,25 @@
|
|
|
351
368
|
text: 'SLM records completed work when an integration supplies evidence. These records do not change recall, ranking, or model routing by themselves.',
|
|
352
369
|
}));
|
|
353
370
|
var evGrid = EL('div', { className: 'kpi-strip', style: 'margin:0' });
|
|
354
|
-
evGrid.appendChild(kpiCard('fact_check', '
|
|
355
|
-
|
|
356
|
-
Number(experience.
|
|
357
|
-
evGrid.appendChild(kpiCard('verified', '
|
|
358
|
-
|
|
359
|
-
|
|
371
|
+
evGrid.appendChild(kpiCard('fact_check', 'Claimed evidence',
|
|
372
|
+
truthCount(experience, 'claimed_experiences_total', ''), 'profile-scoped durable receipts',
|
|
373
|
+
Number(experience.claimed_experiences_total || 0) > 0, undefined, true));
|
|
374
|
+
evGrid.appendChild(kpiCard('verified', 'Independently verified evidence',
|
|
375
|
+
truthCount(experience, 'independently_verified_experiences_total', ''),
|
|
376
|
+
experience.verification_availability === 'not_supported_by_read_model'
|
|
377
|
+
? 'not supported by this read model' : 'independent verifier result',
|
|
378
|
+
Number(experience.independently_verified_experiences_total || 0) > 0, undefined, true));
|
|
360
379
|
evGrid.appendChild(kpiCard('account_tree', 'Cognitive turns',
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
Number(experience.
|
|
380
|
+
truthCount(experience, 'cognitive_turns_total', ''),
|
|
381
|
+
String((experience.cognitive_turns_by_state || {}).open || 0) + ' open · ' +
|
|
382
|
+
String((experience.cognitive_turns_by_state || {}).finalized || 0) + ' finalized',
|
|
383
|
+
Number(experience.cognitive_turns_total || 0) > 0, undefined, true));
|
|
384
|
+
evGrid.appendChild(kpiCard('account_tree', 'External observations',
|
|
385
|
+
truthCount(externalEvidence, 'receipts_total', ''),
|
|
386
|
+
externalEvidence.availability === 'available'
|
|
387
|
+
? String(externalEvidence.demonstrations_total || 0) + ' demonstrations · no automatic learning'
|
|
388
|
+
: 'external evidence unavailable',
|
|
389
|
+
Number(externalEvidence.receipts_total || 0) > 0, undefined, true));
|
|
365
390
|
evb.appendChild(evGrid);
|
|
366
391
|
evc.appendChild(evb);
|
|
367
392
|
sec.appendChild(evc);
|
|
@@ -845,8 +870,8 @@
|
|
|
845
870
|
var head = EL('div', { className: 'page-head' });
|
|
846
871
|
head.appendChild(EL('h2', { text: 'The living brain' }));
|
|
847
872
|
head.appendChild(EL('p', {
|
|
848
|
-
text: '
|
|
849
|
-
'
|
|
873
|
+
text: 'A local view of memory activity, feedback, and evidence. Observations are shown separately ' +
|
|
874
|
+
'from ranking and do not change recall, ranking, or model routing by themselves.',
|
|
850
875
|
}));
|
|
851
876
|
|
|
852
877
|
container.replaceChildren(
|