superlocalmemory 4.1.9 → 4.1.12
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 +43 -0
- package/README.md +15 -4
- package/package.json +1 -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/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/cli/daemon.py +18 -0
- package/src/superlocalmemory/integrations/bounded_loops_mcp.py +229 -0
- package/src/superlocalmemory/learning/database.py +2 -1
- package/src/superlocalmemory/loops/__init__.py +2 -0
- package/src/superlocalmemory/loops/ledger.py +35 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +27 -8
- package/src/superlocalmemory/mcp/server.py +6 -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/mcp/tools_loops.py +51 -17
- package/src/superlocalmemory/server/unified_daemon.py +17 -0
- 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
- package/src/superlocalmemory/ui/index.html +2 -2
|
@@ -15,11 +15,14 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
|
+
import asyncio
|
|
18
19
|
import json
|
|
19
20
|
import logging
|
|
20
21
|
import os
|
|
22
|
+
import sqlite3
|
|
21
23
|
import uuid
|
|
22
24
|
from datetime import datetime, timezone
|
|
25
|
+
from pathlib import Path
|
|
23
26
|
from typing import Callable
|
|
24
27
|
|
|
25
28
|
from mcp.types import ToolAnnotations
|
|
@@ -33,6 +36,72 @@ logger = logging.getLogger(__name__)
|
|
|
33
36
|
_MAX_SUMMARY_LEN = 500 # Truncate input/output summaries
|
|
34
37
|
|
|
35
38
|
|
|
39
|
+
def settle_pending_session_outcomes(
|
|
40
|
+
memory_db_path: str | Path,
|
|
41
|
+
*,
|
|
42
|
+
profile_id: str,
|
|
43
|
+
session_id: str,
|
|
44
|
+
evidence_only: bool = False,
|
|
45
|
+
) -> dict[str, int]:
|
|
46
|
+
"""Finalize real pending recalls for one host-owned session.
|
|
47
|
+
|
|
48
|
+
This is deliberately a narrow adapter around ``EngagementRewardModel``:
|
|
49
|
+
it selects only pending rows belonging to the supplied active profile and
|
|
50
|
+
exact host session, then delegates every state transition and reward
|
|
51
|
+
calculation to the established finalizer. No feedback is inferred here.
|
|
52
|
+
Repeating the call is a no-op because settled rows are not selected.
|
|
53
|
+
"""
|
|
54
|
+
session_id = session_id.strip()
|
|
55
|
+
if not session_id:
|
|
56
|
+
return {"selected": 0, "settled": 0}
|
|
57
|
+
|
|
58
|
+
path = Path(memory_db_path)
|
|
59
|
+
try:
|
|
60
|
+
with sqlite3.connect(str(path), timeout=2.0) as conn:
|
|
61
|
+
rows = conn.execute(
|
|
62
|
+
"SELECT outcome_id, signals_json FROM pending_outcomes "
|
|
63
|
+
"WHERE profile_id=? AND session_id=? AND status='pending'",
|
|
64
|
+
(profile_id, session_id),
|
|
65
|
+
).fetchall()
|
|
66
|
+
except sqlite3.Error as exc:
|
|
67
|
+
raise RuntimeError("pending outcome lookup failed") from exc
|
|
68
|
+
|
|
69
|
+
if evidence_only:
|
|
70
|
+
from superlocalmemory.learning.reward import _carries_evidence
|
|
71
|
+
|
|
72
|
+
rows = [
|
|
73
|
+
row for row in rows
|
|
74
|
+
if _carries_evidence(json.loads(row[1] or "{}"))
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
if not rows:
|
|
78
|
+
return {"selected": 0, "settled": 0}
|
|
79
|
+
|
|
80
|
+
from superlocalmemory.learning.reward import EngagementRewardModel
|
|
81
|
+
|
|
82
|
+
model = EngagementRewardModel(memory_db_path=str(path))
|
|
83
|
+
try:
|
|
84
|
+
for outcome_id, _signals_json in rows:
|
|
85
|
+
# Preserve the established keyword-only finalization contract.
|
|
86
|
+
model.finalize_outcome(outcome_id=outcome_id)
|
|
87
|
+
finally:
|
|
88
|
+
model.close()
|
|
89
|
+
|
|
90
|
+
outcome_ids = [row[0] for row in rows]
|
|
91
|
+
placeholders = ",".join("?" for _ in outcome_ids)
|
|
92
|
+
try:
|
|
93
|
+
with sqlite3.connect(str(path), timeout=2.0) as conn:
|
|
94
|
+
settled = conn.execute(
|
|
95
|
+
"SELECT COUNT(*) FROM pending_outcomes "
|
|
96
|
+
"WHERE profile_id=? AND session_id=? AND status='settled' "
|
|
97
|
+
f"AND outcome_id IN ({placeholders})",
|
|
98
|
+
(profile_id, session_id, *outcome_ids),
|
|
99
|
+
).fetchone()[0]
|
|
100
|
+
except sqlite3.Error as exc:
|
|
101
|
+
raise RuntimeError("pending outcome settlement verification failed") from exc
|
|
102
|
+
return {"selected": len(outcome_ids), "settled": int(settled)}
|
|
103
|
+
|
|
104
|
+
|
|
36
105
|
def register_learning_tools(server, get_engine: Callable) -> None:
|
|
37
106
|
"""Register learning MCP tools for two-way intelligence."""
|
|
38
107
|
|
|
@@ -45,6 +114,9 @@ def register_learning_tools(server, get_engine: Callable) -> None:
|
|
|
45
114
|
output_summary: str = "",
|
|
46
115
|
duration_ms: int = 0,
|
|
47
116
|
metadata: str = "{}",
|
|
117
|
+
session_id: str = "",
|
|
118
|
+
agent_id: str = "",
|
|
119
|
+
project_path: str = "",
|
|
48
120
|
) -> dict:
|
|
49
121
|
"""Log a tool usage event for behavioral learning.
|
|
50
122
|
|
|
@@ -62,8 +134,11 @@ def register_learning_tools(server, get_engine: Callable) -> None:
|
|
|
62
134
|
"""
|
|
63
135
|
engine = get_engine()
|
|
64
136
|
now = datetime.now(timezone.utc).isoformat()
|
|
65
|
-
|
|
66
|
-
|
|
137
|
+
from superlocalmemory.mcp.session_binding import resolve_session_id
|
|
138
|
+
effective_session_id = resolve_session_id(
|
|
139
|
+
session_id, agent_id=agent_id or "mcp_client", allow_agent_fallback=True,
|
|
140
|
+
)
|
|
141
|
+
effective_project_path = project_path or (
|
|
67
142
|
os.environ.get("CLAUDE_PROJECT_DIR")
|
|
68
143
|
or os.environ.get("PROJECT_PATH")
|
|
69
144
|
or os.getcwd()
|
|
@@ -86,15 +161,59 @@ def register_learning_tools(server, get_engine: Callable) -> None:
|
|
|
86
161
|
"(session_id, profile_id, project_path, tool_name, event_type, "
|
|
87
162
|
" input_summary, output_summary, duration_ms, metadata, created_at) "
|
|
88
163
|
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
89
|
-
(
|
|
164
|
+
(effective_session_id, engine.profile_id, effective_project_path, tool_name,
|
|
90
165
|
event_type, input_clean, output_clean, duration_ms, metadata, now),
|
|
91
166
|
)
|
|
92
167
|
authorization.complete()
|
|
93
|
-
return {"success": True, "tool": tool_name, "event": event_type
|
|
168
|
+
return {"success": True, "tool": tool_name, "event": event_type,
|
|
169
|
+
"session_id": effective_session_id}
|
|
94
170
|
except Exception as exc:
|
|
95
171
|
logger.debug("log_tool_event failed: %s", exc)
|
|
96
172
|
return {"success": False, "error": str(exc)}
|
|
97
173
|
|
|
174
|
+
@server.tool()
|
|
175
|
+
@admits(OperationKind.REMEMBER)
|
|
176
|
+
async def settle_session_outcomes(
|
|
177
|
+
session_id: str, agent_id: str = "", finalize: bool = False,
|
|
178
|
+
) -> dict:
|
|
179
|
+
"""Settle pending recall outcomes for one exact host session.
|
|
180
|
+
|
|
181
|
+
Native hosts call this after each turn and at finalization. Per-turn
|
|
182
|
+
calls settle only recalls that have actual evidence; finalization also
|
|
183
|
+
clears evidence-free pending rows. The operation never treats a hook
|
|
184
|
+
boundary itself as feedback. ``session_id`` must be explicit.
|
|
185
|
+
"""
|
|
186
|
+
if not session_id or not session_id.strip():
|
|
187
|
+
return {"success": False, "error": "session_id is required"}
|
|
188
|
+
engine = get_engine()
|
|
189
|
+
try:
|
|
190
|
+
authorization = authorize_mcp_mutation(
|
|
191
|
+
engine,
|
|
192
|
+
"update",
|
|
193
|
+
mutation_source="mcp-settle-session-outcomes",
|
|
194
|
+
profile_id=engine.profile_id,
|
|
195
|
+
content_preview=agent_id[:100],
|
|
196
|
+
)
|
|
197
|
+
from superlocalmemory.hooks._outcome_common import memory_db_path
|
|
198
|
+
|
|
199
|
+
result = await asyncio.to_thread(
|
|
200
|
+
settle_pending_session_outcomes,
|
|
201
|
+
memory_db_path(),
|
|
202
|
+
profile_id=engine.profile_id,
|
|
203
|
+
session_id=session_id,
|
|
204
|
+
evidence_only=not finalize,
|
|
205
|
+
)
|
|
206
|
+
authorization.complete()
|
|
207
|
+
return {
|
|
208
|
+
"success": True,
|
|
209
|
+
"session_id": session_id.strip(),
|
|
210
|
+
"agent_id": agent_id.strip(),
|
|
211
|
+
**result,
|
|
212
|
+
}
|
|
213
|
+
except Exception as exc:
|
|
214
|
+
logger.debug("settle_session_outcomes failed: %s", exc)
|
|
215
|
+
return {"success": False, "error": str(exc)}
|
|
216
|
+
|
|
98
217
|
@server.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
99
218
|
async def get_assertions(
|
|
100
219
|
min_confidence: float = 0.0,
|
|
@@ -47,6 +47,7 @@ from superlocalmemory.loops import (
|
|
|
47
47
|
LedgerEntry,
|
|
48
48
|
Verdict,
|
|
49
49
|
engine_backed_ledger,
|
|
50
|
+
pool_backed_ledger,
|
|
50
51
|
run_bounded_loop,
|
|
51
52
|
)
|
|
52
53
|
|
|
@@ -69,7 +70,11 @@ _MAX_NAME_CHARS = 128
|
|
|
69
70
|
_MAX_QUERY_CHARS = 2000
|
|
70
71
|
|
|
71
72
|
|
|
72
|
-
def register_loop_tools(
|
|
73
|
+
def register_loop_tools(
|
|
74
|
+
server,
|
|
75
|
+
get_engine: Callable,
|
|
76
|
+
get_pool: Callable | None = None,
|
|
77
|
+
) -> None:
|
|
73
78
|
"""Register the 3 bounded-loop tools on *server*.
|
|
74
79
|
|
|
75
80
|
*server* is duck-typed: must support the ``@server.tool()`` decorator.
|
|
@@ -144,10 +149,46 @@ def register_loop_tools(server, get_engine: Callable) -> None:
|
|
|
144
149
|
min_score = float(gate_min_score)
|
|
145
150
|
|
|
146
151
|
engine = get_engine()
|
|
152
|
+
pool = get_pool() if get_pool is not None else None
|
|
147
153
|
|
|
148
154
|
def gate(lap: int) -> Verdict:
|
|
149
|
-
|
|
150
|
-
|
|
155
|
+
if pool is not None:
|
|
156
|
+
resp = pool.recall(gate_query, limit=3, fast=True)
|
|
157
|
+
if not isinstance(resp, dict) or resp.get("ok") is False:
|
|
158
|
+
raise RuntimeError(
|
|
159
|
+
(resp or {}).get("error", "owned SLM reader rejected recall")
|
|
160
|
+
if isinstance(resp, dict)
|
|
161
|
+
else "owned SLM reader returned an invalid response"
|
|
162
|
+
)
|
|
163
|
+
all_results = resp.get("results", []) or []
|
|
164
|
+
floored = bool(resp.get("no_confident_match", False))
|
|
165
|
+
else:
|
|
166
|
+
resp = engine.recall(gate_query, limit=3, fast=True)
|
|
167
|
+
all_results = getattr(resp, "results", None) or []
|
|
168
|
+
floored = bool(getattr(resp, "no_confident_match", False))
|
|
169
|
+
|
|
170
|
+
def _content(result: Any) -> str:
|
|
171
|
+
if isinstance(result, dict):
|
|
172
|
+
fact = result.get("fact")
|
|
173
|
+
if isinstance(fact, dict):
|
|
174
|
+
return str(fact.get("content", "") or "")
|
|
175
|
+
return str(result.get("content", "") or "")
|
|
176
|
+
return str(
|
|
177
|
+
getattr(getattr(result, "fact", None), "content", "") or ""
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def _score(result: Any) -> float:
|
|
181
|
+
if isinstance(result, dict):
|
|
182
|
+
value = result.get("score", result.get("relevance_score", 0.0))
|
|
183
|
+
else:
|
|
184
|
+
value = getattr(result, "score", None)
|
|
185
|
+
if value is None:
|
|
186
|
+
value = getattr(result, "relevance_score", 0.0) or 0.0
|
|
187
|
+
try:
|
|
188
|
+
return float(value)
|
|
189
|
+
except (TypeError, ValueError):
|
|
190
|
+
return 0.0
|
|
191
|
+
|
|
151
192
|
# Exclude the loop's own audit records from the gate evaluation.
|
|
152
193
|
# Each lap writes a LedgerEntry (JSON with "run_id" + "lap") into
|
|
153
194
|
# SLM via store_fast; the FTS5 trigger indexes every such insert.
|
|
@@ -157,20 +198,9 @@ def register_loop_tools(server, get_engine: Callable) -> None:
|
|
|
157
198
|
# can satisfy an independent gate.
|
|
158
199
|
results = [
|
|
159
200
|
r for r in all_results
|
|
160
|
-
if LedgerEntry.from_json(
|
|
161
|
-
getattr(getattr(r, "fact", None), "content", "") or ""
|
|
162
|
-
) is None
|
|
201
|
+
if LedgerEntry.from_json(_content(r)) is None
|
|
163
202
|
]
|
|
164
|
-
|
|
165
|
-
best: float = 0.0
|
|
166
|
-
for r in results:
|
|
167
|
-
s = getattr(r, "score", None)
|
|
168
|
-
if s is None:
|
|
169
|
-
s = getattr(r, "relevance_score", 0.0) or 0.0
|
|
170
|
-
try:
|
|
171
|
-
best = max(best, float(s))
|
|
172
|
-
except (TypeError, ValueError):
|
|
173
|
-
pass
|
|
203
|
+
best = max((_score(result) for result in results), default=0.0)
|
|
174
204
|
passed = bool(results) and not floored and best >= min_score
|
|
175
205
|
return Verdict(
|
|
176
206
|
passed,
|
|
@@ -187,7 +217,11 @@ def register_loop_tools(server, get_engine: Callable) -> None:
|
|
|
187
217
|
time.sleep(poll)
|
|
188
218
|
return LapResult(changed=False, tokens=0)
|
|
189
219
|
|
|
190
|
-
ledger =
|
|
220
|
+
ledger = (
|
|
221
|
+
pool_backed_ledger(pool, engine)
|
|
222
|
+
if pool is not None
|
|
223
|
+
else engine_backed_ledger(engine)
|
|
224
|
+
)
|
|
191
225
|
|
|
192
226
|
# The loop blocks (sleeps between laps); run it off the event loop.
|
|
193
227
|
# The engine's per-call WAL connection model makes this thread-safe.
|
|
@@ -630,6 +630,10 @@ class RememberRequest(BaseModel):
|
|
|
630
630
|
metadata: dict | None = None # v3.4.26: pass-through from MCP pool_store
|
|
631
631
|
idempotency_key: str | None = None
|
|
632
632
|
session_id: str = ""
|
|
633
|
+
# Optional compare-and-write guard for profile-sensitive clients. The
|
|
634
|
+
# daemon remains single-profile, so a stale MCP client must fail instead
|
|
635
|
+
# of silently writing into whichever profile another client selected.
|
|
636
|
+
profile_id: str = ""
|
|
633
637
|
#: WHEN this memory is about, as distinct from when it was written.
|
|
634
638
|
#:
|
|
635
639
|
#: The internal admission record has carried this field all along and this
|
|
@@ -693,6 +697,18 @@ class RememberRequest(BaseModel):
|
|
|
693
697
|
shared_with: list[str] | None = None
|
|
694
698
|
|
|
695
699
|
|
|
700
|
+
def _require_remember_profile(requested: str, active: str) -> None:
|
|
701
|
+
"""Reject a stale profile-bound write before any durable mutation."""
|
|
702
|
+
if requested and requested != active:
|
|
703
|
+
raise HTTPException(
|
|
704
|
+
status_code=409,
|
|
705
|
+
detail=(
|
|
706
|
+
f"profile mismatch: request is bound to '{requested}' but "
|
|
707
|
+
f"the daemon currently serves '{active}'"
|
|
708
|
+
),
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
|
|
696
712
|
class SessionOpenRequest(BaseModel):
|
|
697
713
|
# #49: local session-open warm (no model roundtrip needed)
|
|
698
714
|
project_path: str = ""
|
|
@@ -4671,6 +4687,7 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
4671
4687
|
trusted_actor_id = _require_write_actor(request)
|
|
4672
4688
|
_update_activity()
|
|
4673
4689
|
engine = _get_engine_or_503()
|
|
4690
|
+
_require_remember_profile(req.profile_id, engine._profile_id)
|
|
4674
4691
|
|
|
4675
4692
|
# v3.6.15 multi-scope: resolve the write scope. ``None`` (not specified
|
|
4676
4693
|
# by the caller) → the configured default_scope (personal). Shared
|
|
@@ -165,6 +165,7 @@ from superlocalmemory.storage.migrations import (
|
|
|
165
165
|
M047_fisher_vectors_are_stored_like_every_other_vector as _M047,
|
|
166
166
|
M048_upcoming_holds_only_what_is_upcoming as _M048,
|
|
167
167
|
M049_a_schema_version_marker_is_one_row as _M049,
|
|
168
|
+
M050_execution_learning_v2 as _M050,
|
|
168
169
|
)
|
|
169
170
|
|
|
170
171
|
# Emit under the runner's logger name so operational log filters that key on
|
|
@@ -223,6 +224,7 @@ _MODULES = {
|
|
|
223
224
|
_M047.NAME: _M047,
|
|
224
225
|
_M048.NAME: _M048,
|
|
225
226
|
_M049.NAME: _M049,
|
|
227
|
+
_M050.NAME: _M050,
|
|
226
228
|
}
|
|
227
229
|
|
|
228
230
|
# Exact historical DDL fingerprints whose resulting schema is intentionally
|
|
@@ -21,7 +21,7 @@ import sqlite3
|
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
|
|
23
23
|
#: Highest schema_version this runner can write. Matches the trailing serial of
|
|
24
|
-
#: the latest migration (
|
|
24
|
+
#: the latest migration (M050). Increment when adding new migrations or
|
|
25
25
|
#: table-level breaking changes.
|
|
26
26
|
#:
|
|
27
27
|
#: This sat at 42 while M043, M044 and M045 shipped, so for three migrations the
|
|
@@ -44,7 +44,7 @@ from pathlib import Path
|
|
|
44
44
|
#: be additive is a silent bad write. Those are not comparable, and judging
|
|
45
45
|
#: additivity per migration is exactly the judgement that let it fall three
|
|
46
46
|
#: behind.
|
|
47
|
-
SUPPORTED_SCHEMA_VERSION: int =
|
|
47
|
+
SUPPORTED_SCHEMA_VERSION: int = 50
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
class SchemaVersionError(RuntimeError):
|
|
@@ -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)
|