superlocalmemory 3.4.19 → 3.4.22
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 +24 -0
- package/README.md +42 -34
- package/bin/slm +11 -0
- package/bin/slm.bat +12 -0
- package/package.json +4 -3
- package/pyproject.toml +4 -3
- package/scripts/build-slm-hook.ps1 +40 -0
- package/scripts/build-slm-hook.sh +45 -0
- package/scripts/build_entry.py +452 -0
- package/scripts/ci/stage5b_gate.sh +50 -0
- package/scripts/postinstall/validation.js +187 -0
- package/scripts/postinstall-interactive.js +756 -0
- package/scripts/postinstall_binary.js +287 -0
- package/scripts/release_manifest.py +273 -0
- package/scripts/slm-hook.spec +56 -0
- package/skills/slm-build-graph/SKILL.md +423 -0
- package/skills/slm-list-recent/SKILL.md +348 -0
- package/skills/slm-recall/SKILL.md +343 -0
- package/skills/slm-remember/SKILL.md +194 -0
- package/skills/slm-show-patterns/SKILL.md +224 -0
- package/skills/slm-status/SKILL.md +363 -0
- package/skills/slm-switch-profile/SKILL.md +442 -0
- package/src/superlocalmemory/cli/commands.py +254 -79
- package/src/superlocalmemory/cli/context_commands.py +192 -0
- package/src/superlocalmemory/cli/daemon.py +15 -1
- package/src/superlocalmemory/cli/db_migrate.py +80 -0
- package/src/superlocalmemory/cli/escape_hatch.py +220 -0
- package/src/superlocalmemory/cli/main.py +72 -1
- package/src/superlocalmemory/core/context_cache.py +397 -0
- package/src/superlocalmemory/core/engine.py +38 -2
- package/src/superlocalmemory/core/engine_wiring.py +1 -1
- package/src/superlocalmemory/core/ram_lock.py +111 -0
- package/src/superlocalmemory/core/recall_pipeline.py +433 -3
- package/src/superlocalmemory/core/recall_worker.py +8 -3
- package/src/superlocalmemory/core/security_primitives.py +635 -0
- package/src/superlocalmemory/core/shadow_router.py +319 -0
- package/src/superlocalmemory/core/slm_disabled.py +87 -0
- package/src/superlocalmemory/core/slmignore.py +125 -0
- package/src/superlocalmemory/core/topic_signature.py +143 -0
- package/src/superlocalmemory/core/worker_pool.py +14 -3
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +2 -2
- package/src/superlocalmemory/evolution/budget.py +321 -0
- package/src/superlocalmemory/evolution/llm_dispatch.py +508 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +144 -94
- package/src/superlocalmemory/hooks/_outcome_common.py +506 -0
- package/src/superlocalmemory/hooks/adapter_base.py +317 -0
- package/src/superlocalmemory/hooks/antigravity_adapter.py +192 -0
- package/src/superlocalmemory/hooks/claude_code_hooks.py +33 -1
- package/src/superlocalmemory/hooks/context_payload.py +312 -0
- package/src/superlocalmemory/hooks/copilot_adapter.py +154 -0
- package/src/superlocalmemory/hooks/cross_platform_connector.py +90 -0
- package/src/superlocalmemory/hooks/cursor_adapter.py +195 -0
- package/src/superlocalmemory/hooks/hook_handlers.py +109 -8
- package/src/superlocalmemory/hooks/ide_connector.py +25 -2
- package/src/superlocalmemory/hooks/post_tool_async_hook.py +165 -0
- package/src/superlocalmemory/hooks/post_tool_outcome_hook.py +223 -0
- package/src/superlocalmemory/hooks/prewarm_auth.py +170 -0
- package/src/superlocalmemory/hooks/session_registry.py +186 -0
- package/src/superlocalmemory/hooks/stop_outcome_hook.py +134 -0
- package/src/superlocalmemory/hooks/sync_loop.py +114 -0
- package/src/superlocalmemory/hooks/user_prompt_hook.py +128 -0
- package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +202 -0
- package/src/superlocalmemory/infra/backup.py +3 -3
- package/src/superlocalmemory/infra/cloud_backup.py +2 -2
- package/src/superlocalmemory/infra/event_bus.py +2 -2
- package/src/superlocalmemory/infra/webhook_dispatcher.py +3 -3
- package/src/superlocalmemory/learning/arm_catalog.py +99 -0
- package/src/superlocalmemory/learning/bandit.py +526 -0
- package/src/superlocalmemory/learning/bandit_cache.py +133 -0
- package/src/superlocalmemory/learning/behavioral.py +53 -1
- package/src/superlocalmemory/learning/consolidation_cycle.py +381 -0
- package/src/superlocalmemory/learning/consolidation_worker.py +188 -520
- package/src/superlocalmemory/learning/database.py +256 -0
- package/src/superlocalmemory/learning/dedup_hnsw.py +413 -0
- package/src/superlocalmemory/learning/ensemble.py +300 -0
- package/src/superlocalmemory/learning/fact_outcome_joins.py +207 -0
- package/src/superlocalmemory/learning/forgetting_scheduler.py +55 -0
- package/src/superlocalmemory/learning/hnsw_dedup.py +69 -0
- package/src/superlocalmemory/learning/labeler.py +87 -0
- package/src/superlocalmemory/learning/legacy_migration.py +277 -0
- package/src/superlocalmemory/learning/memory_merge.py +160 -0
- package/src/superlocalmemory/learning/model_cache.py +269 -0
- package/src/superlocalmemory/learning/model_rollback.py +278 -0
- package/src/superlocalmemory/learning/outcome_queue.py +284 -0
- package/src/superlocalmemory/learning/pattern_miner.py +415 -0
- package/src/superlocalmemory/learning/pattern_miner_constants.py +47 -0
- package/src/superlocalmemory/learning/ranker.py +225 -81
- package/src/superlocalmemory/learning/ranker_common.py +163 -0
- package/src/superlocalmemory/learning/ranker_retrain_legacy.py +202 -0
- package/src/superlocalmemory/learning/ranker_retrain_online.py +411 -0
- package/src/superlocalmemory/learning/reward.py +777 -0
- package/src/superlocalmemory/learning/reward_archive.py +210 -0
- package/src/superlocalmemory/learning/reward_boost.py +201 -0
- package/src/superlocalmemory/learning/reward_proxy.py +326 -0
- package/src/superlocalmemory/learning/shadow_test.py +524 -0
- package/src/superlocalmemory/learning/signal_worker.py +270 -0
- package/src/superlocalmemory/learning/signals.py +314 -0
- package/src/superlocalmemory/learning/trigram_index.py +547 -0
- package/src/superlocalmemory/mcp/server.py +5 -5
- package/src/superlocalmemory/mcp/tools_context.py +183 -0
- package/src/superlocalmemory/mcp/tools_core.py +92 -27
- package/src/superlocalmemory/parameterization/soft_prompt_generator.py +13 -0
- package/src/superlocalmemory/retrieval/engine.py +52 -0
- package/src/superlocalmemory/server/api.py +2 -2
- package/src/superlocalmemory/server/bandit_loops.py +140 -0
- package/src/superlocalmemory/server/middleware/__init__.py +11 -0
- package/src/superlocalmemory/server/middleware/security_headers.py +144 -0
- package/src/superlocalmemory/server/routes/backup.py +36 -13
- package/src/superlocalmemory/server/routes/behavioral.py +50 -19
- package/src/superlocalmemory/server/routes/brain.py +1234 -0
- package/src/superlocalmemory/server/routes/data_io.py +4 -4
- package/src/superlocalmemory/server/routes/events.py +2 -2
- package/src/superlocalmemory/server/routes/helpers.py +1 -1
- package/src/superlocalmemory/server/routes/learning.py +192 -7
- package/src/superlocalmemory/server/routes/memories.py +189 -1
- package/src/superlocalmemory/server/routes/prewarm.py +171 -0
- package/src/superlocalmemory/server/routes/profiles.py +3 -3
- package/src/superlocalmemory/server/routes/token.py +88 -0
- package/src/superlocalmemory/server/routes/ws.py +5 -5
- package/src/superlocalmemory/server/security_middleware.py +13 -7
- package/src/superlocalmemory/server/ui.py +2 -2
- package/src/superlocalmemory/server/unified_daemon.py +335 -3
- package/src/superlocalmemory/skills/slm-build-graph/SKILL.md +423 -0
- package/src/superlocalmemory/skills/slm-list-recent/SKILL.md +348 -0
- package/src/superlocalmemory/skills/slm-recall/SKILL.md +343 -0
- package/src/superlocalmemory/skills/slm-remember/SKILL.md +194 -0
- package/src/superlocalmemory/skills/slm-show-patterns/SKILL.md +224 -0
- package/src/superlocalmemory/skills/slm-status/SKILL.md +363 -0
- package/src/superlocalmemory/skills/slm-switch-profile/SKILL.md +442 -0
- package/src/superlocalmemory/storage/migration_runner.py +545 -0
- package/src/superlocalmemory/storage/migrations/M001_add_signal_features_columns.py +67 -0
- package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +132 -0
- package/src/superlocalmemory/storage/migrations/M003_migration_log.py +38 -0
- package/src/superlocalmemory/storage/migrations/M004_cross_platform_sync_log.py +46 -0
- package/src/superlocalmemory/storage/migrations/M005_bandit_tables.py +75 -0
- package/src/superlocalmemory/storage/migrations/M006_action_outcomes_reward.py +75 -0
- package/src/superlocalmemory/storage/migrations/M007_pending_outcomes.py +63 -0
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +54 -0
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +75 -0
- package/src/superlocalmemory/storage/migrations/M011_archive_and_merge.py +87 -0
- package/src/superlocalmemory/storage/migrations/M012_shadow_observations.py +72 -0
- package/src/superlocalmemory/storage/migrations/M013_bi_temporal_columns.py +55 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +81 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/ui/css/brain.css +409 -0
- package/src/superlocalmemory/ui/css/legacy-dashboard.css +645 -0
- package/src/superlocalmemory/ui/index.html +459 -1345
- package/src/superlocalmemory/ui/js/brain.js +1321 -0
- package/src/superlocalmemory/ui/js/clusters.js +123 -4
- package/src/superlocalmemory/ui/js/init.js +48 -39
- package/src/superlocalmemory/ui/js/memories.js +88 -2
- package/src/superlocalmemory/ui/js/modal.js +71 -1
- package/src/superlocalmemory/ui/js/ng-shell.js +101 -88
- package/src/superlocalmemory/ui/js/trust-dashboard.js +168 -25
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/bootstrap-icons.css +2018 -0
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/bootstrap.bundle.min.js +7 -0
- package/src/superlocalmemory/ui/vendor/bootstrap.min.css +6 -0
- package/src/superlocalmemory/ui/vendor/d3.v7.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/graphology-library.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/graphology.umd.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/inter-variable.min.css +8 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable-Italic.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/sigma.min.js +1 -0
- package/src/superlocalmemory/ui/js/behavioral.js +0 -447
- package/src/superlocalmemory/ui/js/graph-core.js +0 -447
- package/src/superlocalmemory/ui/js/graph-interactions.js +0 -351
- package/src/superlocalmemory/ui/js/learning.js +0 -435
- package/src/superlocalmemory/ui/js/patterns.js +0 -93
- package/src/superlocalmemory.egg-info/PKG-INFO +0 -647
- package/src/superlocalmemory.egg-info/SOURCES.txt +0 -335
- package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
- package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
- package/src/superlocalmemory.egg-info/requires.txt +0 -58
- package/src/superlocalmemory.egg-info/top_level.txt +0 -1
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory v3.4.22 — F4.A Stage-8 H-18/H-06 fix
|
|
4
|
+
|
|
5
|
+
"""Reward-gated Ebbinghaus archive.
|
|
6
|
+
|
|
7
|
+
Flags Ebbinghaus-cold facts as archived *only* when they show no
|
|
8
|
+
positive reward in the last 60 days AND are not marked important
|
|
9
|
+
(LLD-12 §4). Writes a payload-preserving row to ``memory_archive`` and
|
|
10
|
+
updates ``atomic_facts.archive_status='archived'``.
|
|
11
|
+
|
|
12
|
+
**Never issues DELETE FROM atomic_facts** (SOUL directive, LLD-12 §1 —
|
|
13
|
+
memory is sacred across v3.4.22 with 18 000 live users).
|
|
14
|
+
|
|
15
|
+
JSON1 join helper (``iter_outcomes_for_fact``) replaces the former
|
|
16
|
+
``fact_ids_json LIKE '%"<fid>"%'`` pattern so that overlapping fact_id
|
|
17
|
+
prefixes no longer collide (H-06).
|
|
18
|
+
|
|
19
|
+
Contract refs:
|
|
20
|
+
- LLD-12 §4 — reward-gated archive criteria.
|
|
21
|
+
- Stage 8 H-18 — split from monolithic hnsw_dedup.py.
|
|
22
|
+
- Stage 8 H-06 — JSON1 replaces fragile LIKE.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import logging
|
|
29
|
+
import sqlite3
|
|
30
|
+
import uuid
|
|
31
|
+
from datetime import datetime, timezone
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
from superlocalmemory.learning.fact_outcome_joins import (
|
|
35
|
+
has_recent_positive_reward,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
REWARD_WINDOW_DAYS: int = 60
|
|
42
|
+
ARCHIVE_REWARD_THRESHOLD: float = 0.3
|
|
43
|
+
|
|
44
|
+
# SEC-L3 — soft cap on ``memory_archive.payload_json`` length. The DDL
|
|
45
|
+
# in M011 is ``TEXT NOT NULL`` with no column cap (SQLite has no native
|
|
46
|
+
# column-length limit), so a runaway fact could write multi-MB blobs
|
|
47
|
+
# and blow past the MASTER-PLAN §2 I4 disk budget. Enforced on the
|
|
48
|
+
# write path; oversize payloads are truncated with a breadcrumb in the
|
|
49
|
+
# ``reason`` column so an operator can still retrieve the original
|
|
50
|
+
# fact via ``atomic_facts`` (archive does not DELETE).
|
|
51
|
+
PAYLOAD_JSON_MAX_BYTES: int = 262_144
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
__all__ = (
|
|
55
|
+
"run_reward_gated_archive",
|
|
56
|
+
"REWARD_WINDOW_DAYS",
|
|
57
|
+
"ARCHIVE_REWARD_THRESHOLD",
|
|
58
|
+
"PAYLOAD_JSON_MAX_BYTES",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _iso_now() -> str:
|
|
63
|
+
return datetime.now(timezone.utc).isoformat()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def run_reward_gated_archive(
|
|
67
|
+
memory_db_path: str | Path,
|
|
68
|
+
profile_id: str,
|
|
69
|
+
*,
|
|
70
|
+
candidate_fact_ids: list[str],
|
|
71
|
+
) -> list[str]:
|
|
72
|
+
"""Archive candidate facts that have no positive reward in 60 days and
|
|
73
|
+
are not flagged important. Returns the list of fact_ids archived.
|
|
74
|
+
|
|
75
|
+
LLD-12 §1 hard invariant: this function NEVER issues
|
|
76
|
+
``DELETE FROM atomic_facts``. It UPDATEs archive_status + writes a
|
|
77
|
+
payload snapshot to ``memory_archive``.
|
|
78
|
+
"""
|
|
79
|
+
if not candidate_fact_ids:
|
|
80
|
+
return []
|
|
81
|
+
|
|
82
|
+
archived: list[str] = []
|
|
83
|
+
conn = sqlite3.connect(str(memory_db_path), timeout=10.0)
|
|
84
|
+
conn.row_factory = sqlite3.Row
|
|
85
|
+
conn.execute("PRAGMA busy_timeout=2000")
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
placeholders = ",".join("?" for _ in candidate_fact_ids)
|
|
89
|
+
rows = conn.execute(
|
|
90
|
+
f"SELECT fact_id, content, canonical_entities_json, importance, "
|
|
91
|
+
f" confidence, embedding, created_at "
|
|
92
|
+
f"FROM atomic_facts "
|
|
93
|
+
f"WHERE profile_id = ? AND fact_id IN ({placeholders}) "
|
|
94
|
+
f" AND (archive_status IS NULL OR archive_status = 'live')",
|
|
95
|
+
(profile_id, *candidate_fact_ids),
|
|
96
|
+
).fetchall()
|
|
97
|
+
|
|
98
|
+
# H-12/H-P-02: compute the archivable set BEFORE acquiring the
|
|
99
|
+
# writer lock. Previously ``BEGIN IMMEDIATE`` wrapped the full
|
|
100
|
+
# per-candidate reward-lookup loop — holding RESERVED for the
|
|
101
|
+
# entire scan starved concurrent ``record_recall`` writers out
|
|
102
|
+
# with SQLITE_BUSY after their 50 ms busy_timeout. Splitting the
|
|
103
|
+
# read phase keeps the writer lock held only for the bulk
|
|
104
|
+
# INSERT/UPDATE pass.
|
|
105
|
+
to_archive: list[dict] = []
|
|
106
|
+
for row in rows:
|
|
107
|
+
fid = row["fact_id"]
|
|
108
|
+
# 1. Important flag skip (LLD-12 §4 criterion 3).
|
|
109
|
+
if float(row["importance"] or 0.0) >= 1.0:
|
|
110
|
+
continue
|
|
111
|
+
# 2. Recent positive reward skip (criterion 2).
|
|
112
|
+
# H-06 fix — JSON1 equality join via helper.
|
|
113
|
+
if has_recent_positive_reward(
|
|
114
|
+
conn, profile_id, fid,
|
|
115
|
+
min_reward=ARCHIVE_REWARD_THRESHOLD,
|
|
116
|
+
window_days=REWARD_WINDOW_DAYS,
|
|
117
|
+
):
|
|
118
|
+
continue
|
|
119
|
+
to_archive.append({
|
|
120
|
+
"fid": fid,
|
|
121
|
+
"content": row["content"],
|
|
122
|
+
"canonical_entities_json": row["canonical_entities_json"],
|
|
123
|
+
"importance": row["importance"],
|
|
124
|
+
"confidence": row["confidence"],
|
|
125
|
+
"embedding": row["embedding"],
|
|
126
|
+
"created_at": row["created_at"],
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
if not to_archive:
|
|
130
|
+
return []
|
|
131
|
+
|
|
132
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
133
|
+
# S9-SKEP-07: re-verify reward under RESERVED lock. Between the
|
|
134
|
+
# read-only reward scan above and this BEGIN IMMEDIATE another
|
|
135
|
+
# writer may have inserted a positive reward row ("user liked
|
|
136
|
+
# the memory we are about to archive"). With the writer lock
|
|
137
|
+
# held we now know no further inserts are landing; one last
|
|
138
|
+
# check per entry catches everything that raced in.
|
|
139
|
+
verified: list[dict] = []
|
|
140
|
+
for entry in to_archive:
|
|
141
|
+
if has_recent_positive_reward(
|
|
142
|
+
conn, profile_id, entry["fid"],
|
|
143
|
+
min_reward=ARCHIVE_REWARD_THRESHOLD,
|
|
144
|
+
window_days=REWARD_WINDOW_DAYS,
|
|
145
|
+
):
|
|
146
|
+
continue
|
|
147
|
+
verified.append(entry)
|
|
148
|
+
to_archive = verified
|
|
149
|
+
if not to_archive:
|
|
150
|
+
conn.execute("COMMIT")
|
|
151
|
+
return []
|
|
152
|
+
for entry in to_archive:
|
|
153
|
+
fid = entry["fid"]
|
|
154
|
+
payload = {
|
|
155
|
+
"fact_id": fid,
|
|
156
|
+
"content": entry["content"],
|
|
157
|
+
"canonical_entities_json": entry["canonical_entities_json"],
|
|
158
|
+
"importance": entry["importance"],
|
|
159
|
+
"confidence": entry["confidence"],
|
|
160
|
+
"embedding": entry["embedding"],
|
|
161
|
+
"created_at": entry["created_at"],
|
|
162
|
+
}
|
|
163
|
+
# SEC-L3 — cap payload_json at 256 KB. Oversize blobs are
|
|
164
|
+
# replaced with a minimal stub + ``truncated`` reason so the
|
|
165
|
+
# archive row stays within the I4 disk budget while still
|
|
166
|
+
# pointing back to the original ``fact_id`` in atomic_facts.
|
|
167
|
+
payload_str = json.dumps(payload)
|
|
168
|
+
reason = "reward_gated_ebbinghaus"
|
|
169
|
+
if len(payload_str.encode("utf-8")) > PAYLOAD_JSON_MAX_BYTES:
|
|
170
|
+
payload_str = json.dumps({
|
|
171
|
+
"fact_id": fid,
|
|
172
|
+
"truncated": True,
|
|
173
|
+
"original_bytes": len(payload_str.encode("utf-8")),
|
|
174
|
+
})
|
|
175
|
+
reason = "reward_gated_ebbinghaus_truncated"
|
|
176
|
+
logger.warning(
|
|
177
|
+
"memory_archive payload >%d bytes for fact_id=%s; "
|
|
178
|
+
"truncated to stub", PAYLOAD_JSON_MAX_BYTES, fid,
|
|
179
|
+
)
|
|
180
|
+
conn.execute(
|
|
181
|
+
"INSERT INTO memory_archive "
|
|
182
|
+
"(archive_id, fact_id, profile_id, payload_json, "
|
|
183
|
+
" archived_at, reason) VALUES (?, ?, ?, ?, ?, ?)",
|
|
184
|
+
(
|
|
185
|
+
str(uuid.uuid4()),
|
|
186
|
+
fid,
|
|
187
|
+
profile_id,
|
|
188
|
+
payload_str,
|
|
189
|
+
_iso_now(),
|
|
190
|
+
reason,
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
conn.execute(
|
|
194
|
+
"UPDATE atomic_facts "
|
|
195
|
+
"SET archive_status='archived', "
|
|
196
|
+
" archive_reason='reward_gated_ebbinghaus' "
|
|
197
|
+
"WHERE fact_id=? "
|
|
198
|
+
" AND (archive_status IS NULL OR archive_status='live')",
|
|
199
|
+
(fid,),
|
|
200
|
+
)
|
|
201
|
+
archived.append(fid)
|
|
202
|
+
|
|
203
|
+
conn.commit()
|
|
204
|
+
except sqlite3.Error as exc:
|
|
205
|
+
conn.rollback()
|
|
206
|
+
logger.warning("run_reward_gated_archive rollback: %s", exc)
|
|
207
|
+
finally:
|
|
208
|
+
conn.close()
|
|
209
|
+
|
|
210
|
+
return archived
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory v3.4.22 — F4.A Stage-8 H-06/H-18 fix
|
|
4
|
+
|
|
5
|
+
"""Strong-memory boost + reward-aware fact selection.
|
|
6
|
+
|
|
7
|
+
Nudges ``atomic_facts.retrieval_prior`` upward for facts with recurring
|
|
8
|
+
high reward, capped at 0.5 (LLD-12 §5). Also exposes
|
|
9
|
+
``select_high_reward_fact_ids`` for the soft-prompt generator.
|
|
10
|
+
|
|
11
|
+
H-06 regression fix: outcome lookups now use the JSON1-backed
|
|
12
|
+
``fact_outcome_joins`` helper instead of the fragile
|
|
13
|
+
``fact_ids_json LIKE '%"<fid>"%'`` pattern that leaked substring matches
|
|
14
|
+
across overlapping fact_id prefixes.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
import sqlite3
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from superlocalmemory.learning.fact_outcome_joins import (
|
|
24
|
+
aggregate_reward_for_fact,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# H-12/H-P-01: single-pass JSON1 aggregation across ALL facts for a profile.
|
|
29
|
+
# Returns ``{fact_id: (count, mean_reward)}`` for outcomes with reward NOT
|
|
30
|
+
# NULL. Replaces the per-fact O(F) loop of ``aggregate_reward_for_fact``
|
|
31
|
+
# with one GROUP BY scan — O(F+O) instead of O(F·O). JSON1 has been
|
|
32
|
+
# mandatory since v3.4.22 (see ``fact_outcome_joins._json1_available``
|
|
33
|
+
# contract in the module docstring); if it is missing at runtime the
|
|
34
|
+
# caller falls back to the per-fact helper which retains its own
|
|
35
|
+
# LIKE-based shim.
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# S9-W3 H-PERF-03: consolidation invokes ``apply_strong_memory_boost``
|
|
39
|
+
# AND ``select_high_reward_fact_ids`` in the same cycle. Both call
|
|
40
|
+
# ``_bulk_fact_reward_stats`` which is a full GROUP BY scan — at 100k
|
|
41
|
+
# outcomes that's 1-3 s × 2 = 2-6 s wasted inside the 5-min cap. We
|
|
42
|
+
# memoise the result with a short TTL so consecutive calls within the
|
|
43
|
+
# same consolidation cycle share the stats. Key is (id(conn),
|
|
44
|
+
# profile_id) so different conns / profiles get independent caches.
|
|
45
|
+
# TTL expires quickly so live recall updates are reflected within one
|
|
46
|
+
# cycle of the consolidation loop.
|
|
47
|
+
_BULK_STATS_TTL_SEC: float = 30.0
|
|
48
|
+
_bulk_stats_cache: dict[tuple[int, str], tuple[float, dict[str, tuple[int, float]]]] = {}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# S9-W3 M-PERF-07: module-level MISS constant so ``stats.get(fid, _MISS)``
|
|
52
|
+
# does not allocate a fresh ``(0, 0.0)`` tuple per call. At 100k facts
|
|
53
|
+
# that saved ~2 MB of short-lived garbage per consolidation cycle.
|
|
54
|
+
_MISS: tuple[int, float] = (0, 0.0)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _bulk_fact_reward_stats(
|
|
58
|
+
conn: sqlite3.Connection, profile_id: str,
|
|
59
|
+
) -> dict[str, tuple[int, float]]:
|
|
60
|
+
import time as _time
|
|
61
|
+
now = _time.monotonic()
|
|
62
|
+
key = (id(conn), profile_id)
|
|
63
|
+
cached = _bulk_stats_cache.get(key)
|
|
64
|
+
if cached is not None:
|
|
65
|
+
ts, result = cached
|
|
66
|
+
if now - ts < _BULK_STATS_TTL_SEC:
|
|
67
|
+
return result
|
|
68
|
+
try:
|
|
69
|
+
rows = conn.execute(
|
|
70
|
+
"SELECT j.value AS fact_id, "
|
|
71
|
+
" COUNT(*) AS c, "
|
|
72
|
+
" AVG(reward) AS m "
|
|
73
|
+
"FROM action_outcomes a, json_each(a.fact_ids_json) j "
|
|
74
|
+
"WHERE a.profile_id = ? AND a.reward IS NOT NULL "
|
|
75
|
+
"GROUP BY j.value",
|
|
76
|
+
(profile_id,),
|
|
77
|
+
).fetchall()
|
|
78
|
+
except sqlite3.OperationalError:
|
|
79
|
+
# JSON1 missing — signal to caller to fall back. Do NOT cache
|
|
80
|
+
# this (empty) result: a subsequent call on the same conn may
|
|
81
|
+
# fall through to the per-fact loop intentionally.
|
|
82
|
+
return {}
|
|
83
|
+
out: dict[str, tuple[int, float]] = {}
|
|
84
|
+
for fid, c, m in rows:
|
|
85
|
+
if fid is None:
|
|
86
|
+
continue
|
|
87
|
+
out[str(fid)] = (int(c or 0), float(m or 0.0))
|
|
88
|
+
_bulk_stats_cache[key] = (now, out)
|
|
89
|
+
# Prune the cache if it grows beyond 64 entries (multi-profile envs).
|
|
90
|
+
if len(_bulk_stats_cache) > 64:
|
|
91
|
+
stale = [k for k, (t, _) in _bulk_stats_cache.items()
|
|
92
|
+
if now - t >= _BULK_STATS_TTL_SEC]
|
|
93
|
+
for k in stale:
|
|
94
|
+
_bulk_stats_cache.pop(k, None)
|
|
95
|
+
return out
|
|
96
|
+
|
|
97
|
+
logger = logging.getLogger(__name__)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
STRONG_BOOST_INCREMENT: float = 0.1
|
|
101
|
+
STRONG_BOOST_CAP: float = 0.5
|
|
102
|
+
STRONG_BOOST_MIN_OUTCOMES: int = 3
|
|
103
|
+
STRONG_BOOST_MIN_MEAN: float = 0.7
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
__all__ = (
|
|
107
|
+
"apply_strong_memory_boost",
|
|
108
|
+
"select_high_reward_fact_ids",
|
|
109
|
+
"STRONG_BOOST_INCREMENT",
|
|
110
|
+
"STRONG_BOOST_CAP",
|
|
111
|
+
"STRONG_BOOST_MIN_OUTCOMES",
|
|
112
|
+
"STRONG_BOOST_MIN_MEAN",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def apply_strong_memory_boost(
|
|
117
|
+
memory_db_path: str | Path, profile_id: str,
|
|
118
|
+
) -> int:
|
|
119
|
+
"""Nudge retrieval_prior up for high-reward facts, capped at 0.5.
|
|
120
|
+
|
|
121
|
+
Eligibility: ≥ MIN_OUTCOMES outcomes with mean reward > MIN_MEAN.
|
|
122
|
+
Effect: retrieval_prior = MIN(retrieval_prior + INCREMENT, CAP).
|
|
123
|
+
|
|
124
|
+
Returns number of rows boosted.
|
|
125
|
+
"""
|
|
126
|
+
conn = sqlite3.connect(str(memory_db_path), timeout=10.0)
|
|
127
|
+
conn.execute("PRAGMA busy_timeout=2000")
|
|
128
|
+
boosted = 0
|
|
129
|
+
try:
|
|
130
|
+
rows = conn.execute(
|
|
131
|
+
"SELECT fact_id FROM atomic_facts WHERE profile_id=? "
|
|
132
|
+
" AND (archive_status IS NULL OR archive_status='live')",
|
|
133
|
+
(profile_id,),
|
|
134
|
+
).fetchall()
|
|
135
|
+
if not rows:
|
|
136
|
+
return 0
|
|
137
|
+
|
|
138
|
+
# H-12/H-P-01: single JSON1 GROUP BY replaces the per-fact loop.
|
|
139
|
+
# Fallback to per-fact helper preserves legacy behaviour on
|
|
140
|
+
# SQLite without JSON1.
|
|
141
|
+
stats = _bulk_fact_reward_stats(conn, profile_id)
|
|
142
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
143
|
+
for (fid,) in rows:
|
|
144
|
+
if stats:
|
|
145
|
+
count, mean = stats.get(fid, _MISS)
|
|
146
|
+
else:
|
|
147
|
+
count, mean = aggregate_reward_for_fact(conn, profile_id, fid)
|
|
148
|
+
if count < STRONG_BOOST_MIN_OUTCOMES:
|
|
149
|
+
continue
|
|
150
|
+
if mean <= STRONG_BOOST_MIN_MEAN:
|
|
151
|
+
continue
|
|
152
|
+
conn.execute(
|
|
153
|
+
"UPDATE atomic_facts "
|
|
154
|
+
"SET retrieval_prior = MIN(COALESCE(retrieval_prior, 0) + ?, ?) "
|
|
155
|
+
"WHERE fact_id=?",
|
|
156
|
+
(STRONG_BOOST_INCREMENT, STRONG_BOOST_CAP, fid),
|
|
157
|
+
)
|
|
158
|
+
boosted += 1
|
|
159
|
+
conn.commit()
|
|
160
|
+
except sqlite3.Error as exc:
|
|
161
|
+
conn.rollback()
|
|
162
|
+
logger.warning("apply_strong_memory_boost rollback: %s", exc)
|
|
163
|
+
finally:
|
|
164
|
+
conn.close()
|
|
165
|
+
return boosted
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def select_high_reward_fact_ids(
|
|
169
|
+
memory_db_path: str | Path,
|
|
170
|
+
profile_id: str,
|
|
171
|
+
*,
|
|
172
|
+
min_reward: float = 0.6,
|
|
173
|
+
min_outcomes: int = 1,
|
|
174
|
+
) -> list[str]:
|
|
175
|
+
"""Return fact_ids whose mean outcome reward ≥ ``min_reward``.
|
|
176
|
+
|
|
177
|
+
Used by ``soft_prompt_generator`` to mine only high-reward facts
|
|
178
|
+
(LLD-12 §6). JSON1-backed — no substring false positives.
|
|
179
|
+
"""
|
|
180
|
+
conn = sqlite3.connect(str(memory_db_path), timeout=10.0)
|
|
181
|
+
try:
|
|
182
|
+
fact_rows = conn.execute(
|
|
183
|
+
"SELECT fact_id FROM atomic_facts WHERE profile_id=? "
|
|
184
|
+
" AND (archive_status IS NULL OR archive_status='live')",
|
|
185
|
+
(profile_id,),
|
|
186
|
+
).fetchall()
|
|
187
|
+
# H-12/H-P-01: bulk aggregate replaces per-fact loop.
|
|
188
|
+
stats = _bulk_fact_reward_stats(conn, profile_id)
|
|
189
|
+
out: list[str] = []
|
|
190
|
+
for (fid,) in fact_rows:
|
|
191
|
+
if stats:
|
|
192
|
+
count, mean = stats.get(fid, _MISS)
|
|
193
|
+
else:
|
|
194
|
+
count, mean = aggregate_reward_for_fact(conn, profile_id, fid)
|
|
195
|
+
if count < min_outcomes:
|
|
196
|
+
continue
|
|
197
|
+
if mean >= min_reward:
|
|
198
|
+
out.append(fid)
|
|
199
|
+
return out
|
|
200
|
+
finally:
|
|
201
|
+
conn.close()
|