superlocalmemory 4.1.4 → 4.1.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/.claude-plugin/marketplace.json +2 -2
- package/CHANGELOG.md +32 -0
- package/README.md +3 -3
- 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 +2 -2
- 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 +2 -2
- package/plugin/skills/slm-status/SKILL.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 +2 -2
- 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 +2 -2
- 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/diagnostics_cmd.py +74 -1
- package/src/superlocalmemory/cli/main.py +12 -0
- package/src/superlocalmemory/core/remember_runtime.py +12 -1
- package/src/superlocalmemory/reliability/__init__.py +45 -0
- package/src/superlocalmemory/reliability/join_liveness.py +301 -0
- package/src/superlocalmemory/reliability/prior_distance.py +243 -0
- package/src/superlocalmemory/server/routes/backup.py +1 -1
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"license": "AGPL-3.0-or-later",
|
|
22
22
|
"name": "superlocalmemory",
|
|
23
23
|
"source": "./plugin",
|
|
24
|
-
"version": "4.1.
|
|
24
|
+
"version": "4.1.5"
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"author": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"license": "AGPL-3.0-or-later",
|
|
40
40
|
"name": "superlocalmemory-codex",
|
|
41
41
|
"source": "./codex-plugin",
|
|
42
|
-
"version": "4.1.
|
|
42
|
+
"version": "4.1.5"
|
|
43
43
|
}
|
|
44
44
|
]
|
|
45
45
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,38 @@ All notable changes to SuperLocalMemory will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [4.1.5] — Ask whether a feature is working, not whether it is present
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **`slm diagnostics reliability`** — two read-only checks that answer a question
|
|
12
|
+
neither a code search nor a call trace can: is a wired feature actually taking
|
|
13
|
+
effect on *your* store?
|
|
14
|
+
- The first asks whether the retrieval learner's posteriors have moved away from
|
|
15
|
+
where they started. A learner that records activity and never shifts its
|
|
16
|
+
distribution looks healthy from every counter it keeps; this reports it plainly.
|
|
17
|
+
- The second asks whether a feature gated on a table or column can run here at
|
|
18
|
+
all, and when it cannot, searches the rest of the store for the data it needs
|
|
19
|
+
and tells you where it is, how much of what is needed it covers, and whether
|
|
20
|
+
connecting it would need a backfill.
|
|
21
|
+
Both are read-only, run in a second, and print what they found in plain language.
|
|
22
|
+
Run it on your own store: it may tell you something you assumed was on is not.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- **A refused write said only "temporarily unavailable."** Three unrelated causes
|
|
26
|
+
produced that one message — a busy write journal, another process holding the
|
|
27
|
+
writer, and a rejected stale write. They call for different responses, and one of
|
|
28
|
+
them is not a fault at all. The cause is now named, so a log line says which
|
|
29
|
+
happened instead of leaving you to guess.
|
|
30
|
+
- **The Codex and Antigravity plugins still advertised 4.1.3 after 4.1.4 shipped.**
|
|
31
|
+
Their manifests are generated from a shared source and were not regenerated during
|
|
32
|
+
that release, so both surfaces reported a version they were not. All four editor
|
|
33
|
+
surfaces are regenerated and checked against the package version on every release.
|
|
34
|
+
- **The benchmark instructions could not be followed.** The documented command named
|
|
35
|
+
a directory that does not exist and an interpreter path pointing outside the
|
|
36
|
+
project, and the environment it suggested carries an older release — so anyone
|
|
37
|
+
following it measured the wrong code. Corrected, with a check that the interpreter
|
|
38
|
+
you choose has the version you mean to test.
|
|
39
|
+
|
|
8
40
|
## [4.1.4] — A busy daemon is not a dead daemon
|
|
9
41
|
|
|
10
42
|
### Fixed
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
</picture>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
|
-
<h1 align="center">SuperLocalMemory V4.1.
|
|
8
|
+
<h1 align="center">SuperLocalMemory V4.1.5</h1>
|
|
9
9
|
|
|
10
10
|
<h2 align="center">Rent the LLM. Own the memory.</h2>
|
|
11
11
|
|
|
@@ -27,12 +27,12 @@ guarantee here is stated as a falsifiable invariant, tested under an adversarial
|
|
|
27
27
|
negative control, and shipped with the harness that regenerates the evidence:
|
|
28
28
|
<code>python benchmark/run_all.py --trials 200 --output-dir results/</code>. What each experiment
|
|
29
29
|
does <em>not</em> exercise is stated too.</p>
|
|
30
|
-
<p align="center"><code>v4.1.
|
|
30
|
+
<p align="center"><code>v4.1.5</code> — one control plane: <strong>SLM-Mesh</strong> peer coordination · multi-scope memory (personal / shared / global) · profiles · Cache · Compress · 7-layer retrieval · code graph · Entity Explorer · skill evolution · Modes A/B/C · GDPR retention & audit chain · bounded loops — across CLI, MCP, dashboard, the <strong>Claude plugin</strong>, the <strong>Codex add-on</strong>, and documented IDE integrations.<br/>
|
|
31
31
|
Proxy: <code>slm wrap claude</code> · MCP: add <code>slm_compress</code> to your config · Skill: zero-config</p>
|
|
32
32
|
<p align="center"><strong>Four public arXiv preprints</strong> · V4: <a href="https://arxiv.org/abs/2608.08253">arXiv:2608.08253</a> · companion archive: <a href="https://zenodo.org/records/21853302">Zenodo 21853302</a> (<a href="https://doi.org/10.5281/zenodo.21853302">DOI 10.5281/zenodo.21853302</a>) · prior preprints: <a href="https://arxiv.org/abs/2603.02240">2603.02240</a> · <a href="https://arxiv.org/abs/2603.14588">2603.14588</a> · <a href="https://arxiv.org/abs/2604.04514">2604.04514</a>.</p>
|
|
33
33
|
|
|
34
34
|
<p align="center">
|
|
35
|
-
<a href="CHANGELOG.md"><img src="https://img.shields.io/badge/v4.1.
|
|
35
|
+
<a href="CHANGELOG.md"><img src="https://img.shields.io/badge/v4.1.5-Current_Release-2ea44f?style=for-the-badge&logo=checkmarx&logoColor=white" alt="v4.1.5 — Current Release"/></a>
|
|
36
36
|
<a href="https://arxiv.org/abs/2608.08253"><img src="https://img.shields.io/badge/arXiv-2608.08253-b31b1b?style=for-the-badge&logo=arxiv&logoColor=white" alt="SuperLocalMemory 4.0 paper on arXiv:2608.08253"/></a>
|
|
37
37
|
<a href="https://zenodo.org/records/21853302"><img src="https://img.shields.io/badge/Zenodo-10.5281%2Fzenodo.21853302-1682D4?style=for-the-badge&logo=zenodo&logoColor=white" alt="V4 paper on Zenodo: 10.5281/zenodo.21853302"/></a>
|
|
38
38
|
<a href="https://arxiv.org/abs/2603.14588"><img src="https://img.shields.io/badge/arXiv-2603.14588-b31b1b?style=for-the-badge&logo=arxiv&logoColor=white" alt="arXiv Paper"/></a>
|
package/package.json
CHANGED
package/plugin/CLAUDE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- BEGIN SuperLocalMemory v4.1.
|
|
1
|
+
<!-- BEGIN SuperLocalMemory v4.1.5 -->
|
|
2
2
|
|
|
3
3
|
## SuperLocalMemory (SLM) — Agent Rules
|
|
4
4
|
|
|
@@ -39,6 +39,6 @@ slm-recall · slm-remember · slm-session · slm-status · slm-cache · slm-comp
|
|
|
39
39
|
### Subagents
|
|
40
40
|
slm-memory-advisor (memory decisions, session hygiene, scope/profile guidance) · slm-optimize-advisor (context compression + KV cache) · slm-governance-advisor (scope/roles/compliance/GDPR)
|
|
41
41
|
|
|
42
|
-
<!-- END SuperLocalMemory v4.1.
|
|
42
|
+
<!-- END SuperLocalMemory v4.1.5 -->
|
|
43
43
|
|
|
44
|
-
SuperLocalMemory v4.1.
|
|
44
|
+
SuperLocalMemory v4.1.5 · Qualixar · AGPL-3.0-or-later
|
|
@@ -77,4 +77,4 @@ slm-scope · slm-governance · slm-profile · slm-remember · slm-recall
|
|
|
77
77
|
# What NOT to do
|
|
78
78
|
Never session_init twice; never forget without dry-run preview; never store secrets; never bypass role checks; never claim an erasure succeeded without verifying via recall.
|
|
79
79
|
|
|
80
|
-
SuperLocalMemory v4.1.
|
|
80
|
+
SuperLocalMemory v4.1.5 · Qualixar · AGPL-3.0-or-later
|
|
@@ -46,4 +46,4 @@ slm-recall · slm-remember · slm-session · slm-scope · slm-profile · slm-gov
|
|
|
46
46
|
# What NOT to do
|
|
47
47
|
Never session_init twice; never forget dry_run=False without reporting preview; never dump a whole file into remember; never invent a memory; never claim "saved" without success:true / clean CLI exit; never bypass scope or governance restrictions.
|
|
48
48
|
|
|
49
|
-
SuperLocalMemory v4.1.
|
|
49
|
+
SuperLocalMemory v4.1.5 · Qualixar · AGPL-3.0-or-later
|
|
@@ -41,4 +41,4 @@ slm-compress · slm-cache · slm-status · slm-profile
|
|
|
41
41
|
# What NOT to do
|
|
42
42
|
Never compress code-for-edit/JSON-to-parse/<500 chars; never store secrets/ccr_ids; never let optimize failure block/alter the task; never claim a specific savings %; never carry ccr_ids across profile switches.
|
|
43
43
|
|
|
44
|
-
SuperLocalMemory v4.1.
|
|
44
|
+
SuperLocalMemory v4.1.5 · Qualixar · AGPL-3.0-or-later
|
package/plugin/requirements.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
superlocalmemory==4.1.
|
|
1
|
+
superlocalmemory==4.1.5
|
|
@@ -97,7 +97,7 @@ mesh_send(to=target_id, message="I'm starting work on auth/handler.py — please
|
|
|
97
97
|
mesh_send(to="broadcast", message="Deploying to staging in 5 minutes")
|
|
98
98
|
|
|
99
99
|
# Message all sessions working in the same project
|
|
100
|
-
mesh_send(to="project
|
|
100
|
+
mesh_send(to="project:~/myproject", message="Tests are green on main")
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
**4 KB message cap.** For large payloads (diffs, file contents), write to a file
|
|
@@ -279,4 +279,4 @@ mesh availability.
|
|
|
279
279
|
|
|
280
280
|
---
|
|
281
281
|
|
|
282
|
-
*SuperLocalMemory v4.1.
|
|
282
|
+
*SuperLocalMemory v4.1.5 · Qualixar · AGPL-3.0-or-later*
|
|
@@ -52,7 +52,7 @@ recalls or remembers in that session.
|
|
|
52
52
|
|
|
53
53
|
```
|
|
54
54
|
session_init(
|
|
55
|
-
project_path: str = "", # working directory path, e.g. "
|
|
55
|
+
project_path: str = "", # working directory path, e.g. "~/projects/foo"
|
|
56
56
|
query: str = "", # topic override; if omitted, derived from project_path
|
|
57
57
|
max_results: int = 10, # max memories to return (default: 10)
|
|
58
58
|
max_age_days: int = 30, # suppress memories older than N days unless score >= 0.7
|
|
@@ -253,4 +253,4 @@ explicitly and call `recall` with `include_global`/`include_shared` after
|
|
|
253
253
|
|
|
254
254
|
---
|
|
255
255
|
|
|
256
|
-
*SuperLocalMemory v4.1.
|
|
256
|
+
*SuperLocalMemory v4.1.5 · Qualixar · AGPL-3.0-or-later*
|
|
@@ -137,4 +137,4 @@ When the SLM MCP server is unavailable, use these CLI equivalents:
|
|
|
137
137
|
- **slm-optimize-advisor** — context compression and KV cache
|
|
138
138
|
- **slm-governance-advisor** — scope/role compliance, retention policies, GDPR
|
|
139
139
|
|
|
140
|
-
SuperLocalMemory v4.1.
|
|
140
|
+
SuperLocalMemory v4.1.5 · Qualixar · AGPL-3.0-or-later
|
|
@@ -97,7 +97,7 @@ mesh_send(to=target_id, message="I'm starting work on auth/handler.py — please
|
|
|
97
97
|
mesh_send(to="broadcast", message="Deploying to staging in 5 minutes")
|
|
98
98
|
|
|
99
99
|
# Message all sessions working in the same project
|
|
100
|
-
mesh_send(to="project
|
|
100
|
+
mesh_send(to="project:~/myproject", message="Tests are green on main")
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
**4 KB message cap.** For large payloads (diffs, file contents), write to a file
|
|
@@ -279,4 +279,4 @@ mesh availability.
|
|
|
279
279
|
|
|
280
280
|
---
|
|
281
281
|
|
|
282
|
-
*SuperLocalMemory v4.1.
|
|
282
|
+
*SuperLocalMemory v4.1.5 · Qualixar · AGPL-3.0-or-later*
|
|
@@ -52,7 +52,7 @@ recalls or remembers in that session.
|
|
|
52
52
|
|
|
53
53
|
```
|
|
54
54
|
session_init(
|
|
55
|
-
project_path: str = "", # working directory path, e.g. "
|
|
55
|
+
project_path: str = "", # working directory path, e.g. "~/projects/foo"
|
|
56
56
|
query: str = "", # topic override; if omitted, derived from project_path
|
|
57
57
|
max_results: int = 10, # max memories to return (default: 10)
|
|
58
58
|
max_age_days: int = 30, # suppress memories older than N days unless score >= 0.7
|
|
@@ -253,4 +253,4 @@ explicitly and call `recall` with `include_global`/`include_shared` after
|
|
|
253
253
|
|
|
254
254
|
---
|
|
255
255
|
|
|
256
|
-
*SuperLocalMemory v4.1.
|
|
256
|
+
*SuperLocalMemory v4.1.5 · Qualixar · AGPL-3.0-or-later*
|
package/pyproject.toml
CHANGED
|
@@ -32,7 +32,7 @@ if "OMP_NUM_THREADS" not in os.environ:
|
|
|
32
32
|
os.environ["OMP_NUM_THREADS"] = "2"
|
|
33
33
|
# ---------------------------------------------------------------------------
|
|
34
34
|
|
|
35
|
-
__version__ = "4.1.
|
|
35
|
+
__version__ = "4.1.5"
|
|
36
36
|
|
|
37
37
|
_REQUIRED_VERSIONS = {
|
|
38
38
|
"sentence_transformers": "5.3.0",
|
|
@@ -10,8 +10,11 @@ from argparse import Namespace
|
|
|
10
10
|
|
|
11
11
|
def cmd_diagnostics(args: Namespace) -> None:
|
|
12
12
|
action = getattr(args, "diagnostics_command", None)
|
|
13
|
+
if action == "reliability":
|
|
14
|
+
_cmd_reliability(args)
|
|
15
|
+
return
|
|
13
16
|
if action != "export":
|
|
14
|
-
raise SystemExit("choose a diagnostics subcommand: export")
|
|
17
|
+
raise SystemExit("choose a diagnostics subcommand: export, reliability")
|
|
15
18
|
|
|
16
19
|
from superlocalmemory.cli.json_output import json_print
|
|
17
20
|
from superlocalmemory.infra.local_diagnostics import default_diagnostics
|
|
@@ -25,4 +28,74 @@ def cmd_diagnostics(args: Namespace) -> None:
|
|
|
25
28
|
print("No automatic reporting was enabled.")
|
|
26
29
|
|
|
27
30
|
|
|
31
|
+
def _cmd_reliability(args: Namespace) -> None:
|
|
32
|
+
"""Report whether wired mechanisms are effective, not merely present.
|
|
33
|
+
|
|
34
|
+
``implemented``, ``reachable`` and ``effective`` are three different
|
|
35
|
+
questions. A grep answers the first and an import answers the second; only
|
|
36
|
+
querying this store answers the third. Both checks are read-only.
|
|
37
|
+
"""
|
|
38
|
+
from superlocalmemory.cli.json_output import json_print
|
|
39
|
+
from superlocalmemory.infra.data_root import state_path
|
|
40
|
+
from superlocalmemory.reliability import (
|
|
41
|
+
DEFAULT_MIN_OBSERVATIONS,
|
|
42
|
+
check_beta_learners,
|
|
43
|
+
check_schema_guards,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
floor = getattr(args, "min_observations", None) or DEFAULT_MIN_OBSERVATIONS
|
|
47
|
+
learners = check_beta_learners(state_path("learning.db"), min_observations=floor)
|
|
48
|
+
guards = check_schema_guards(state_path("memory.db"))
|
|
49
|
+
|
|
50
|
+
payload = {
|
|
51
|
+
"learners": [
|
|
52
|
+
{
|
|
53
|
+
"table": v.table,
|
|
54
|
+
"verdict": v.verdict,
|
|
55
|
+
"units": v.units,
|
|
56
|
+
"units_at_prior_mean": v.units_at_prior_mean,
|
|
57
|
+
"units_matching_neutral_identity": v.units_matching_neutral_identity,
|
|
58
|
+
"observations": v.observations,
|
|
59
|
+
"detail": v.detail,
|
|
60
|
+
}
|
|
61
|
+
for v in learners
|
|
62
|
+
],
|
|
63
|
+
"schema_guards": [
|
|
64
|
+
{
|
|
65
|
+
"name": g.name,
|
|
66
|
+
"verdict": g.verdict,
|
|
67
|
+
"missing": list(g.missing),
|
|
68
|
+
"found_elsewhere": [
|
|
69
|
+
{
|
|
70
|
+
"table": tbl,
|
|
71
|
+
"column": col,
|
|
72
|
+
"populated_rows": n,
|
|
73
|
+
# -1 when coverage over the guarded table could not be
|
|
74
|
+
# measured. Populated rows alone overstate the remedy.
|
|
75
|
+
"coverage_pct_of_guarded_table": cov,
|
|
76
|
+
}
|
|
77
|
+
for tbl, col, n, cov in g.found_elsewhere
|
|
78
|
+
],
|
|
79
|
+
"detail": g.detail,
|
|
80
|
+
}
|
|
81
|
+
for g in guards
|
|
82
|
+
],
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if bool(getattr(args, "json", False)):
|
|
86
|
+
json_print("diagnostics-reliability", data=payload)
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
if not learners and not guards:
|
|
90
|
+
print("No Bayesian learners or schema-guarded paths found in this store.")
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
for v in learners:
|
|
94
|
+
print(f"[{v.verdict}] {v.table}")
|
|
95
|
+
print(f" {v.detail}")
|
|
96
|
+
for g in guards:
|
|
97
|
+
print(f"[{g.verdict}] {g.name}")
|
|
98
|
+
print(f" {g.detail}")
|
|
99
|
+
|
|
100
|
+
|
|
28
101
|
__all__ = ["cmd_diagnostics"]
|
|
@@ -875,6 +875,18 @@ def main() -> None:
|
|
|
875
875
|
"export", help="Write a deterministic content-free JSON report",
|
|
876
876
|
)
|
|
877
877
|
diagnostics_export.add_argument("destination")
|
|
878
|
+
diagnostics_reliability = diagnostics_sub.add_parser(
|
|
879
|
+
"reliability",
|
|
880
|
+
help=(
|
|
881
|
+
"Ask whether wired mechanisms are actually effective: has each "
|
|
882
|
+
"Bayesian learner moved off its prior, and has each schema-guarded "
|
|
883
|
+
"path ever executed against this store"
|
|
884
|
+
),
|
|
885
|
+
)
|
|
886
|
+
diagnostics_reliability.add_argument(
|
|
887
|
+
"--min-observations", type=int, default=None,
|
|
888
|
+
help="Observation floor below which an unmoved posterior is not reported",
|
|
889
|
+
)
|
|
878
890
|
diagnostics_export.add_argument(
|
|
879
891
|
"--json", action="store_true", help="Output structured JSON",
|
|
880
892
|
)
|
|
@@ -419,8 +419,19 @@ class CanonicalRememberRuntime:
|
|
|
419
419
|
OwnershipRequiredError,
|
|
420
420
|
WriteCoordinatorError,
|
|
421
421
|
) as exc:
|
|
422
|
+
# Name which of the three it was. They are not interchangeable and
|
|
423
|
+
# they call for different responses: an unavailable journal is I/O
|
|
424
|
+
# or lock contention and worth retrying, lost ownership means
|
|
425
|
+
# another writer holds the lease, and a coordinator error is the
|
|
426
|
+
# same type the generation fence raises to reject a stale epoch.
|
|
427
|
+
# Collapsing all three into one string makes a spurious fence
|
|
428
|
+
# rejection indistinguishable from a transient disk stall, for the
|
|
429
|
+
# operator reading a log and for a caller deciding whether to
|
|
430
|
+
# retry. Only the class name is included: it is the whole of the
|
|
431
|
+
# discriminating information and carries no request content.
|
|
422
432
|
raise CanonicalRememberUnavailable(
|
|
423
|
-
"canonical remember is temporarily unavailable"
|
|
433
|
+
"canonical remember is temporarily unavailable "
|
|
434
|
+
f"({type(exc).__name__})"
|
|
424
435
|
) from exc
|
|
425
436
|
finally:
|
|
426
437
|
clear_admission_epoch(request.profile_id, request.idempotency_key)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""Checks that ask whether a mechanism is *effective*, not merely present.
|
|
6
|
+
|
|
7
|
+
Three questions get confused in a system this size:
|
|
8
|
+
|
|
9
|
+
* **Implemented** — the code exists. A grep answers this.
|
|
10
|
+
* **Reachable** — something calls it. A call-graph trace answers this.
|
|
11
|
+
* **Effective** — it has actually changed an outcome against real data.
|
|
12
|
+
**Neither of the above answers this.** Only querying the store does.
|
|
13
|
+
|
|
14
|
+
A mechanism can pass the first two questions for months and fail the third
|
|
15
|
+
silently: a learner whose reward channel emits a constant still records plays,
|
|
16
|
+
and a conditional path guarded by a missing column still appears in coverage.
|
|
17
|
+
Nothing raises, nothing logs, and every file is present.
|
|
18
|
+
|
|
19
|
+
The two checks here answer the third question directly.
|
|
20
|
+
|
|
21
|
+
* :mod:`.prior_distance` — has a Bayesian learner's posterior actually moved
|
|
22
|
+
away from its prior?
|
|
23
|
+
* :mod:`.join_liveness` — has a schema-guarded code path ever executed against
|
|
24
|
+
this store, and if not, which requirement is missing?
|
|
25
|
+
|
|
26
|
+
Both are read-only, both are fail-soft, and neither is on a hot path.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from superlocalmemory.reliability.join_liveness import (
|
|
30
|
+
GuardVerdict,
|
|
31
|
+
check_schema_guards,
|
|
32
|
+
)
|
|
33
|
+
from superlocalmemory.reliability.prior_distance import (
|
|
34
|
+
DEFAULT_MIN_OBSERVATIONS,
|
|
35
|
+
LearnerVerdict,
|
|
36
|
+
check_beta_learners,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"DEFAULT_MIN_OBSERVATIONS",
|
|
41
|
+
"GuardVerdict",
|
|
42
|
+
"LearnerVerdict",
|
|
43
|
+
"check_beta_learners",
|
|
44
|
+
"check_schema_guards",
|
|
45
|
+
]
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""Can a schema-guarded code path run against this store, and has it had the data?
|
|
6
|
+
|
|
7
|
+
A note on what this proves, because the honest scope is narrower than the
|
|
8
|
+
convenient phrasing. The check reads the schema: it establishes whether the
|
|
9
|
+
guard's requirements are present *now*, and therefore whether the path is
|
|
10
|
+
reachable at all. It does not read an execution history, so a satisfied guard
|
|
11
|
+
means "this would execute", not "this has executed". The inference to "never
|
|
12
|
+
executed" is sound only in the other direction -- if a required column is absent
|
|
13
|
+
from a store that has been in service, the guarded path cannot have run against
|
|
14
|
+
it -- and that is the direction the findings rely on.
|
|
15
|
+
|
|
16
|
+
Some features are wired behind a guard that asks the schema a question before
|
|
17
|
+
doing any work — "is this table here, does that column exist" — and fall back
|
|
18
|
+
silently when the answer is no. The fallback is correct behaviour: it is what
|
|
19
|
+
keeps an old store openable. But it means a feature can be implemented, called
|
|
20
|
+
on the hot path, and covered by tests, while never once executing against real
|
|
21
|
+
data. Static analysis passes. A call-graph trace passes. Coverage passes. The
|
|
22
|
+
guard returns ``False`` and the feature is arithmetically absent.
|
|
23
|
+
|
|
24
|
+
Two things make that failure hard to see from inside the code:
|
|
25
|
+
|
|
26
|
+
1. **The guard is doing its job.** There is no error to raise. Falling back is
|
|
27
|
+
the designed response to a missing column.
|
|
28
|
+
2. **The fallback is often a neutral value**, which composes into an identity.
|
|
29
|
+
A decay-rate multiplier that falls back to a trust of 1.0 collapses
|
|
30
|
+
``lambda * (1 + kappa * (1 - trust))`` to ``lambda`` — the feature is on, and
|
|
31
|
+
it computes exactly what having no feature would compute.
|
|
32
|
+
|
|
33
|
+
So this check does two things a plain schema assertion does not. It records
|
|
34
|
+
whether each named guard **passes right now**, and when a guard fails it looks
|
|
35
|
+
for the required data **elsewhere in the store** — because the common case is
|
|
36
|
+
not that the data is missing, it is that the guard is asking the wrong table.
|
|
37
|
+
|
|
38
|
+
A guard reported as ``SATISFIED_ELSEWHERE`` is the most actionable outcome
|
|
39
|
+
available: the feature is one re-keyed join away from working, and no backfill
|
|
40
|
+
or migration is required.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
import logging
|
|
46
|
+
import sqlite3
|
|
47
|
+
from dataclasses import dataclass, field
|
|
48
|
+
from typing import Any
|
|
49
|
+
|
|
50
|
+
logger = logging.getLogger("superlocalmemory.reliability.join_liveness")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class Requirement:
|
|
55
|
+
"""One schema object a guard needs: a table, optionally a column on it."""
|
|
56
|
+
|
|
57
|
+
table: str
|
|
58
|
+
column: str | None = None
|
|
59
|
+
|
|
60
|
+
def __str__(self) -> str:
|
|
61
|
+
return f"{self.table}.{self.column}" if self.column else self.table
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class Guard:
|
|
66
|
+
"""A named conditional path and the schema it requires to execute."""
|
|
67
|
+
|
|
68
|
+
name: str
|
|
69
|
+
describes: str
|
|
70
|
+
requires: tuple[Requirement, ...]
|
|
71
|
+
#: What the feature computes when the guard fails, in words. Recording this
|
|
72
|
+
#: is the difference between "a feature is off" and "a feature is off and
|
|
73
|
+
#: indistinguishable from not having it".
|
|
74
|
+
fallback_behaviour: str
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class GuardVerdict:
|
|
79
|
+
name: str
|
|
80
|
+
describes: str
|
|
81
|
+
#: LIVE = requirements present, so the path is reachable now. DEAD / #: SATISFIED_ELSEWHERE = a requirement is absent, so the path cannot have run
|
|
82
|
+
#: against this store. LIVE is NOT evidence of past execution.
|
|
83
|
+
verdict: str
|
|
84
|
+
missing: tuple[str, ...] = field(default=())
|
|
85
|
+
found_elsewhere: tuple[tuple[str, str, int, float], ...] = field(default=())
|
|
86
|
+
detail: str = ""
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def is_live(self) -> bool:
|
|
90
|
+
return self.verdict == "LIVE"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
#: Guards worth reporting on. Each entry names a real conditional in the code,
|
|
94
|
+
#: so that a reader can go from this list to the line that asks the question.
|
|
95
|
+
GUARDS: tuple[Guard, ...] = (
|
|
96
|
+
Guard(
|
|
97
|
+
name="trust_weighted_forgetting",
|
|
98
|
+
describes=(
|
|
99
|
+
"learning/forgetting_scheduler.py::_has_trust_tables — gates the "
|
|
100
|
+
"per-fact trust lookup that modulates the decay rate"
|
|
101
|
+
),
|
|
102
|
+
requires=(
|
|
103
|
+
Requirement("trust_scores"),
|
|
104
|
+
Requirement("atomic_facts", "created_by"),
|
|
105
|
+
),
|
|
106
|
+
fallback_behaviour=(
|
|
107
|
+
"every fact takes trust = 1.0, so lambda_eff collapses to "
|
|
108
|
+
"lambda_base and the decay rate is identical to no trust weighting"
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _table_exists(conn: sqlite3.Connection, table: str) -> bool:
|
|
115
|
+
return (
|
|
116
|
+
conn.execute(
|
|
117
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=? LIMIT 1",
|
|
118
|
+
(table,),
|
|
119
|
+
).fetchone()
|
|
120
|
+
is not None
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _columns(conn: sqlite3.Connection, table: str) -> set[str]:
|
|
125
|
+
try:
|
|
126
|
+
return {r[1] for r in conn.execute(f'PRAGMA table_info("{table}")')}
|
|
127
|
+
except sqlite3.Error:
|
|
128
|
+
return set()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _satisfied(conn: sqlite3.Connection, requirement: Requirement) -> bool:
|
|
132
|
+
if not _table_exists(conn, requirement.table):
|
|
133
|
+
return False
|
|
134
|
+
if requirement.column is None:
|
|
135
|
+
return True
|
|
136
|
+
return requirement.column in _columns(conn, requirement.table)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _look_elsewhere(
|
|
140
|
+
conn: sqlite3.Connection,
|
|
141
|
+
column: str,
|
|
142
|
+
*,
|
|
143
|
+
join_target: str | None = None,
|
|
144
|
+
join_key: str = "fact_id",
|
|
145
|
+
) -> list[tuple[str, str, int]]:
|
|
146
|
+
"""Find other tables carrying ``column``, with a populated-row count.
|
|
147
|
+
|
|
148
|
+
This is the part that turns a failed guard into a fix. A column the guard
|
|
149
|
+
could not find on its own table is often present on a neighbouring one,
|
|
150
|
+
already populated.
|
|
151
|
+
|
|
152
|
+
**A populated count is not coverage, and reporting it alone overstates the
|
|
153
|
+
remedy.** A provenance-style table can carry a value on every one of its own
|
|
154
|
+
rows while describing only part of the set the join needs: rows can be
|
|
155
|
+
missing for older entities entirely. When ``join_target`` is given, the
|
|
156
|
+
coverage fraction over that table is measured and returned, because the
|
|
157
|
+
honest question is not "does this column exist somewhere" but "how much of
|
|
158
|
+
what the guard needs would the re-keyed join actually resolve".
|
|
159
|
+
"""
|
|
160
|
+
out: list[tuple[str, str, int]] = []
|
|
161
|
+
tables = [
|
|
162
|
+
r[0]
|
|
163
|
+
for r in conn.execute(
|
|
164
|
+
"SELECT name FROM sqlite_master WHERE type='table' "
|
|
165
|
+
"AND name NOT LIKE 'sqlite_%'",
|
|
166
|
+
)
|
|
167
|
+
]
|
|
168
|
+
for table in tables:
|
|
169
|
+
if column not in _columns(conn, table):
|
|
170
|
+
continue
|
|
171
|
+
try:
|
|
172
|
+
populated = conn.execute(
|
|
173
|
+
f'SELECT COUNT(*) FROM "{table}" '
|
|
174
|
+
f'WHERE "{column}" IS NOT NULL AND TRIM("{column}") <> \'\'',
|
|
175
|
+
).fetchone()[0]
|
|
176
|
+
except sqlite3.Error:
|
|
177
|
+
populated = 0
|
|
178
|
+
covered = -1
|
|
179
|
+
if join_target and join_target != table:
|
|
180
|
+
try:
|
|
181
|
+
total = conn.execute(
|
|
182
|
+
f'SELECT COUNT(*) FROM "{join_target}"',
|
|
183
|
+
).fetchone()[0]
|
|
184
|
+
if total:
|
|
185
|
+
hit = conn.execute(
|
|
186
|
+
f'SELECT COUNT(*) FROM "{join_target}" t WHERE EXISTS ('
|
|
187
|
+
f' SELECT 1 FROM "{table}" s WHERE s."{join_key}" = t."{join_key}"'
|
|
188
|
+
f' AND s."{column}" IS NOT NULL AND TRIM(s."{column}") <> \'\')',
|
|
189
|
+
).fetchone()[0]
|
|
190
|
+
covered = round(100.0 * hit / total, 1)
|
|
191
|
+
except sqlite3.Error:
|
|
192
|
+
covered = -1
|
|
193
|
+
out.append((table, column, int(populated), covered))
|
|
194
|
+
return out
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _evaluate(conn: sqlite3.Connection, guard: Guard) -> GuardVerdict:
|
|
198
|
+
missing = [str(r) for r in guard.requires if not _satisfied(conn, r)]
|
|
199
|
+
if not missing:
|
|
200
|
+
return GuardVerdict(
|
|
201
|
+
name=guard.name,
|
|
202
|
+
describes=guard.describes,
|
|
203
|
+
verdict="LIVE",
|
|
204
|
+
detail=(
|
|
205
|
+
"Every requirement is present, so the guarded path is reachable "
|
|
206
|
+
"on this store. This is schema evidence, not execution history: "
|
|
207
|
+
"it does not establish that the path has ever actually run."
|
|
208
|
+
),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
elsewhere: list[tuple[str, str, int, float]] = []
|
|
212
|
+
for requirement in guard.requires:
|
|
213
|
+
if requirement.column and not _satisfied(conn, requirement):
|
|
214
|
+
for found in _look_elsewhere(
|
|
215
|
+
conn, requirement.column, join_target=requirement.table,
|
|
216
|
+
):
|
|
217
|
+
if found[0] != requirement.table:
|
|
218
|
+
elsewhere.append(found)
|
|
219
|
+
|
|
220
|
+
populated = [e for e in elsewhere if e[2] > 0]
|
|
221
|
+
if populated:
|
|
222
|
+
# Rank by coverage of the guarded table, falling back to row count only
|
|
223
|
+
# when coverage could not be measured. Picking the largest table instead
|
|
224
|
+
# would recommend a 4,000-row partial source over a smaller complete one.
|
|
225
|
+
best = max(populated, key=lambda e: (e[3], e[2]))
|
|
226
|
+
cov = best[3]
|
|
227
|
+
if cov < 0:
|
|
228
|
+
remedy = (
|
|
229
|
+
f"Re-keying the join onto that table would make the path "
|
|
230
|
+
f"executable; coverage over the guarded table could not be "
|
|
231
|
+
f"measured here, so confirm it before relying on the remedy."
|
|
232
|
+
)
|
|
233
|
+
elif cov >= 99.5:
|
|
234
|
+
remedy = (
|
|
235
|
+
f"That table covers {cov}% of the rows the guard needs, so "
|
|
236
|
+
f"re-keying the join makes the path live without a backfill."
|
|
237
|
+
)
|
|
238
|
+
else:
|
|
239
|
+
remedy = (
|
|
240
|
+
f"That table covers only {cov}% of the rows the guard needs. "
|
|
241
|
+
f"Re-keying the join makes the path executable for those rows "
|
|
242
|
+
f"and leaves the remainder on the same fallback, so this is a "
|
|
243
|
+
f"partial remedy and a backfill decision, not a free fix."
|
|
244
|
+
)
|
|
245
|
+
return GuardVerdict(
|
|
246
|
+
name=guard.name,
|
|
247
|
+
describes=guard.describes,
|
|
248
|
+
verdict="SATISFIED_ELSEWHERE",
|
|
249
|
+
missing=tuple(missing),
|
|
250
|
+
found_elsewhere=tuple(elsewhere),
|
|
251
|
+
detail=(
|
|
252
|
+
f"The guard requires {', '.join(missing)}, which is absent, so "
|
|
253
|
+
f"the path has never executed against this store — "
|
|
254
|
+
f"{guard.fallback_behaviour}. The data it needs is present on "
|
|
255
|
+
f"{best[0]}.{best[1]} with {best[2]} populated rows. {remedy}"
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
return GuardVerdict(
|
|
260
|
+
name=guard.name,
|
|
261
|
+
describes=guard.describes,
|
|
262
|
+
verdict="DEAD",
|
|
263
|
+
missing=tuple(missing),
|
|
264
|
+
detail=(
|
|
265
|
+
f"The guard requires {', '.join(missing)}, which is absent from "
|
|
266
|
+
f"this store and not carried by any other table, so the path has "
|
|
267
|
+
f"never executed — {guard.fallback_behaviour}."
|
|
268
|
+
),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def check_schema_guards(
|
|
273
|
+
memory_db: Any, *, guards: tuple[Guard, ...] = GUARDS,
|
|
274
|
+
) -> list[GuardVerdict]:
|
|
275
|
+
"""Report, per registered guard, whether its path can run on this store.
|
|
276
|
+
|
|
277
|
+
``memory_db`` may be a path or an open connection. Read-only, and fail-soft:
|
|
278
|
+
an error yields an empty list, because a diagnostic must never be the reason
|
|
279
|
+
something breaks.
|
|
280
|
+
"""
|
|
281
|
+
owns_connection = not isinstance(memory_db, sqlite3.Connection)
|
|
282
|
+
conn: sqlite3.Connection | None = None
|
|
283
|
+
try:
|
|
284
|
+
conn = (
|
|
285
|
+
sqlite3.connect(f"file:{memory_db}?mode=ro", uri=True)
|
|
286
|
+
if owns_connection
|
|
287
|
+
else memory_db
|
|
288
|
+
)
|
|
289
|
+
return [_evaluate(conn, guard) for guard in guards]
|
|
290
|
+
except Exception:
|
|
291
|
+
logger.debug("join-liveness check skipped", exc_info=True)
|
|
292
|
+
return []
|
|
293
|
+
finally:
|
|
294
|
+
if owns_connection and conn is not None:
|
|
295
|
+
try:
|
|
296
|
+
conn.close()
|
|
297
|
+
except Exception:
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
__all__ = ["GUARDS", "Guard", "GuardVerdict", "Requirement", "check_schema_guards"]
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""Has a Bayesian learner's posterior actually moved off its prior?
|
|
6
|
+
|
|
7
|
+
A Thompson-sampling selector over Beta posteriors records a play, applies a
|
|
8
|
+
reward, and reports both. None of that tells you whether it learned anything.
|
|
9
|
+
Its own counters cannot: a reward channel that emits one constant value
|
|
10
|
+
increments the posterior on every play, so the play count rises, the timestamps
|
|
11
|
+
advance, and the dashboard looks alive while the distribution never moves.
|
|
12
|
+
|
|
13
|
+
This is not a hypothetical. A Beta posterior updated as
|
|
14
|
+
``alpha += r; beta += (1 - r)`` is stationary in mean for exactly one reward
|
|
15
|
+
value: ``r = 0.5``. And that value is the usual neutral fallback when a reward
|
|
16
|
+
cannot be attributed to a play. So the failure that produces *no learning at
|
|
17
|
+
all* is also the failure that produces *the most normal-looking counters*.
|
|
18
|
+
|
|
19
|
+
The signature is exact, not statistical. With a ``Beta(a0, b0)`` prior and *n*
|
|
20
|
+
observations all equal to 0.5::
|
|
21
|
+
|
|
22
|
+
alpha - a0 == beta - b0 == n * 0.5
|
|
23
|
+
|
|
24
|
+
0.5 is exactly representable in binary floating point, so ``n * 0.5`` is exact
|
|
25
|
+
for any plausible *n*. A learner matching that identity on every unit has
|
|
26
|
+
provably received the neutral value every single time — there is no sampling
|
|
27
|
+
noise to argue about, and one run is enough to establish it.
|
|
28
|
+
|
|
29
|
+
What this check does NOT claim: that a moving posterior is a *good* one. Motion
|
|
30
|
+
off the prior is necessary for learning, not sufficient. This distinguishes
|
|
31
|
+
"receiving signal" from "receiving nothing", which is the distinction the
|
|
32
|
+
learner's own metrics cannot make.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import logging
|
|
38
|
+
import sqlite3
|
|
39
|
+
from dataclasses import dataclass, field
|
|
40
|
+
from typing import Any
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger("superlocalmemory.reliability.prior_distance")
|
|
43
|
+
|
|
44
|
+
#: Below this many observations in total, a posterior sitting at its prior is
|
|
45
|
+
#: expected rather than suspicious, so no verdict is issued.
|
|
46
|
+
DEFAULT_MIN_OBSERVATIONS = 20
|
|
47
|
+
|
|
48
|
+
#: And below this many observations *per unit*. An aggregate floor alone lets a
|
|
49
|
+
#: store with many units and few observations report STALLED on units that were
|
|
50
|
+
#: never played at all.
|
|
51
|
+
_MIN_OBSERVATIONS_PER_UNIT = 2.0
|
|
52
|
+
|
|
53
|
+
#: Distance from the prior mean below which a unit counts as unmoved. Kept well
|
|
54
|
+
#: above float noise so that a genuinely tiny update is not reported as none.
|
|
55
|
+
_MEAN_EPSILON = 1e-9
|
|
56
|
+
|
|
57
|
+
#: Beta learners in this store, as (table, unit column, observation column).
|
|
58
|
+
#: Each is a Beta(1, 1) posterior over a named unit.
|
|
59
|
+
_BETA_LEARNERS: tuple[tuple[str, str, str | None], ...] = (
|
|
60
|
+
("bandit_arms", "arm_id", "plays"),
|
|
61
|
+
("source_quality", "source_id", None),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
_PRIOR_ALPHA = 1.0
|
|
65
|
+
_PRIOR_BETA = 1.0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class LearnerVerdict:
|
|
70
|
+
"""One Beta learner, and whether its posterior has moved."""
|
|
71
|
+
|
|
72
|
+
table: str
|
|
73
|
+
units: int
|
|
74
|
+
units_at_prior_mean: int
|
|
75
|
+
units_matching_neutral_identity: int
|
|
76
|
+
observations: int
|
|
77
|
+
verdict: str
|
|
78
|
+
detail: str
|
|
79
|
+
sample: tuple[tuple[str, float, float], ...] = field(default=())
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def is_stalled(self) -> bool:
|
|
83
|
+
return self.verdict == "STALLED"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _table_exists(conn: sqlite3.Connection, table: str) -> bool:
|
|
87
|
+
row = conn.execute(
|
|
88
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=? LIMIT 1",
|
|
89
|
+
(table,),
|
|
90
|
+
).fetchone()
|
|
91
|
+
return row is not None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _columns(conn: sqlite3.Connection, table: str) -> set[str]:
|
|
95
|
+
return {r[1] for r in conn.execute(f'PRAGMA table_info("{table}")')}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _inspect_learner(
|
|
99
|
+
conn: sqlite3.Connection,
|
|
100
|
+
table: str,
|
|
101
|
+
unit_column: str,
|
|
102
|
+
observation_column: str | None,
|
|
103
|
+
*,
|
|
104
|
+
min_observations: int,
|
|
105
|
+
) -> LearnerVerdict | None:
|
|
106
|
+
"""Inspect one Beta learner. Returns None when the table is absent."""
|
|
107
|
+
if not _table_exists(conn, table):
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
available = _columns(conn, table)
|
|
111
|
+
if not {"alpha", "beta"}.issubset(available):
|
|
112
|
+
return None
|
|
113
|
+
unit = unit_column if unit_column in available else "rowid"
|
|
114
|
+
obs_col = observation_column if (observation_column or "") in available else None
|
|
115
|
+
|
|
116
|
+
select = f'SELECT "{unit}", alpha, beta'
|
|
117
|
+
select += f', "{obs_col}"' if obs_col else ", NULL"
|
|
118
|
+
rows = conn.execute(f'{select} FROM "{table}"').fetchall()
|
|
119
|
+
if not rows:
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
units = len(rows)
|
|
123
|
+
at_prior_mean = 0
|
|
124
|
+
neutral_identity = 0
|
|
125
|
+
observations = 0
|
|
126
|
+
for _unit, alpha, beta, obs in rows:
|
|
127
|
+
alpha = float(alpha or 0.0)
|
|
128
|
+
beta = float(beta or 0.0)
|
|
129
|
+
total = alpha + beta
|
|
130
|
+
if total > 0 and abs(alpha / total - 0.5) < _MEAN_EPSILON:
|
|
131
|
+
at_prior_mean += 1
|
|
132
|
+
n = int(obs) if obs is not None else None
|
|
133
|
+
if n is None:
|
|
134
|
+
# No per-unit observation count. Infer it from the identity itself:
|
|
135
|
+
# n = (alpha - a0) / 0.5 only holds if every reward was neutral, so
|
|
136
|
+
# this is checked, never assumed.
|
|
137
|
+
candidate = (alpha - _PRIOR_ALPHA) * 2.0
|
|
138
|
+
n = int(round(candidate)) if candidate >= 0 else 0
|
|
139
|
+
observations += n
|
|
140
|
+
if n > 0 and (
|
|
141
|
+
abs((alpha - _PRIOR_ALPHA) - n * 0.5) < _MEAN_EPSILON
|
|
142
|
+
and abs((beta - _PRIOR_BETA) - n * 0.5) < _MEAN_EPSILON
|
|
143
|
+
):
|
|
144
|
+
neutral_identity += 1
|
|
145
|
+
|
|
146
|
+
sample = tuple(
|
|
147
|
+
(str(r[0]), float(r[1] or 0.0), float(r[2] or 0.0)) for r in rows[:3]
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# The floor has to bind per unit, not in aggregate. 165 arms sharing 20
|
|
151
|
+
# observations leaves most of them untouched at exactly the prior, which
|
|
152
|
+
# satisfies the unmoved test for a reason that carries no information. A
|
|
153
|
+
# verdict of STALLED must mean "measured inert", never "too sparse to tell".
|
|
154
|
+
per_unit = observations / units if units else 0.0
|
|
155
|
+
if observations < min_observations or per_unit < _MIN_OBSERVATIONS_PER_UNIT:
|
|
156
|
+
verdict = "INSUFFICIENT_DATA"
|
|
157
|
+
detail = (
|
|
158
|
+
f"{observations} observations across {units} units "
|
|
159
|
+
f"({per_unit:.2f} per unit) is too sparse for an unmoved posterior to "
|
|
160
|
+
f"mean anything; the floors are {min_observations} in total and "
|
|
161
|
+
f"{_MIN_OBSERVATIONS_PER_UNIT:g} per unit."
|
|
162
|
+
)
|
|
163
|
+
elif neutral_identity == units:
|
|
164
|
+
verdict = "STALLED"
|
|
165
|
+
detail = (
|
|
166
|
+
f"All {units} units satisfy (alpha-{_PRIOR_ALPHA:g}) == "
|
|
167
|
+
f"(beta-{_PRIOR_BETA:g}) == n/2 exactly across {observations} "
|
|
168
|
+
f"observations, so the rewards sum to exactly n/2 on every unit and "
|
|
169
|
+
f"each posterior mean is still {0.5}. No unit has acquired any "
|
|
170
|
+
f"preference. Note the identity constrains the SUM: it is also "
|
|
171
|
+
f"satisfied by a symmetric non-neutral stream, so confirm against a "
|
|
172
|
+
f"per-observation record before concluding the reward was constant."
|
|
173
|
+
)
|
|
174
|
+
elif at_prior_mean == units:
|
|
175
|
+
verdict = "STALLED"
|
|
176
|
+
detail = (
|
|
177
|
+
f"All {units} units sit at posterior mean 0.5 after {observations} "
|
|
178
|
+
f"observations, without matching the exact neutral identity. The "
|
|
179
|
+
f"updates are symmetric but not uniformly 0.5 — inspect the reward "
|
|
180
|
+
f"source."
|
|
181
|
+
)
|
|
182
|
+
else:
|
|
183
|
+
moved = units - at_prior_mean
|
|
184
|
+
verdict = "MOVING"
|
|
185
|
+
detail = (
|
|
186
|
+
f"{moved} of {units} units have moved off the prior mean across "
|
|
187
|
+
f"{observations} observations."
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return LearnerVerdict(
|
|
191
|
+
table=table,
|
|
192
|
+
units=units,
|
|
193
|
+
units_at_prior_mean=at_prior_mean,
|
|
194
|
+
units_matching_neutral_identity=neutral_identity,
|
|
195
|
+
observations=observations,
|
|
196
|
+
verdict=verdict,
|
|
197
|
+
detail=detail,
|
|
198
|
+
sample=sample,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def check_beta_learners(
|
|
203
|
+
learning_db: Any,
|
|
204
|
+
*,
|
|
205
|
+
min_observations: int = DEFAULT_MIN_OBSERVATIONS,
|
|
206
|
+
) -> list[LearnerVerdict]:
|
|
207
|
+
"""Report, per Beta learner in ``learning_db``, whether it has learned.
|
|
208
|
+
|
|
209
|
+
``learning_db`` may be a path or an open :class:`sqlite3.Connection`. The
|
|
210
|
+
database is only read. Fail-soft: any error yields an empty list and a log
|
|
211
|
+
line, because a diagnostic must never be the reason something breaks.
|
|
212
|
+
"""
|
|
213
|
+
owns_connection = not isinstance(learning_db, sqlite3.Connection)
|
|
214
|
+
conn: sqlite3.Connection | None = None
|
|
215
|
+
try:
|
|
216
|
+
if owns_connection:
|
|
217
|
+
conn = sqlite3.connect(f"file:{learning_db}?mode=ro", uri=True)
|
|
218
|
+
else:
|
|
219
|
+
conn = learning_db
|
|
220
|
+
out: list[LearnerVerdict] = []
|
|
221
|
+
for table, unit_column, observation_column in _BETA_LEARNERS:
|
|
222
|
+
verdict = _inspect_learner(
|
|
223
|
+
conn,
|
|
224
|
+
table,
|
|
225
|
+
unit_column,
|
|
226
|
+
observation_column,
|
|
227
|
+
min_observations=min_observations,
|
|
228
|
+
)
|
|
229
|
+
if verdict is not None:
|
|
230
|
+
out.append(verdict)
|
|
231
|
+
return out
|
|
232
|
+
except Exception:
|
|
233
|
+
logger.debug("prior-distance check skipped", exc_info=True)
|
|
234
|
+
return []
|
|
235
|
+
finally:
|
|
236
|
+
if owns_connection and conn is not None:
|
|
237
|
+
try:
|
|
238
|
+
conn.close()
|
|
239
|
+
except Exception:
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
__all__ = ["DEFAULT_MIN_OBSERVATIONS", "LearnerVerdict", "check_beta_learners"]
|
|
@@ -737,7 +737,7 @@ a { color: #00D4AA; }
|
|
|
737
737
|
<p class="sub">Back up your memories to a private GitHub repository.</p>
|
|
738
738
|
|
|
739
739
|
<label>Personal Access Token</label>
|
|
740
|
-
<input type="password" id="pat" placeholder="
|
|
740
|
+
<input type="password" id="pat" placeholder="GitHub personal access token">
|
|
741
741
|
|
|
742
742
|
<label>Repository Name</label>
|
|
743
743
|
<input type="text" id="repo" value="slm-backup" placeholder="slm-backup">
|