cortexm 0.3.0__py3-none-any.whl
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.
- context_m.py +17 -0
- cortexm/__init__.py +45 -0
- cortexm/accel.py +403 -0
- cortexm/api/__init__.py +0 -0
- cortexm/api/chaos.py +118 -0
- cortexm/api/memory.py +635 -0
- cortexm/bench/__init__.py +0 -0
- cortexm/bench/abilities.py +311 -0
- cortexm/bench/baselines.py +89 -0
- cortexm/bench/beam_loader.py +317 -0
- cortexm/bench/generator.py +376 -0
- cortexm/bench/harness.py +211 -0
- cortexm/bench/messy.py +218 -0
- cortexm/bench/micro.py +251 -0
- cortexm/bench/ood.py +443 -0
- cortexm/bench/run.py +137 -0
- cortexm/bridge/__init__.py +0 -0
- cortexm/bridge/dates.py +178 -0
- cortexm/bridge/decoders.py +204 -0
- cortexm/bridge/enrich.py +255 -0
- cortexm/bridge/extractor.py +316 -0
- cortexm/bridge/fallback.py +332 -0
- cortexm/bridge/onnx_runtime.py +158 -0
- cortexm/bridge/patterns.py +760 -0
- cortexm/bridge/ppr.py +104 -0
- cortexm/bridge/prefilter.py +188 -0
- cortexm/bridge/query_extract.py +420 -0
- cortexm/bridge/reader.py +1174 -0
- cortexm/bridge/rerank.py +204 -0
- cortexm/bridge/writer.py +492 -0
- cortexm/cli.py +295 -0
- cortexm/cognition/__init__.py +53 -0
- cortexm/cognition/abstraction.py +192 -0
- cortexm/cognition/analogy.py +159 -0
- cortexm/cognition/engine.py +204 -0
- cortexm/cognition/gaps.py +365 -0
- cortexm/cognition/scanner.py +204 -0
- cortexm/config.py +375 -0
- cortexm/cortexm.py +8 -0
- cortexm/enterprise/__init__.py +0 -0
- cortexm/enterprise/audit.py +178 -0
- cortexm/enterprise/governance.py +239 -0
- cortexm/errors.py +35 -0
- cortexm/features/__init__.py +0 -0
- cortexm/features/git.py +204 -0
- cortexm/features/prefetch.py +88 -0
- cortexm/features/zk.py +105 -0
- cortexm/federation/__init__.py +39 -0
- cortexm/federation/crdt.py +275 -0
- cortexm/federation/fabric.py +109 -0
- cortexm/federation/hlc.py +80 -0
- cortexm/federation/node.py +145 -0
- cortexm/federation/schema_report.py +73 -0
- cortexm/federation/transport.py +164 -0
- cortexm/index/__init__.py +19 -0
- cortexm/index/nsg.py +386 -0
- cortexm/mcp/__init__.py +0 -0
- cortexm/mcp/server.py +985 -0
- cortexm/metrics.py +62 -0
- cortexm/migrate/__init__.py +0 -0
- cortexm/migrate/importers.py +192 -0
- cortexm/provenance/__init__.py +78 -0
- cortexm/provenance/agent.py +214 -0
- cortexm/provenance/cose.py +201 -0
- cortexm/provenance/scitt.py +258 -0
- cortexm/provenance/vc.py +250 -0
- cortexm/security/__init__.py +0 -0
- cortexm/security/crypto.py +162 -0
- cortexm/security/hashes.py +140 -0
- cortexm/security/injection.py +149 -0
- cortexm/security/mind.py +154 -0
- cortexm/security/pii.py +265 -0
- cortexm/security/rbac.py +169 -0
- cortexm/security/sandbox.py +131 -0
- cortexm/security/zk_hamming.py +142 -0
- cortexm/security/zk_sql.py +485 -0
- cortexm/server/__init__.py +0 -0
- cortexm/server/metrics.py +88 -0
- cortexm/server/rest.py +936 -0
- cortexm/server/sparql.py +984 -0
- cortexm/text/__init__.py +0 -0
- cortexm/text/dissim.py +252 -0
- cortexm/text/embedder.py +155 -0
- cortexm/text/fuzzy.py +218 -0
- cortexm/text/idiolect.py +253 -0
- cortexm/text/labse.py +374 -0
- cortexm/text/tokenizer.py +79 -0
- cortexm/trace/__init__.py +0 -0
- cortexm/trace/blob_arena.py +277 -0
- cortexm/trace/consolidate.py +337 -0
- cortexm/trace/contradictions.py +69 -0
- cortexm/trace/dedup.py +114 -0
- cortexm/trace/edges.py +214 -0
- cortexm/trace/fact.py +121 -0
- cortexm/trace/fade.py +245 -0
- cortexm/trace/lifecycle.py +112 -0
- cortexm/trace/rebuild.py +173 -0
- cortexm/trace/rules.py +171 -0
- cortexm/trace/store.py +680 -0
- cortexm/trace/structural.py +183 -0
- cortexm/trace/tmt.py +335 -0
- cortexm/util.py +148 -0
- cortexm/vsa/__init__.py +0 -0
- cortexm/vsa/attribution.py +149 -0
- cortexm/vsa/cleanup.py +161 -0
- cortexm/vsa/codecs.py +397 -0
- cortexm/vsa/hologram_overlay.py +139 -0
- cortexm/vsa/index.py +163 -0
- cortexm/vsa/ops.py +149 -0
- cortexm/vsa/palace.py +446 -0
- cortexm/vsa/role_vectors.py +236 -0
- cortexm/vsa/slb.py +78 -0
- cortexm/vsa/tlsh_trie.py +137 -0
- cortexm/vsa/working_memory.py +249 -0
- cortexm-0.3.0.dist-info/METADATA +482 -0
- cortexm-0.3.0.dist-info/RECORD +120 -0
- cortexm-0.3.0.dist-info/WHEEL +5 -0
- cortexm-0.3.0.dist-info/entry_points.txt +2 -0
- cortexm-0.3.0.dist-info/licenses/LICENSE +190 -0
- cortexm-0.3.0.dist-info/top_level.txt +2 -0
cortexm/trace/fade.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""FadeMem-style forgetting — biologically-inspired decay + retention.
|
|
2
|
+
|
|
3
|
+
arXiv:2601.18642 (FadeMem, 2026) implements:
|
|
4
|
+
* Dual-layer hierarchy (STM / LTM)
|
|
5
|
+
* Differential decay rates modulated by:
|
|
6
|
+
- semantic relevance (matches current query intent)
|
|
7
|
+
- access frequency (reinforcement strengthens)
|
|
8
|
+
- temporal patterns (recent > old)
|
|
9
|
+
- contradiction pressure (superseded facts fade faster)
|
|
10
|
+
* LLM-guided conflict resolution (we keep μ=0 — uses similarity)
|
|
11
|
+
* Reported: 45% storage reduction with superior multi-hop reasoning
|
|
12
|
+
|
|
13
|
+
Context-M's existing `retention_score` in lifecycle.py already does
|
|
14
|
+
reinforcement x recency x contradiction. This module adds:
|
|
15
|
+
|
|
16
|
+
1. EXPONENTIAL DECAY — retention_score is multiplied by
|
|
17
|
+
exp(-lambda * age_days). Old untouched facts decay to zero.
|
|
18
|
+
|
|
19
|
+
2. ACCESS-DRIVEN RECONSOLIDATION — each retrieval event bumps the
|
|
20
|
+
fact's `last_accessed` AND its `reinforcement` by a small delta
|
|
21
|
+
(0.1 per access, capped at 3.0). This is "retrieval-induced
|
|
22
|
+
reconsolidation" — the act of remembering strengthens the memory,
|
|
23
|
+
exactly as in human recollection.
|
|
24
|
+
|
|
25
|
+
3. CLUSTER CONSOLIDATION — when multiple facts in the same
|
|
26
|
+
(subject, relation) group all have retention < 0.30, they're
|
|
27
|
+
candidates for FADE MERGE: keep the highest-confidence one,
|
|
28
|
+
mark the others inactive, wire MERGED_WITH edges (audit-safe).
|
|
29
|
+
|
|
30
|
+
4. SLEEP SWEEP — `fade_sweep(store, palace, cfg)` runs the full
|
|
31
|
+
decay + deactivate + merge pass. Designed to be called from
|
|
32
|
+
`cortexm consolidate` CLI as part of the Aeon-style dreaming
|
|
33
|
+
cycle. Idempotent — safe to call repeatedly.
|
|
34
|
+
|
|
35
|
+
The sweep is BI-TEMPORAL SAFE: deactivated facts keep their
|
|
36
|
+
`valid_from`/`valid_to`/`tx_from`/`tx_to` windows intact, so
|
|
37
|
+
`allow_inactive=True` retrieval still serves them for temporal queries.
|
|
38
|
+
The sweep only changes `is_active` and `provenance.fade_state`.
|
|
39
|
+
"""
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import datetime as _dt
|
|
43
|
+
import math
|
|
44
|
+
from datetime import datetime, timezone
|
|
45
|
+
from typing import Iterable
|
|
46
|
+
|
|
47
|
+
from cortexm.trace.edges import MERGED_WITH
|
|
48
|
+
from cortexm.trace.fact import Fact
|
|
49
|
+
from cortexm.trace.lifecycle import retention_score
|
|
50
|
+
from cortexm.util import iso, similarity
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _now() -> datetime:
|
|
54
|
+
return datetime.now(timezone.utc)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def fade_retention_score(fact: Fact, now: datetime,
|
|
58
|
+
*, lambda_: float = 0.05,
|
|
59
|
+
access_boost: float = 0.5,
|
|
60
|
+
contradiction_penalty: float = 0.25) -> float:
|
|
61
|
+
"""Exponential-decay retention score.
|
|
62
|
+
|
|
63
|
+
Builds on lifecycle.retention_score (which already does
|
|
64
|
+
reinforcement x recency x contradiction) and multiplies by an
|
|
65
|
+
exponential decay envelope:
|
|
66
|
+
|
|
67
|
+
fade_factor = exp(-lambda * age_days)
|
|
68
|
+
|
|
69
|
+
The result is a more aggressive forgetting curve than the
|
|
70
|
+
hyperbolic 1/(1+age/30) in lifecycle.retention_score — long-untouched
|
|
71
|
+
facts decay to ~0 instead of plateauing at ~0.3.
|
|
72
|
+
|
|
73
|
+
Lambda=0.05 means:
|
|
74
|
+
* 7 days idle: 0.97 fade_factor
|
|
75
|
+
* 30 days idle: 0.86
|
|
76
|
+
* 90 days idle: 0.61
|
|
77
|
+
* 365 days idle: 0.16 → below deactivate_threshold
|
|
78
|
+
"""
|
|
79
|
+
base = retention_score(fact, now)
|
|
80
|
+
# parse the fact's tx_from (transaction time) — that's when the fact
|
|
81
|
+
# entered the store, which is the right "age" anchor for forgetting
|
|
82
|
+
from cortexm.util import parse_ts
|
|
83
|
+
tx = parse_ts(fact.tx_from) or now
|
|
84
|
+
age_days = max(0.0, (now - tx).total_seconds() / 86400.0)
|
|
85
|
+
fade = math.exp(-lambda_ * age_days)
|
|
86
|
+
# access boost: each retrieval multiplies retention by (1 + boost)
|
|
87
|
+
# capped to avoid runaway reinforcement
|
|
88
|
+
access_mult = 1.0 + min(access_boost * fact.access_count, 2.5)
|
|
89
|
+
# contradiction pressure: each chain_updates in provenance pulls
|
|
90
|
+
# retention down (the fact has been disputed)
|
|
91
|
+
chain_updates = fact.provenance.get("chain_updates", 0) \
|
|
92
|
+
if isinstance(fact.provenance, dict) else 0
|
|
93
|
+
contradiction_mult = max(0.1, 1.0 - contradiction_penalty * chain_updates)
|
|
94
|
+
return base * fade * access_mult * contradiction_mult
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def fade_sweep(store, palace=None, *,
|
|
98
|
+
lambda_: float = 0.05,
|
|
99
|
+
access_boost: float = 0.5,
|
|
100
|
+
contradiction_penalty: float = 0.25,
|
|
101
|
+
deactivate_threshold: float = 0.10,
|
|
102
|
+
merge_threshold: float = 0.30,
|
|
103
|
+
merge_similarity: float = 0.92,
|
|
104
|
+
user_id: str | None = None,
|
|
105
|
+
dry_run: bool = False) -> dict:
|
|
106
|
+
"""Run a single FadeMem sweep over the store.
|
|
107
|
+
|
|
108
|
+
Steps:
|
|
109
|
+
1. Compute fade_retention_score for every active fact.
|
|
110
|
+
2. Deactivate facts with score < deactivate_threshold.
|
|
111
|
+
(Bi-temporal safe: only `is_active` flips, all temporal
|
|
112
|
+
fields preserved.)
|
|
113
|
+
3. For each (user_id, subject, relation) cluster where all
|
|
114
|
+
members have score < merge_threshold, MERGE the lowest-
|
|
115
|
+
confidence ones into the highest-confidence one via
|
|
116
|
+
MERGED_WITH edges.
|
|
117
|
+
|
|
118
|
+
Returns a stats dict. Idempotent — safe to call repeatedly.
|
|
119
|
+
"""
|
|
120
|
+
now = _now()
|
|
121
|
+
stats = {
|
|
122
|
+
"scanned": 0,
|
|
123
|
+
"deactivated": 0,
|
|
124
|
+
"merged_into": 0,
|
|
125
|
+
"merged_dropped": 0,
|
|
126
|
+
"min_score": 1.0,
|
|
127
|
+
"max_score": 0.0,
|
|
128
|
+
"mean_score": 0.0,
|
|
129
|
+
"dry_run": dry_run,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
where = "is_active=1 AND quarantined=0"
|
|
133
|
+
args: tuple = ()
|
|
134
|
+
if user_id is not None:
|
|
135
|
+
where += " AND user_id=?"
|
|
136
|
+
args = (user_id,)
|
|
137
|
+
rows = store.conn.execute(
|
|
138
|
+
f"SELECT id FROM facts WHERE {where}", args).fetchall()
|
|
139
|
+
fact_ids = [r[0] for r in rows]
|
|
140
|
+
|
|
141
|
+
facts = store.get_facts(fact_ids)
|
|
142
|
+
scores: dict[str, float] = {}
|
|
143
|
+
for f in facts:
|
|
144
|
+
s = fade_retention_score(f, now, lambda_=lambda_,
|
|
145
|
+
access_boost=access_boost,
|
|
146
|
+
contradiction_penalty=contradiction_penalty)
|
|
147
|
+
scores[f.id] = s
|
|
148
|
+
stats["scanned"] += 1
|
|
149
|
+
stats["min_score"] = min(stats["min_score"], s)
|
|
150
|
+
stats["max_score"] = max(stats["max_score"], s)
|
|
151
|
+
if scores:
|
|
152
|
+
stats["mean_score"] = sum(scores.values()) / len(scores)
|
|
153
|
+
|
|
154
|
+
# ---- 1. Deactivate low-retention facts ------------------------------
|
|
155
|
+
to_deactivate = [fid for fid, s in scores.items()
|
|
156
|
+
if s < deactivate_threshold]
|
|
157
|
+
if not dry_run and to_deactivate:
|
|
158
|
+
store.begin_batch()
|
|
159
|
+
commit = store.create_commit(
|
|
160
|
+
f"fade_sweep: deactivate {len(to_deactivate)} low-retention facts",
|
|
161
|
+
n_facts=0)
|
|
162
|
+
for fid in to_deactivate:
|
|
163
|
+
store.update_fact(
|
|
164
|
+
fid, is_active=0, tx_to=iso(now),
|
|
165
|
+
retired_commit=commit,
|
|
166
|
+
provenance={"fade_state": "deactivated",
|
|
167
|
+
"fade_score": round(scores[fid], 4),
|
|
168
|
+
"faded_at": iso(now)})
|
|
169
|
+
store.end_batch()
|
|
170
|
+
stats["deactivated"] = len(to_deactivate)
|
|
171
|
+
|
|
172
|
+
# ---- 2. Cluster merge (low-retention siblings) ---------------------
|
|
173
|
+
# Group remaining active facts by (user, subject, relation) and
|
|
174
|
+
# find clusters where ALL members have score < merge_threshold.
|
|
175
|
+
# These are stale repetitions — keep the highest-confidence one,
|
|
176
|
+
# merge the rest.
|
|
177
|
+
surviving = [f for f in facts
|
|
178
|
+
if scores[f.id] >= deactivate_threshold]
|
|
179
|
+
clusters: dict[tuple, list[Fact]] = {}
|
|
180
|
+
for f in surviving:
|
|
181
|
+
key = (f.user_id, f.subject, f.relation)
|
|
182
|
+
clusters.setdefault(key, []).append(f)
|
|
183
|
+
|
|
184
|
+
merge_count = 0
|
|
185
|
+
drop_count = 0
|
|
186
|
+
if not dry_run:
|
|
187
|
+
store.begin_batch()
|
|
188
|
+
commit = store.create_commit(
|
|
189
|
+
f"fade_sweep: cluster merge pass", n_facts=0)
|
|
190
|
+
for key, group in clusters.items():
|
|
191
|
+
if len(group) < 2:
|
|
192
|
+
continue
|
|
193
|
+
group_scores = [scores[f.id] for f in group]
|
|
194
|
+
# only merge if ALL members are below merge_threshold
|
|
195
|
+
if max(group_scores) >= merge_threshold:
|
|
196
|
+
continue
|
|
197
|
+
# pick the keeper: highest score, tie-break on confidence
|
|
198
|
+
group.sort(key=lambda f: (-scores[f.id], -f.confidence))
|
|
199
|
+
keeper = group[0]
|
|
200
|
+
for f in group[1:]:
|
|
201
|
+
if similarity(keeper.value, f.value) >= merge_similarity:
|
|
202
|
+
if not dry_run:
|
|
203
|
+
store.update_fact(
|
|
204
|
+
f.id, is_active=0, tx_to=iso(now),
|
|
205
|
+
retired_commit=commit,
|
|
206
|
+
provenance={"fade_state": "merged",
|
|
207
|
+
"fade_score": round(scores[f.id], 4),
|
|
208
|
+
"merged_into": keeper.id,
|
|
209
|
+
"faded_at": iso(now)})
|
|
210
|
+
store.add_edge(keeper.id, f.id, MERGED_WITH,
|
|
211
|
+
{"fade_score": round(scores[f.id], 4),
|
|
212
|
+
"faded_at": iso(now)})
|
|
213
|
+
drop_count += 1
|
|
214
|
+
merge_count += 1
|
|
215
|
+
if not dry_run:
|
|
216
|
+
store.end_batch()
|
|
217
|
+
stats["merged_into"] = merge_count
|
|
218
|
+
stats["merged_dropped"] = drop_count
|
|
219
|
+
|
|
220
|
+
return stats
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def fade_report(store, since: str | None = None) -> dict:
|
|
224
|
+
"""Summary of fade activity since `since` (ISO)."""
|
|
225
|
+
where = "is_active=0 AND provenance LIKE '%fade_state%'"
|
|
226
|
+
args: tuple = ()
|
|
227
|
+
if since:
|
|
228
|
+
where += " AND tx_to >= ?"
|
|
229
|
+
args = (since,)
|
|
230
|
+
rows = store.conn.execute(
|
|
231
|
+
f"SELECT provenance FROM facts WHERE {where}", args).fetchall()
|
|
232
|
+
deactivated = 0
|
|
233
|
+
merged = 0
|
|
234
|
+
for r in rows:
|
|
235
|
+
prov = r[0] or ""
|
|
236
|
+
if '"deactivated"' in prov:
|
|
237
|
+
deactivated += 1
|
|
238
|
+
if '"merged"' in prov:
|
|
239
|
+
merged += 1
|
|
240
|
+
return {"fade_deactivated": deactivated,
|
|
241
|
+
"fade_merged": merged,
|
|
242
|
+
"fade_total": deactivated + merged}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
__all__ = ["fade_retention_score", "fade_sweep", "fade_report"]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Interference-aware lifecycle — promotion, decay, consolidation.
|
|
2
|
+
|
|
3
|
+
From the August-2026 memory research (Dual-Layer Agentic Memory,
|
|
4
|
+
Controlled Memory Interference, LiveMem): facts are not just stored —
|
|
5
|
+
before commitment each candidate is evaluated for how it *interacts*
|
|
6
|
+
with existing memory; retention is shaped by reinforcement, recency
|
|
7
|
+
and interference rather than a pure Ebbinghaus curve.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import datetime as _dt
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
from cortexm.trace.contradictions import Action, Conflict, find_conflicts
|
|
16
|
+
from cortexm.trace.fact import Fact
|
|
17
|
+
from cortexm.trace.store import TraceStore
|
|
18
|
+
from cortexm.util import parse_ts, similarity
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class LifecycleDecision:
|
|
23
|
+
action: Action
|
|
24
|
+
commit_fact: bool = True
|
|
25
|
+
target_ids: list[str] | None = None
|
|
26
|
+
quarantine: bool = False
|
|
27
|
+
note: str = ""
|
|
28
|
+
interference: float = 0.0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def assess(store: TraceStore, candidate: Fact,
|
|
32
|
+
value_match_threshold: float = 0.92) -> LifecycleDecision:
|
|
33
|
+
"""Pre-commit interference evaluation (Controlled Memory Interference)."""
|
|
34
|
+
conflict = find_conflicts(store, candidate)
|
|
35
|
+
inter = 0.0
|
|
36
|
+
|
|
37
|
+
if conflict.action is Action.SUPERSEDE:
|
|
38
|
+
old = conflict.existing[0]
|
|
39
|
+
# semantic interference: how much the new fact disrupts the old one
|
|
40
|
+
inter = similarity(old.value, candidate.value)
|
|
41
|
+
note = conflict.note
|
|
42
|
+
if inter > 0.75 and candidate.confidence < 0.6:
|
|
43
|
+
note += "; low-confidence supersession flagged for audit"
|
|
44
|
+
return LifecycleDecision(
|
|
45
|
+
action=Action.SUPERSEDE, commit_fact=True,
|
|
46
|
+
target_ids=[f.id for f in conflict.existing],
|
|
47
|
+
note=note, interference=inter)
|
|
48
|
+
|
|
49
|
+
if conflict.action is Action.MERGE:
|
|
50
|
+
return LifecycleDecision(
|
|
51
|
+
Action.MERGE, commit_fact=False,
|
|
52
|
+
target_ids=[f.id for f in conflict.existing],
|
|
53
|
+
note=conflict.note, interference=1.0)
|
|
54
|
+
|
|
55
|
+
if conflict.action is Action.SKIP:
|
|
56
|
+
return LifecycleDecision(
|
|
57
|
+
Action.SKIP, commit_fact=False,
|
|
58
|
+
target_ids=[f.id for f in conflict.existing],
|
|
59
|
+
note=conflict.note)
|
|
60
|
+
|
|
61
|
+
if conflict.action is Action.COEXIST:
|
|
62
|
+
# interference over a bounded recent sample (linear at scale)
|
|
63
|
+
if candidate.relation == "mentioned":
|
|
64
|
+
inter = 0.0
|
|
65
|
+
else:
|
|
66
|
+
recent = conflict.existing[-8:]
|
|
67
|
+
inter = max((similarity(f.value, candidate.value) for f in recent),
|
|
68
|
+
default=0.0)
|
|
69
|
+
return LifecycleDecision(
|
|
70
|
+
Action.COEXIST, commit_fact=True, interference=inter,
|
|
71
|
+
note=conflict.note)
|
|
72
|
+
|
|
73
|
+
return LifecycleDecision(Action.COMMIT, commit_fact=True, note="new fact")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def retention_score(fact: Fact, now: _dt.datetime) -> float:
|
|
77
|
+
"""Interference-aware retention: conf x reinforcement x recency decay,
|
|
78
|
+
reduced by contradiction pressure (number of supersessions the
|
|
79
|
+
subject-relation chain went through)."""
|
|
80
|
+
age_days = max(
|
|
81
|
+
0.0, (now - (parse_ts(fact.tx_from) or now)).total_seconds() / 86400.0)
|
|
82
|
+
recency = 1.0 / (1.0 + age_days / 30.0) ** 0.5
|
|
83
|
+
reinforce = 1.0 + 0.5 * (fact.reinforcement - 1) + 0.1 * min(fact.access_count, 20)
|
|
84
|
+
pressure = 1.0 + 0.25 * fact.provenance.get("chain_updates", 0)
|
|
85
|
+
return fact.confidence * recency * reinforce / pressure
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def consolidate(store: TraceStore, now: _dt.datetime | None = None,
|
|
89
|
+
promote_reinforcement: int = 2,
|
|
90
|
+
demote_threshold: float = 0.15) -> dict:
|
|
91
|
+
"""Fast-write routing + slow consolidation (Dual-Layer Agentic Memory):
|
|
92
|
+
promote reinforced short-term facts, decay untouched ones."""
|
|
93
|
+
now = now or _dt.datetime.now(_dt.timezone.utc)
|
|
94
|
+
promoted, demoted, deactivated = 0, 0, 0
|
|
95
|
+
for f in store.query_facts(active=True, derived=None):
|
|
96
|
+
if f.is_derived:
|
|
97
|
+
continue
|
|
98
|
+
score = retention_score(f, now)
|
|
99
|
+
if f.memory_type == "short_term" and (
|
|
100
|
+
f.reinforcement >= promote_reinforcement or f.access_count >= 3) \
|
|
101
|
+
and score >= 0.25:
|
|
102
|
+
store.update_fact(f.id, memory_type="long_term")
|
|
103
|
+
promoted += 1
|
|
104
|
+
elif f.memory_type == "short_term" and score < demote_threshold:
|
|
105
|
+
store.update_fact(f.id, is_active=0,
|
|
106
|
+
provenance={**f.provenance, "deactivated": "decay"})
|
|
107
|
+
deactivated += 1
|
|
108
|
+
elif f.memory_type == "long_term" and score < demote_threshold * 0.5:
|
|
109
|
+
demoted += 1
|
|
110
|
+
store.update_fact(f.id, memory_type="short_term",
|
|
111
|
+
provenance={**f.provenance, "demoted": "decay"})
|
|
112
|
+
return {"promoted": promoted, "demoted": demoted, "deactivated": deactivated}
|
cortexm/trace/rebuild.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Trace-based rebuild — checksum-driven re-materialization from the
|
|
2
|
+
symbolic Trace.
|
|
3
|
+
|
|
4
|
+
User concern: "Self-healing is theater → Use checksums + rebuild from
|
|
5
|
+
Trace". The existing palace.heal() re-encodes corrupt records from the
|
|
6
|
+
symbolic Trace, which IS the canonical rebuild path — but it's
|
|
7
|
+
invoked manually and doesn't cover the full lifecycle.
|
|
8
|
+
|
|
9
|
+
This module formalizes the rebuild op:
|
|
10
|
+
1. Checksum audit: every vector record carries a stored vec_hash;
|
|
11
|
+
health_check() detects mismatches.
|
|
12
|
+
2. Rebuild: for every corrupt record, re-encode from the canonical
|
|
13
|
+
Fact in TraceStore. The Trace is the source of truth — vectors
|
|
14
|
+
are derived.
|
|
15
|
+
3. Full rebuild: drop all vectors, re-build from scratch from all
|
|
16
|
+
Facts in the Trace. Use after schema changes, codec swaps, or
|
|
17
|
+
suspected widespread corruption.
|
|
18
|
+
4. Audit log: every rebuild leaves a tamper-evident record in the
|
|
19
|
+
rebuild_log table.
|
|
20
|
+
|
|
21
|
+
Designed for the MemoryPalace + TraceStore pair.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import datetime as _dt
|
|
27
|
+
import json
|
|
28
|
+
from dataclasses import dataclass, asdict
|
|
29
|
+
|
|
30
|
+
from cortexm.util import iso
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
REBUILD_LOG_TABLE = """
|
|
34
|
+
CREATE TABLE IF NOT EXISTS rebuild_log (
|
|
35
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
36
|
+
ts TEXT NOT NULL,
|
|
37
|
+
kind TEXT NOT NULL, -- 'partial' | 'full'
|
|
38
|
+
scope TEXT NOT NULL, -- 'all' | 'user:N' | 'scope:N'
|
|
39
|
+
checked INTEGER NOT NULL,
|
|
40
|
+
rebuilt INTEGER NOT NULL,
|
|
41
|
+
note TEXT DEFAULT ''
|
|
42
|
+
)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class RebuildReport:
|
|
48
|
+
kind: str
|
|
49
|
+
scope: str
|
|
50
|
+
checked: int
|
|
51
|
+
rebuilt: int
|
|
52
|
+
corrupt_ids: list[str]
|
|
53
|
+
missing_ids: list[str]
|
|
54
|
+
duration_ms: int
|
|
55
|
+
ts: str
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> dict:
|
|
58
|
+
d = asdict(self)
|
|
59
|
+
return d
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TraceRebuilder:
|
|
63
|
+
"""Drive checksum-audited rebuilds of the MemoryPalace from Trace."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, palace, store) -> None:
|
|
66
|
+
self.palace = palace
|
|
67
|
+
self.store = store
|
|
68
|
+
store.conn.execute(REBUILD_LOG_TABLE)
|
|
69
|
+
store.conn.commit()
|
|
70
|
+
|
|
71
|
+
def audit(self, sample: int | None = None) -> dict:
|
|
72
|
+
"""Run a checksum audit — detect but don't fix."""
|
|
73
|
+
report = self.palace.health_check(sample=sample)
|
|
74
|
+
return {
|
|
75
|
+
"checked": report["checked"],
|
|
76
|
+
"corrupt": report["corrupt"],
|
|
77
|
+
"corrupt_ids": report["corrupt_ids"],
|
|
78
|
+
"tmr_disagree_bits": report.get("tmr_disagree_bits", 0),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
def rebuild_partial(self, max_records: int = 1000) -> RebuildReport:
|
|
82
|
+
"""Re-encode corrupt records from the Trace.
|
|
83
|
+
|
|
84
|
+
For every corrupt fact_id, look up the canonical Fact in the
|
|
85
|
+
TraceStore, re-encode it via palace.encode_fact, and overwrite
|
|
86
|
+
the corrupt vector record. Leaves a row in rebuild_log.
|
|
87
|
+
"""
|
|
88
|
+
t0 = _dt.datetime.now(_dt.timezone.utc)
|
|
89
|
+
audit = self.palace.health_check()
|
|
90
|
+
corrupt_ids = audit["corrupt_ids"][:max_records]
|
|
91
|
+
# fetch canonical facts
|
|
92
|
+
rebuilt = 0
|
|
93
|
+
missing_ids: list[str] = []
|
|
94
|
+
for fid in corrupt_ids:
|
|
95
|
+
fact = self._lookup_fact(fid)
|
|
96
|
+
if fact is None:
|
|
97
|
+
missing_ids.append(fid)
|
|
98
|
+
continue
|
|
99
|
+
vec = self.palace.encode_fact(fact)
|
|
100
|
+
self.palace.add(fid, vec) # add overwrites by fact_id
|
|
101
|
+
rebuilt += 1
|
|
102
|
+
t1 = _dt.datetime.now(_dt.timezone.utc)
|
|
103
|
+
dur_ms = int((t1 - t0).total_seconds() * 1000)
|
|
104
|
+
report = RebuildReport(
|
|
105
|
+
kind="partial", scope="all",
|
|
106
|
+
checked=audit["checked"], rebuilt=rebuilt,
|
|
107
|
+
corrupt_ids=corrupt_ids, missing_ids=missing_ids,
|
|
108
|
+
duration_ms=dur_ms, ts=iso(t1))
|
|
109
|
+
self._log(report)
|
|
110
|
+
return report
|
|
111
|
+
|
|
112
|
+
def rebuild_full(self, user_id: str | None = None) -> RebuildReport:
|
|
113
|
+
"""Drop all vectors and re-build from scratch from Trace.
|
|
114
|
+
|
|
115
|
+
Use after schema changes, codec swaps, or suspected widespread
|
|
116
|
+
corruption. Slow but authoritative.
|
|
117
|
+
"""
|
|
118
|
+
t0 = _dt.datetime.now(_dt.timezone.utc)
|
|
119
|
+
# fetch all facts
|
|
120
|
+
scope = f"user:{user_id}" if user_id else "all"
|
|
121
|
+
facts = self.store.query_facts(active=True, user_id=user_id)
|
|
122
|
+
# drop all vectors
|
|
123
|
+
self.palace.remove_ids([f.id for f in facts]) # clears them
|
|
124
|
+
rebuilt = 0
|
|
125
|
+
for fact in facts:
|
|
126
|
+
try:
|
|
127
|
+
vec = self.palace.encode_fact(fact)
|
|
128
|
+
self.palace.add(fact.id, vec)
|
|
129
|
+
rebuilt += 1
|
|
130
|
+
except Exception:
|
|
131
|
+
continue
|
|
132
|
+
t1 = _dt.datetime.now(_dt.timezone.utc)
|
|
133
|
+
dur_ms = int((t1 - t0).total_seconds() * 1000)
|
|
134
|
+
report = RebuildReport(
|
|
135
|
+
kind="full", scope=scope,
|
|
136
|
+
checked=len(facts), rebuilt=rebuilt,
|
|
137
|
+
corrupt_ids=[], missing_ids=[],
|
|
138
|
+
duration_ms=dur_ms, ts=iso(t1))
|
|
139
|
+
self._log(report)
|
|
140
|
+
return report
|
|
141
|
+
|
|
142
|
+
def rebuild_log(self, limit: int = 50) -> list[dict]:
|
|
143
|
+
"""Return recent rebuild log entries (tamper-evident audit)."""
|
|
144
|
+
rows = self.store.conn.execute(
|
|
145
|
+
"SELECT * FROM rebuild_log ORDER BY id DESC LIMIT ?", (limit,)
|
|
146
|
+
).fetchall()
|
|
147
|
+
return [dict(r) for r in rows]
|
|
148
|
+
|
|
149
|
+
# --- internals ---
|
|
150
|
+
def _lookup_fact(self, fact_id: str):
|
|
151
|
+
"""Fetch a fact by id from the TraceStore."""
|
|
152
|
+
try:
|
|
153
|
+
facts = self.store.query_facts(active=True)
|
|
154
|
+
for f in facts:
|
|
155
|
+
if f.id == fact_id:
|
|
156
|
+
return f
|
|
157
|
+
except Exception:
|
|
158
|
+
pass
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
def _log(self, report: RebuildReport) -> None:
|
|
162
|
+
self.store.conn.execute(
|
|
163
|
+
"""INSERT INTO rebuild_log(ts, kind, scope, checked, rebuilt, note)
|
|
164
|
+
VALUES(?,?,?,?,?,?)""",
|
|
165
|
+
(report.ts, report.kind, report.scope,
|
|
166
|
+
report.checked, report.rebuilt,
|
|
167
|
+
json.dumps({"corrupt_ids": report.corrupt_ids[:10],
|
|
168
|
+
"missing_ids": report.missing_ids[:10]}))
|
|
169
|
+
)
|
|
170
|
+
self.store.conn.commit()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
__all__ = ["TraceRebuilder", "RebuildReport", "REBUILD_LOG_TABLE"]
|