superlocalmemory 4.1.6 → 4.1.8
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 +46 -0
- package/README.md +3 -3
- package/package.json +3 -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 +49 -0
- package/plugin-src/agents/slm-optimize-advisor.md +44 -0
- 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 +5 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/host_upgrades.py +21 -7
- package/src/superlocalmemory/core/engine.py +7 -1
- package/src/superlocalmemory/core/recall_pipeline.py +15 -5
- package/src/superlocalmemory/core/session_identity.py +14 -1
- package/src/superlocalmemory/hooks/codex_assets.py +165 -45
- package/src/superlocalmemory/hooks/post_tool_outcome_hook.py +122 -0
- package/src/superlocalmemory/learning/bandit.py +22 -2
- package/src/superlocalmemory/learning/engagement_features.py +279 -0
- package/src/superlocalmemory/learning/outcome_queue.py +14 -0
- package/src/superlocalmemory/learning/propensity.py +131 -0
- package/src/superlocalmemory/learning/reward.py +42 -16
- package/src/superlocalmemory/learning/reward_model.py +144 -0
- package/src/superlocalmemory/learning/reward_proxy.py +148 -22
- package/src/superlocalmemory/server/routes/v3_api.py +4 -3
- package/src/superlocalmemory/storage/migrations/M048_upcoming_holds_only_what_is_upcoming.py +22 -0
|
@@ -34,6 +34,11 @@ from datetime import datetime, timedelta, timezone
|
|
|
34
34
|
from pathlib import Path
|
|
35
35
|
from typing import Any
|
|
36
36
|
|
|
37
|
+
from superlocalmemory.learning.engagement_features import (
|
|
38
|
+
EngagementFeatures,
|
|
39
|
+
extract_features,
|
|
40
|
+
)
|
|
41
|
+
from superlocalmemory.learning.reward_model import score
|
|
37
42
|
from superlocalmemory.core.topic_signature import compute_topic_signature
|
|
38
43
|
from superlocalmemory.learning.bandit import ContextualBandit
|
|
39
44
|
|
|
@@ -105,10 +110,10 @@ def _shown_fact_ids(
|
|
|
105
110
|
"""The fact_ids this play recorded at recall time (M044), or [].
|
|
106
111
|
|
|
107
112
|
Preferred over the ``learning_signals`` lookup below because it does not
|
|
108
|
-
depend on the exposure enqueue, which is off
|
|
109
|
-
|
|
110
|
-
signals rows there
|
|
111
|
-
|
|
113
|
+
depend on the exposure enqueue, which is off because it wrote a row per
|
|
114
|
+
displayed memory per query and badly inflated the ranking-phase counter.
|
|
115
|
+
With no signals rows there is nothing to look for, so plays fell through to
|
|
116
|
+
the age-based default and arms stayed on their priors.
|
|
112
117
|
|
|
113
118
|
Returns [] on a store where M044 has not run — "no such column" is an
|
|
114
119
|
``sqlite3.Error`` and is caught, so an unmigrated install falls back to the
|
|
@@ -366,6 +371,98 @@ def _default_deadline(
|
|
|
366
371
|
)
|
|
367
372
|
|
|
368
373
|
|
|
374
|
+
#: Once a play is older than this with nothing observed, it is closed without
|
|
375
|
+
#: touching the posterior. Leaving it open forever would make every settler
|
|
376
|
+
#: pass rescan it; settling it at a neutral value is what this module stopped
|
|
377
|
+
#: doing. Closed-and-unjudged is the third option that was missing.
|
|
378
|
+
_ABSTAIN_EXPIRY_SEC = 900
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _expire_play(learning_conn: sqlite3.Connection, play_id: int) -> bool:
|
|
382
|
+
"""Close a play without applying any reward.
|
|
383
|
+
|
|
384
|
+
The posterior is deliberately untouched: nothing was observed, so there is
|
|
385
|
+
nothing to learn, and a neutral update would shrink the arm's variance
|
|
386
|
+
around its prior and make it harder to move once evidence does arrive.
|
|
387
|
+
"""
|
|
388
|
+
try:
|
|
389
|
+
learning_conn.execute(
|
|
390
|
+
"UPDATE bandit_plays SET settled_at = ?, settlement_type = ? "
|
|
391
|
+
"WHERE play_id = ? AND settled_at IS NULL",
|
|
392
|
+
(datetime.now(timezone.utc).isoformat(), "unobserved", int(play_id)),
|
|
393
|
+
)
|
|
394
|
+
learning_conn.commit()
|
|
395
|
+
return True
|
|
396
|
+
except sqlite3.Error as exc:
|
|
397
|
+
logger.debug("reward_proxy: expire play %s: %s", play_id, exc)
|
|
398
|
+
return False
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _session_for_play(
|
|
402
|
+
memory_conn: sqlite3.Connection | None,
|
|
403
|
+
query_id: str,
|
|
404
|
+
played_at: datetime,
|
|
405
|
+
profile_id: str,
|
|
406
|
+
) -> str:
|
|
407
|
+
"""The conversation this play happened in, or "" when it cannot be named.
|
|
408
|
+
|
|
409
|
+
The play and its outcome ticket are minted with different uuids on the same
|
|
410
|
+
recall, so the query_id is tried first and a time-and-profile match is the
|
|
411
|
+
fallback. An empty answer means no observation is possible, which the
|
|
412
|
+
caller turns into an abstention rather than a guess.
|
|
413
|
+
"""
|
|
414
|
+
if memory_conn is None:
|
|
415
|
+
return ""
|
|
416
|
+
try:
|
|
417
|
+
row = memory_conn.execute(
|
|
418
|
+
"SELECT session_id FROM pending_outcomes WHERE recall_query_id = ? "
|
|
419
|
+
"LIMIT 1", (str(query_id),),
|
|
420
|
+
).fetchone()
|
|
421
|
+
if row and row[0]:
|
|
422
|
+
return str(row[0])
|
|
423
|
+
window_ms = 5000
|
|
424
|
+
centre = int(played_at.timestamp() * 1000)
|
|
425
|
+
row = memory_conn.execute(
|
|
426
|
+
"SELECT session_id FROM pending_outcomes "
|
|
427
|
+
"WHERE profile_id = ? AND created_at_ms BETWEEN ? AND ? "
|
|
428
|
+
"ORDER BY ABS(created_at_ms - ?) LIMIT 1",
|
|
429
|
+
(str(profile_id), centre - window_ms, centre + window_ms, centre),
|
|
430
|
+
).fetchone()
|
|
431
|
+
return str(row[0]) if row and row[0] else ""
|
|
432
|
+
except sqlite3.Error:
|
|
433
|
+
return ""
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _ips_for_play(
|
|
437
|
+
learning_conn: sqlite3.Connection, play_id: int, stratum: str, profile_id: str,
|
|
438
|
+
):
|
|
439
|
+
"""Inverse-propensity weight for this play against its stratum's arms."""
|
|
440
|
+
from superlocalmemory.learning.propensity import ips_weight
|
|
441
|
+
|
|
442
|
+
try:
|
|
443
|
+
row = learning_conn.execute(
|
|
444
|
+
"SELECT arm_id FROM bandit_plays WHERE play_id = ?", (int(play_id),),
|
|
445
|
+
).fetchone()
|
|
446
|
+
if not row or not row[0]:
|
|
447
|
+
return ips_weight(None, None)
|
|
448
|
+
arm_id = str(row[0])
|
|
449
|
+
rows = learning_conn.execute(
|
|
450
|
+
"SELECT arm_id, alpha, beta FROM bandit_arms "
|
|
451
|
+
"WHERE profile_id = ? AND stratum = ?",
|
|
452
|
+
(str(profile_id), str(stratum or "")),
|
|
453
|
+
).fetchall()
|
|
454
|
+
except sqlite3.Error:
|
|
455
|
+
return ips_weight(None, None)
|
|
456
|
+
|
|
457
|
+
mine, others = None, []
|
|
458
|
+
for candidate, alpha, beta in rows:
|
|
459
|
+
if str(candidate) == arm_id:
|
|
460
|
+
mine = (float(alpha), float(beta))
|
|
461
|
+
else:
|
|
462
|
+
others.append((float(alpha), float(beta)))
|
|
463
|
+
return ips_weight(mine, others)
|
|
464
|
+
|
|
465
|
+
|
|
369
466
|
def settle_stale_plays(
|
|
370
467
|
profile_id: str,
|
|
371
468
|
db_path: Path | str,
|
|
@@ -406,27 +503,56 @@ def settle_stale_plays(
|
|
|
406
503
|
_shown_fact_ids(learning_conn, row["play_id"])
|
|
407
504
|
or _top3_fact_ids(learning_conn, row["query_id"])
|
|
408
505
|
)
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
506
|
+
# The ladder this replaces asked one question — did a recalled
|
|
507
|
+
# fact_id appear verbatim in a later tool event — and answered 0.5
|
|
508
|
+
# whenever it could not tell. Both halves failed: nothing makes an
|
|
509
|
+
# agent echo the marker, and 0.5 is not an absence of judgement but
|
|
510
|
+
# a confident one, tightening the posterior around its prior.
|
|
511
|
+
requeried = bool(
|
|
512
|
+
memory_conn is not None and _requery_detected(
|
|
513
|
+
memory_conn, played, row["query_id"],
|
|
514
|
+
profile_id=str(profile_id),
|
|
515
|
+
)
|
|
516
|
+
)
|
|
517
|
+
marker_hit = bool(
|
|
518
|
+
memory_conn is not None and _tool_event_hit(
|
|
519
|
+
memory_conn, played, top3, profile_id=str(profile_id),
|
|
520
|
+
)
|
|
521
|
+
)
|
|
522
|
+
session_id = _session_for_play(
|
|
523
|
+
memory_conn, str(row["query_id"] or ""), played, str(profile_id),
|
|
524
|
+
)
|
|
525
|
+
if memory_conn is not None and session_id:
|
|
526
|
+
features = extract_features(
|
|
527
|
+
memory_conn,
|
|
528
|
+
session_id=session_id,
|
|
529
|
+
profile_id=str(profile_id),
|
|
530
|
+
fact_ids=top3,
|
|
531
|
+
recalled_at=played,
|
|
532
|
+
requeried=requeried,
|
|
533
|
+
marker_hit=marker_hit,
|
|
534
|
+
)
|
|
425
535
|
else:
|
|
426
|
-
|
|
536
|
+
features = EngagementFeatures(
|
|
537
|
+
requeried=requeried, marker_hit=marker_hit,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
decision = score(features)
|
|
541
|
+
if decision.reward is None:
|
|
542
|
+
# Nothing observed. Wait while the window is still open, then
|
|
543
|
+
# close the play unjudged rather than inventing a number.
|
|
544
|
+
if age > _ABSTAIN_EXPIRY_SEC:
|
|
545
|
+
_expire_play(learning_conn, int(row["play_id"]))
|
|
427
546
|
continue
|
|
428
547
|
|
|
429
|
-
|
|
548
|
+
estimate = _ips_for_play(
|
|
549
|
+
learning_conn, int(row["play_id"]),
|
|
550
|
+
str(row["stratum"] or ""), str(profile_id),
|
|
551
|
+
)
|
|
552
|
+
if bandit.update(
|
|
553
|
+
int(row["play_id"]), decision.reward,
|
|
554
|
+
kind=decision.kind, weight=estimate.weight,
|
|
555
|
+
):
|
|
430
556
|
settled += 1
|
|
431
557
|
finally:
|
|
432
558
|
try:
|
|
@@ -12,6 +12,7 @@ from pathlib import Path
|
|
|
12
12
|
import os
|
|
13
13
|
from fastapi import APIRouter, HTTPException, Request
|
|
14
14
|
from fastapi.responses import JSONResponse
|
|
15
|
+
from superlocalmemory.core.session_identity import synthetic_session_id
|
|
15
16
|
from superlocalmemory.core.config import CANONICAL_RECALL_LIMIT
|
|
16
17
|
from superlocalmemory.core.status_contract import (
|
|
17
18
|
COUNT_QUERIES,
|
|
@@ -36,15 +37,15 @@ def _signal_session_id() -> str:
|
|
|
36
37
|
|
|
37
38
|
agent = str(get_current_agent_id() or "").strip()
|
|
38
39
|
if agent:
|
|
39
|
-
return
|
|
40
|
+
return synthetic_session_id("agent", agent)
|
|
40
41
|
except Exception: # noqa: BLE001 -- naming the caller must never fail a read
|
|
41
42
|
pass
|
|
42
43
|
try:
|
|
43
44
|
from superlocalmemory.server.routes.helpers import get_active_profile
|
|
44
45
|
|
|
45
|
-
return
|
|
46
|
+
return synthetic_session_id("api", str(get_active_profile()))
|
|
46
47
|
except Exception: # noqa: BLE001
|
|
47
|
-
return "api
|
|
48
|
+
return synthetic_session_id("api", "default")
|
|
48
49
|
|
|
49
50
|
|
|
50
51
|
router = APIRouter(prefix="/api/v3", tags=["v3"])
|
package/src/superlocalmemory/storage/migrations/M048_upcoming_holds_only_what_is_upcoming.py
CHANGED
|
@@ -205,3 +205,25 @@ def verify(conn: sqlite3.Connection) -> bool:
|
|
|
205
205
|
)
|
|
206
206
|
return False
|
|
207
207
|
return True
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def blocks_serving(conn: sqlite3.Connection) -> bool:
|
|
211
|
+
"""Should a daemon refuse to serve while this check does not hold? No.
|
|
212
|
+
|
|
213
|
+
Nothing here is about schema. ``verify()`` reads ``content`` and compares
|
|
214
|
+
today's reading of it against the ``fact_type`` already stored, so a False
|
|
215
|
+
means some memories are filed as plans that no longer read as plans. Every
|
|
216
|
+
table and column a query needs is present either way; the store answers
|
|
217
|
+
normally, and at worst a handful of memories carry a stale label until the
|
|
218
|
+
next maintenance pass re-reads them.
|
|
219
|
+
|
|
220
|
+
The distinction matters because this is a standing guard over data that
|
|
221
|
+
ordinary use re-violates by design: a plan whose date passes stops reading
|
|
222
|
+
as upcoming, which is the rule working, not a fault. Treating that like a
|
|
223
|
+
missing table let one drifted row answer 503 on every route for as long as
|
|
224
|
+
the process lived — and because the readiness snapshot is taken once at
|
|
225
|
+
startup, the background pass that repairs the data could not lift the
|
|
226
|
+
refusal it caused. An outage produced by a quality check is worse than the
|
|
227
|
+
thing the check is for. Same reasoning as ``M043.blocks_serving``.
|
|
228
|
+
"""
|
|
229
|
+
return False
|