superlocalmemory 4.1.11 → 4.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/CHANGELOG.md +37 -0
- package/README.md +15 -4
- package/package.json +2 -2
- 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/agents/slm-memory-advisor.md +1 -1
- package/plugin-src/agents/slm-optimize-advisor.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 +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- 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 +1 -1
- 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/integrations/bounded_loops_mcp.py +229 -0
- package/src/superlocalmemory/learning/database.py +2 -1
- package/src/superlocalmemory/mcp/tools_active.py +20 -5
- package/src/superlocalmemory/mcp/tools_brain.py +38 -1
- package/src/superlocalmemory/mcp/tools_learning.py +123 -4
- package/src/superlocalmemory/storage/_migration_internals.py +2 -0
- package/src/superlocalmemory/storage/_schema_version.py +2 -2
- package/src/superlocalmemory/storage/agent_experience.py +38 -4
- package/src/superlocalmemory/storage/execution_learning.py +285 -0
- package/src/superlocalmemory/storage/migration_runner.py +3 -0
- package/src/superlocalmemory/storage/migrations/M050_execution_learning_v2.py +70 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +2 -0
|
@@ -254,9 +254,33 @@ class AgentExperienceStore:
|
|
|
254
254
|
external_count = conn.execute(
|
|
255
255
|
"DELETE FROM external_evidence_receipts WHERE profile_id=?", (profile_id,)
|
|
256
256
|
).rowcount
|
|
257
|
+
execution_count = 0
|
|
258
|
+
execution_tables = {
|
|
259
|
+
row[0]
|
|
260
|
+
for row in conn.execute(
|
|
261
|
+
"SELECT name FROM sqlite_master WHERE type='table' "
|
|
262
|
+
"AND name IN ('execution_learning_receipts', 'execution_learning_events')"
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
if execution_tables and execution_tables != {
|
|
266
|
+
"execution_learning_receipts", "execution_learning_events"
|
|
267
|
+
}:
|
|
268
|
+
raise sqlite3.OperationalError("incomplete execution-learning receipt schema")
|
|
269
|
+
has_execution = bool(execution_tables)
|
|
270
|
+
if has_execution:
|
|
271
|
+
execution_count += conn.execute(
|
|
272
|
+
"DELETE FROM execution_learning_events WHERE profile_id=?", (profile_id,)
|
|
273
|
+
).rowcount
|
|
274
|
+
execution_count += conn.execute(
|
|
275
|
+
"DELETE FROM execution_learning_receipts WHERE profile_id=?", (profile_id,)
|
|
276
|
+
).rowcount
|
|
257
277
|
receipt_tables = ["agent_experiences", "cognitive_turn_receipts"]
|
|
258
278
|
if has_external:
|
|
259
279
|
receipt_tables.append("external_evidence_receipts")
|
|
280
|
+
if has_execution:
|
|
281
|
+
receipt_tables.extend([
|
|
282
|
+
"execution_learning_receipts", "execution_learning_events",
|
|
283
|
+
])
|
|
260
284
|
residue = sum(
|
|
261
285
|
int(
|
|
262
286
|
conn.execute(
|
|
@@ -267,7 +291,7 @@ class AgentExperienceStore:
|
|
|
267
291
|
)
|
|
268
292
|
if residue:
|
|
269
293
|
raise RuntimeError("learning receipt erasure left profile residue")
|
|
270
|
-
return experience_count + turn_count + external_count
|
|
294
|
+
return experience_count + turn_count + external_count + execution_count
|
|
271
295
|
|
|
272
296
|
return self._write(erase)
|
|
273
297
|
|
|
@@ -434,7 +458,8 @@ def purge_profile_receipts(
|
|
|
434
458
|
for row in conn.execute(
|
|
435
459
|
"SELECT name FROM sqlite_master WHERE type='table' "
|
|
436
460
|
"AND name IN ('agent_experiences', 'cognitive_turn_receipts', "
|
|
437
|
-
"'agent_receipt_profile_closures', 'external_evidence_receipts'
|
|
461
|
+
"'agent_receipt_profile_closures', 'external_evidence_receipts', "
|
|
462
|
+
"'execution_learning_receipts', 'execution_learning_events')"
|
|
438
463
|
)
|
|
439
464
|
}
|
|
440
465
|
if not tables:
|
|
@@ -443,13 +468,22 @@ def purge_profile_receipts(
|
|
|
443
468
|
"agent_experiences", "cognitive_turn_receipts", "agent_receipt_profile_closures"
|
|
444
469
|
}
|
|
445
470
|
if tables != expected:
|
|
446
|
-
|
|
471
|
+
optional = {"external_evidence_receipts"}
|
|
472
|
+
execution = {"execution_learning_receipts", "execution_learning_events"}
|
|
473
|
+
if tables in (expected | optional, expected | execution, expected | optional | execution):
|
|
447
474
|
from superlocalmemory.storage.migrations import M041_external_evidence_receipts as m041
|
|
448
475
|
|
|
449
476
|
with sqlite3.connect(path) as conn:
|
|
450
477
|
# Erasure needs a valid table, not its optional performance indexes.
|
|
451
478
|
# A damaged index must never strand profile-scoped evidence.
|
|
452
|
-
|
|
479
|
+
external_ok = (
|
|
480
|
+
"external_evidence_receipts" not in tables or m041._table_is_valid(conn)
|
|
481
|
+
)
|
|
482
|
+
execution_ok = (
|
|
483
|
+
not execution.intersection(tables)
|
|
484
|
+
or execution <= tables
|
|
485
|
+
)
|
|
486
|
+
if external_ok and execution_ok:
|
|
453
487
|
return AgentExperienceStore(
|
|
454
488
|
path, is_profile_active=lambda _: True
|
|
455
489
|
).erase_profile(profile_id, close_profile=close_profile)
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""Bridge-v2 immutable receipt storage and strictly bounded execution learning.
|
|
2
|
+
|
|
3
|
+
This module deliberately has no dependency on recall, semantic facts, or user
|
|
4
|
+
preferences. Bounded Loops receipts can only update the rebuildable execution
|
|
5
|
+
event projection after the consumer validates every eligibility condition.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
import sqlite3
|
|
14
|
+
from copy import deepcopy
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Callable
|
|
19
|
+
|
|
20
|
+
from superlocalmemory.storage.agent_experience import (
|
|
21
|
+
_ProfileAdmissionGate,
|
|
22
|
+
_PROCESS_LOCKS_GUARD,
|
|
23
|
+
_PROFILE_GATES,
|
|
24
|
+
ProfileAdmissionError,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
_CONTRACT = "bounded-loops.dev/slm-bridge/v2"
|
|
28
|
+
_SHA256_PREFIX = "sha256:"
|
|
29
|
+
_ALLOWED_STATES = frozenset({"SUCCEEDED", "FAILED"})
|
|
30
|
+
_MAX_PAYLOAD_BYTES = 32 * 1024
|
|
31
|
+
_MAX_NODES = 256
|
|
32
|
+
_MAX_ARTIFACTS_PER_NODE = 64
|
|
33
|
+
_SAFE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")
|
|
34
|
+
_DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ExecutionLearningValidationError(ValueError):
|
|
38
|
+
"""A v2 receipt is not eligible for execution-learning ingestion."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# This is deliberately an in-process capability, not a serializable field in
|
|
42
|
+
# the producer payload. An MCP producer can send JSON; it cannot mint this
|
|
43
|
+
# boundary witness. It prevents an accidental future caller from turning a
|
|
44
|
+
# schema-valid JSON document directly into learned execution behaviour.
|
|
45
|
+
_BRIDGE_SEAL = object()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class VerifiedExecutionEvidence:
|
|
50
|
+
"""A v2 payload bound to one negotiated, locally observed producer session.
|
|
51
|
+
|
|
52
|
+
``payload`` intentionally remains available for the immutable receipt
|
|
53
|
+
record. The private seal is checked by :class:`ExecutionLearningStore`
|
|
54
|
+
and never crosses MCP or disk boundaries.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
payload: dict[str, Any]
|
|
58
|
+
producer_identity: str
|
|
59
|
+
capability_digest: str
|
|
60
|
+
terminal_listing_digest: str
|
|
61
|
+
payload_sha256: str
|
|
62
|
+
_seal: object
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _seal_verified_execution_evidence(
|
|
66
|
+
payload: dict[str, Any],
|
|
67
|
+
*,
|
|
68
|
+
producer_identity: str,
|
|
69
|
+
capability_digest: str,
|
|
70
|
+
terminal_listing_digest: str,
|
|
71
|
+
) -> VerifiedExecutionEvidence:
|
|
72
|
+
"""Create the sole accepted execution-learning input at the MCP boundary."""
|
|
73
|
+
_validate(payload)
|
|
74
|
+
for label, value in {
|
|
75
|
+
"producer identity": producer_identity,
|
|
76
|
+
"capability digest": capability_digest,
|
|
77
|
+
"terminal listing digest": terminal_listing_digest,
|
|
78
|
+
}.items():
|
|
79
|
+
if not isinstance(value, str) or not value:
|
|
80
|
+
raise ExecutionLearningValidationError(f"v2 receipt has no verified {label}")
|
|
81
|
+
sealed_payload = deepcopy(payload)
|
|
82
|
+
return VerifiedExecutionEvidence(
|
|
83
|
+
payload=sealed_payload,
|
|
84
|
+
producer_identity=producer_identity,
|
|
85
|
+
capability_digest=capability_digest,
|
|
86
|
+
terminal_listing_digest=terminal_listing_digest,
|
|
87
|
+
payload_sha256=_digest(sealed_payload),
|
|
88
|
+
_seal=_BRIDGE_SEAL,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ExecutionLearningStore:
|
|
93
|
+
"""Atomically persist v2 receipts before their derived event projection."""
|
|
94
|
+
|
|
95
|
+
def __init__(self, path: str | Path, *, is_profile_active: Callable[[str], bool]) -> None:
|
|
96
|
+
self._path = Path(path).resolve()
|
|
97
|
+
self._is_profile_active = is_profile_active
|
|
98
|
+
with _PROCESS_LOCKS_GUARD:
|
|
99
|
+
self._gate = _PROFILE_GATES.setdefault(str(self._path), _ProfileAdmissionGate())
|
|
100
|
+
|
|
101
|
+
def ingest(self, evidence: VerifiedExecutionEvidence) -> bool:
|
|
102
|
+
if (
|
|
103
|
+
not isinstance(evidence, VerifiedExecutionEvidence)
|
|
104
|
+
or evidence._seal is not _BRIDGE_SEAL
|
|
105
|
+
):
|
|
106
|
+
raise ExecutionLearningValidationError(
|
|
107
|
+
"v2 execution learning requires verified bridge provenance"
|
|
108
|
+
)
|
|
109
|
+
payload = evidence.payload
|
|
110
|
+
if evidence.payload_sha256 != _digest(payload):
|
|
111
|
+
raise ExecutionLearningValidationError(
|
|
112
|
+
"v2 execution-learning provenance was modified after verification"
|
|
113
|
+
)
|
|
114
|
+
_validate(payload)
|
|
115
|
+
profile_id = payload["profile_id"]
|
|
116
|
+
self._gate.admit(profile_id, self._is_profile_active)
|
|
117
|
+
digest = _digest(payload)
|
|
118
|
+
receipt = payload["receipt"]
|
|
119
|
+
route_key = _route_key(payload["route"])
|
|
120
|
+
signal = 1 if payload["run_state"] == "SUCCEEDED" else -1
|
|
121
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
122
|
+
conn: sqlite3.Connection | None = None
|
|
123
|
+
try:
|
|
124
|
+
conn = sqlite3.connect(str(self._path), timeout=1.0, isolation_level=None)
|
|
125
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
126
|
+
# The same process-wide admission gate drains in-flight writes
|
|
127
|
+
# before erasure. This durable closure check also prevents a late
|
|
128
|
+
# transaction from resurrecting a profile after a completed close.
|
|
129
|
+
closed = conn.execute(
|
|
130
|
+
"SELECT 1 FROM agent_receipt_profile_closures WHERE profile_id=?", (profile_id,)
|
|
131
|
+
).fetchone()
|
|
132
|
+
if closed is not None:
|
|
133
|
+
raise ProfileAdmissionError("profile is inactive or closing for erasure")
|
|
134
|
+
existing = conn.execute(
|
|
135
|
+
"SELECT payload_sha256 FROM execution_learning_receipts "
|
|
136
|
+
"WHERE profile_id=? AND workspace_id=? AND run_ref=?",
|
|
137
|
+
(profile_id, payload["workspace_id"], payload["run_ref"]),
|
|
138
|
+
).fetchone()
|
|
139
|
+
if existing is not None:
|
|
140
|
+
if existing[0] != digest:
|
|
141
|
+
raise ExecutionLearningValidationError(
|
|
142
|
+
"external run address has a different receipt head"
|
|
143
|
+
)
|
|
144
|
+
conn.execute("ROLLBACK")
|
|
145
|
+
return False
|
|
146
|
+
conn.execute(
|
|
147
|
+
"INSERT INTO execution_learning_receipts "
|
|
148
|
+
"(profile_id, workspace_id, run_ref, run_id, receipt_head_digest, "
|
|
149
|
+
"payload_json, payload_sha256, producer_identity, capability_digest, "
|
|
150
|
+
"terminal_listing_digest, observed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
151
|
+
(profile_id, payload["workspace_id"], payload["run_ref"], payload["run_id"],
|
|
152
|
+
receipt["head_digest"], _json(payload), digest, evidence.producer_identity,
|
|
153
|
+
evidence.capability_digest, evidence.terminal_listing_digest, now),
|
|
154
|
+
)
|
|
155
|
+
conn.execute(
|
|
156
|
+
"INSERT INTO execution_learning_events "
|
|
157
|
+
"(profile_id, workspace_id, run_ref, receipt_head_digest, route_key, signal, created_at) "
|
|
158
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
159
|
+
(profile_id, payload["workspace_id"], payload["run_ref"],
|
|
160
|
+
receipt["head_digest"], route_key, signal, now),
|
|
161
|
+
)
|
|
162
|
+
conn.execute("COMMIT")
|
|
163
|
+
return True
|
|
164
|
+
except Exception:
|
|
165
|
+
if conn is not None and conn.in_transaction:
|
|
166
|
+
conn.execute("ROLLBACK")
|
|
167
|
+
raise
|
|
168
|
+
finally:
|
|
169
|
+
if conn is not None:
|
|
170
|
+
conn.close()
|
|
171
|
+
self._gate.release(profile_id)
|
|
172
|
+
|
|
173
|
+
def status(self, profile_id: str) -> dict[str, int]:
|
|
174
|
+
conn = sqlite3.connect(str(self._path), timeout=0.5)
|
|
175
|
+
try:
|
|
176
|
+
receipts = conn.execute(
|
|
177
|
+
"SELECT COUNT(*) FROM execution_learning_receipts WHERE profile_id=?", (profile_id,)
|
|
178
|
+
).fetchone()[0]
|
|
179
|
+
totals = conn.execute(
|
|
180
|
+
"SELECT COUNT(*), COALESCE(SUM(signal = 1), 0), COALESCE(SUM(signal = -1), 0) "
|
|
181
|
+
"FROM execution_learning_events WHERE profile_id=?", (profile_id,),
|
|
182
|
+
).fetchone()
|
|
183
|
+
return {"receipts_total": int(receipts), "learning_events_total": int(totals[0]),
|
|
184
|
+
"positive_events": int(totals[1]), "negative_events": int(totals[2])}
|
|
185
|
+
finally:
|
|
186
|
+
conn.close()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _validate(payload: dict[str, Any]) -> None:
|
|
190
|
+
required = {"contract", "profile_id", "workspace_id", "run_ref", "run_id", "outcome",
|
|
191
|
+
"run_state", "demonstration", "eligible_for_learning", "terminal_at",
|
|
192
|
+
"graph_digest", "plan_digest", "policy_digest", "receipt", "nodes",
|
|
193
|
+
"learning_authority", "route", "usage"}
|
|
194
|
+
if set(payload) != required or payload.get("contract") != _CONTRACT:
|
|
195
|
+
raise ExecutionLearningValidationError("v2 receipt does not match the bounded contract")
|
|
196
|
+
if len(_json(payload).encode("utf-8")) > _MAX_PAYLOAD_BYTES:
|
|
197
|
+
raise ExecutionLearningValidationError("v2 receipt exceeds the bounded payload size")
|
|
198
|
+
for field in ("profile_id", "workspace_id", "run_ref", "run_id"):
|
|
199
|
+
if not isinstance(payload[field], str) or not _SAFE_ID.fullmatch(payload[field]):
|
|
200
|
+
raise ExecutionLearningValidationError(f"v2 receipt has an invalid {field}")
|
|
201
|
+
for field in ("workspace_id", "graph_digest", "plan_digest", "policy_digest"):
|
|
202
|
+
if not isinstance(payload[field], str) or not _DIGEST.fullmatch(payload[field]):
|
|
203
|
+
raise ExecutionLearningValidationError(f"v2 receipt has an invalid {field}")
|
|
204
|
+
if not isinstance(payload["terminal_at"], str) or len(payload["terminal_at"]) > 64:
|
|
205
|
+
raise ExecutionLearningValidationError("v2 receipt has an invalid terminal timestamp")
|
|
206
|
+
if payload["demonstration"] is not False or payload["eligible_for_learning"] is not True:
|
|
207
|
+
raise ExecutionLearningValidationError("receipt is not eligible for execution learning")
|
|
208
|
+
if payload["run_state"] not in _ALLOWED_STATES or payload["outcome"] != payload["run_state"]:
|
|
209
|
+
raise ExecutionLearningValidationError("only terminal succeeded or executed-gate failure is learnable")
|
|
210
|
+
receipt, authority = payload["receipt"], payload["learning_authority"]
|
|
211
|
+
if not isinstance(receipt, dict) or not isinstance(authority, dict):
|
|
212
|
+
raise ExecutionLearningValidationError("receipt authority is malformed")
|
|
213
|
+
if set(receipt) != {"sequence", "head_digest", "trust"}:
|
|
214
|
+
raise ExecutionLearningValidationError("receipt chain metadata is malformed")
|
|
215
|
+
if (
|
|
216
|
+
isinstance(receipt["sequence"], bool)
|
|
217
|
+
or not isinstance(receipt["sequence"], int)
|
|
218
|
+
or receipt["sequence"] < 1
|
|
219
|
+
or receipt["trust"] != "local_hash_chain_only"
|
|
220
|
+
or not isinstance(receipt.get("head_digest"), str)
|
|
221
|
+
or not _DIGEST.fullmatch(receipt["head_digest"])
|
|
222
|
+
):
|
|
223
|
+
raise ExecutionLearningValidationError("receipt chain head is invalid")
|
|
224
|
+
if authority != {"scope": "execution_reliability_only", "reason_code": "verified_terminal_receipt",
|
|
225
|
+
"verification_state": "reconciled", "gate_authority": "deterministic_gate",
|
|
226
|
+
"trust_class": "local_hash_chain_only"}:
|
|
227
|
+
raise ExecutionLearningValidationError("receipt lacks bounded deterministic learning authority")
|
|
228
|
+
_validate_nodes(payload["nodes"])
|
|
229
|
+
_validate_route(payload["route"])
|
|
230
|
+
_validate_usage(payload["usage"])
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _validate_nodes(nodes: Any) -> None:
|
|
234
|
+
if not isinstance(nodes, list) or not nodes or len(nodes) > _MAX_NODES:
|
|
235
|
+
raise ExecutionLearningValidationError("receipt nodes are malformed")
|
|
236
|
+
for node in nodes:
|
|
237
|
+
if not isinstance(node, dict) or set(node) != {
|
|
238
|
+
"node_id", "state", "gate_passed", "attempts", "artifact_digests"
|
|
239
|
+
}:
|
|
240
|
+
raise ExecutionLearningValidationError("receipt node schema is malformed")
|
|
241
|
+
if (
|
|
242
|
+
not isinstance(node["node_id"], str)
|
|
243
|
+
or not _SAFE_ID.fullmatch(node["node_id"])
|
|
244
|
+
or not isinstance(node["state"], str)
|
|
245
|
+
or not _SAFE_ID.fullmatch(node["state"])
|
|
246
|
+
or node["gate_passed"] not in (True, False, None)
|
|
247
|
+
or isinstance(node["attempts"], bool)
|
|
248
|
+
or not isinstance(node["attempts"], int)
|
|
249
|
+
or not 0 <= node["attempts"] <= 10_000
|
|
250
|
+
or not isinstance(node["artifact_digests"], list)
|
|
251
|
+
or len(node["artifact_digests"]) > _MAX_ARTIFACTS_PER_NODE
|
|
252
|
+
or not all(isinstance(item, str) and _DIGEST.fullmatch(item)
|
|
253
|
+
for item in node["artifact_digests"])
|
|
254
|
+
):
|
|
255
|
+
raise ExecutionLearningValidationError("receipt node values are malformed")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _validate_route(route: Any) -> None:
|
|
259
|
+
if not isinstance(route, dict) or set(route) - {"runner", "provider", "model", "effort"}:
|
|
260
|
+
raise ExecutionLearningValidationError("route or usage is malformed")
|
|
261
|
+
if not all(isinstance(value, str) and _SAFE_ID.fullmatch(value) for value in route.values()):
|
|
262
|
+
raise ExecutionLearningValidationError("route or usage is malformed")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _validate_usage(usage: Any) -> None:
|
|
266
|
+
if (
|
|
267
|
+
not isinstance(usage, dict)
|
|
268
|
+
or set(usage) != {"attempts"}
|
|
269
|
+
or isinstance(usage["attempts"], bool)
|
|
270
|
+
or not isinstance(usage["attempts"], int)
|
|
271
|
+
or not 0 <= usage["attempts"] <= 1_000_000
|
|
272
|
+
):
|
|
273
|
+
raise ExecutionLearningValidationError("route or usage is malformed")
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _route_key(route: dict[str, Any]) -> str:
|
|
277
|
+
return "|".join(str(route.get(key, "")) for key in ("runner", "provider", "model", "effort"))
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _json(value: Any) -> str:
|
|
281
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _digest(value: dict[str, Any]) -> str:
|
|
285
|
+
return hashlib.sha256(_json(value).encode("utf-8")).hexdigest()
|
|
@@ -168,6 +168,7 @@ from superlocalmemory.storage.migrations import (
|
|
|
168
168
|
M047_fisher_vectors_are_stored_like_every_other_vector as _M047,
|
|
169
169
|
M048_upcoming_holds_only_what_is_upcoming as _M048,
|
|
170
170
|
M049_a_schema_version_marker_is_one_row as _M049,
|
|
171
|
+
M050_execution_learning_v2 as _M050,
|
|
171
172
|
)
|
|
172
173
|
from superlocalmemory.storage.migrations import (
|
|
173
174
|
M043_quarantine_display_summaries as _M043,
|
|
@@ -259,6 +260,8 @@ MIGRATIONS: list[Migration] = [
|
|
|
259
260
|
dependencies=(_M003.NAME,)),
|
|
260
261
|
Migration(name=_M041.NAME, db_target="learning", ddl=_M041.DDL,
|
|
261
262
|
dependencies=(_M040.NAME,)),
|
|
263
|
+
Migration(name=_M050.NAME, db_target="learning", ddl=_M050.DDL,
|
|
264
|
+
dependencies=(_M041.NAME,)),
|
|
262
265
|
# Review-gated correction metadata is self-contained in memory.db. It
|
|
263
266
|
# contains identifiers only and does not alter temporal fact state.
|
|
264
267
|
Migration(name=_M042.NAME, db_target="memory", ddl=_M042.DDL,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""M050 — immutable bridge-v2 receipts and rebuildable execution learning."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sqlite3
|
|
6
|
+
|
|
7
|
+
NAME = "M050_execution_learning_v2"
|
|
8
|
+
DB_TARGET = "learning"
|
|
9
|
+
|
|
10
|
+
DDL = """
|
|
11
|
+
BEGIN IMMEDIATE;
|
|
12
|
+
CREATE TABLE IF NOT EXISTS execution_learning_receipts (
|
|
13
|
+
profile_id TEXT NOT NULL,
|
|
14
|
+
workspace_id TEXT NOT NULL,
|
|
15
|
+
run_ref TEXT NOT NULL,
|
|
16
|
+
run_id TEXT NOT NULL,
|
|
17
|
+
receipt_head_digest TEXT NOT NULL,
|
|
18
|
+
payload_json TEXT NOT NULL,
|
|
19
|
+
payload_sha256 TEXT NOT NULL,
|
|
20
|
+
producer_identity TEXT NOT NULL,
|
|
21
|
+
capability_digest TEXT NOT NULL,
|
|
22
|
+
terminal_listing_digest TEXT NOT NULL,
|
|
23
|
+
observed_at TEXT NOT NULL,
|
|
24
|
+
PRIMARY KEY (profile_id, workspace_id, run_ref)
|
|
25
|
+
);
|
|
26
|
+
CREATE TABLE IF NOT EXISTS execution_learning_events (
|
|
27
|
+
profile_id TEXT NOT NULL,
|
|
28
|
+
workspace_id TEXT NOT NULL,
|
|
29
|
+
run_ref TEXT NOT NULL,
|
|
30
|
+
receipt_head_digest TEXT NOT NULL,
|
|
31
|
+
route_key TEXT NOT NULL,
|
|
32
|
+
signal INTEGER NOT NULL CHECK (signal IN (-1, 1)),
|
|
33
|
+
created_at TEXT NOT NULL,
|
|
34
|
+
PRIMARY KEY (profile_id, workspace_id, run_ref, receipt_head_digest)
|
|
35
|
+
);
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_execution_learning_events_profile_route
|
|
37
|
+
ON execution_learning_events (profile_id, route_key);
|
|
38
|
+
COMMIT;
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def apply(conn: sqlite3.Connection) -> None:
|
|
43
|
+
"""Install only additive, independently-owned v2 tables."""
|
|
44
|
+
conn.executescript(DDL)
|
|
45
|
+
if not verify(conn):
|
|
46
|
+
raise sqlite3.OperationalError("M050 execution-learning schema did not reach its end-state")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def verify(conn: sqlite3.Connection) -> bool:
|
|
50
|
+
required = {"execution_learning_receipts", "execution_learning_events"}
|
|
51
|
+
tables = {row[0] for row in conn.execute(
|
|
52
|
+
"SELECT name FROM sqlite_master WHERE type='table'"
|
|
53
|
+
)}
|
|
54
|
+
if not required <= tables:
|
|
55
|
+
return False
|
|
56
|
+
receipt_columns = {
|
|
57
|
+
row[1] for row in conn.execute("PRAGMA table_info(execution_learning_receipts)")
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
"producer_identity", "capability_digest", "terminal_listing_digest",
|
|
61
|
+
} <= receipt_columns
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def repair(conn: sqlite3.Connection) -> None:
|
|
65
|
+
"""Restore missing derived indexes without mutating immutable receipts."""
|
|
66
|
+
if not verify(conn):
|
|
67
|
+
apply(conn)
|
|
68
|
+
return
|
|
69
|
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_execution_learning_events_profile_route "
|
|
70
|
+
"ON execution_learning_events (profile_id, route_key)")
|
|
@@ -33,6 +33,7 @@ from . import (
|
|
|
33
33
|
M040_agent_experience_receipts,
|
|
34
34
|
M041_external_evidence_receipts,
|
|
35
35
|
M042_correction_case_ledger,
|
|
36
|
+
M050_execution_learning_v2,
|
|
36
37
|
)
|
|
37
38
|
|
|
38
39
|
# ---------------------------------------------------------------------------
|
|
@@ -91,6 +92,7 @@ __all__ = (
|
|
|
91
92
|
"M040_agent_experience_receipts",
|
|
92
93
|
"M041_external_evidence_receipts",
|
|
93
94
|
"M042_correction_case_ledger",
|
|
95
|
+
"M050_execution_learning_v2",
|
|
94
96
|
# Legacy re-exports (backward compat):
|
|
95
97
|
"CURRENT_SCHEMA_VERSION",
|
|
96
98
|
"get_schema_version",
|