quantum-cortex 1.0.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.
Files changed (41) hide show
  1. cortex_c2/__init__.py +161 -0
  2. cortex_c2/consolidation.py +146 -0
  3. cortex_c2/router_ablation.py +254 -0
  4. cortex_c2/router_v2.py +109 -0
  5. cortex_c2b/__init__.py +724 -0
  6. cortex_c2b/companion_demo.py +203 -0
  7. cortex_c2b/crypto.py +102 -0
  8. cortex_c2b/external_arm.py +591 -0
  9. cortex_c2b/hm_lm.py +983 -0
  10. cortex_c2b/hm_protocol.py +308 -0
  11. cortex_c2b/lifecycle.py +454 -0
  12. cortex_c2b/lm_bridge.py +338 -0
  13. cortex_c2b/organ_use.py +254 -0
  14. cortex_c2b/read_path.py +218 -0
  15. cortex_c2b/reconstruction_probe.py +471 -0
  16. cortex_c2b/write_path.py +176 -0
  17. cortex_data/__init__.py +439 -0
  18. cortex_data/relay.py +174 -0
  19. cortex_eval/__init__.py +20 -0
  20. cortex_eval/ablation_mqar.py +187 -0
  21. cortex_eval/confirmation_run.py +133 -0
  22. cortex_eval/domain_map.py +127 -0
  23. cortex_eval/external_baseline.py +148 -0
  24. cortex_eval/ingest_confirmation.py +194 -0
  25. cortex_eval/mqar.py +122 -0
  26. cortex_eval/multiseed.py +148 -0
  27. cortex_eval/recurrent_base.py +197 -0
  28. cortex_eval/recurrent_ladder.py +159 -0
  29. cortex_eval/resumable_ladder.py +231 -0
  30. cortex_eval/resumable_rb.py +207 -0
  31. cortex_eval/run_mqar.py +127 -0
  32. cortex_eval/serial_position.py +116 -0
  33. quantum_cortex-1.0.0.dist-info/METADATA +209 -0
  34. quantum_cortex-1.0.0.dist-info/RECORD +41 -0
  35. quantum_cortex-1.0.0.dist-info/WHEEL +5 -0
  36. quantum_cortex-1.0.0.dist-info/licenses/AUTHORS.md +30 -0
  37. quantum_cortex-1.0.0.dist-info/licenses/LICENSE +73 -0
  38. quantum_cortex-1.0.0.dist-info/licenses/LICENSE-DOCS.md +34 -0
  39. quantum_cortex-1.0.0.dist-info/licenses/NOTICE +50 -0
  40. quantum_cortex-1.0.0.dist-info/top_level.txt +5 -0
  41. train.py +885 -0
@@ -0,0 +1,454 @@
1
+ """cortex_c2b.lifecycle -- Slice E: consolidate, demote, evict on a rhythm (ADR-007 D4 and D6).
2
+
3
+ The journal was a store (A), a gated write path (B), a router path (C) and a
4
+ measured organ (D). Slice E makes it ALIVE: on a rhythm (RES-9, the sleep of the
5
+ model) live entries are replayed into the persistent associative memory,
6
+ consolidated entries are demoted into content-addressed summaries (RES-16),
7
+ and forgetting is scheduled (D4.8: noise is what never contributes). Budgets are
8
+ homeostatic setpoints, not limits. It is a RES-17 state machine, the same law
9
+ the router uses (`cortex_c2.consolidation`), at the memory scale.
10
+
11
+ Eight invariants, validated by the founder on 2026-09-12 before any code, each
12
+ one exercised by `tests/test_c2b_lifecycle.py`:
13
+
14
+ 1. No shortcut around the state machine. Every transition, every salience
15
+ change (credit, contract, scheduled decay) and every demotion link is an
16
+ event in the append only log; replaying the log rebuilds the lifecycle
17
+ state exactly (states, saliences, links, references).
18
+ 2. Forward only, no resurrection. States move live -> consolidated ->
19
+ demoted -> evicted; an evicted entry never scores, never reads, never
20
+ returns. Payloads are released at the LAST pointer, by identity (the
21
+ content hash) and never by similarity: an irreversible operation rests
22
+ only on an exact relation.
23
+ 3. Consolidation is a replay, not a tick. An entry becomes `consolidated`
24
+ only after it has been replayed into the persistent associative memory
25
+ and its reconstruction verified under a declared error. Replay priority:
26
+ salience, then primacy (RES-9).
27
+ 4. Demotion keeps access. K SIMILAR consolidated entries (same index bucket,
28
+ pairwise cosine above a declared radius) become one summary: a content
29
+ addressed payload holding the K pointers and a digest; the sources move
30
+ to `demoted` and the summary is reachable from every source cue. A summary
31
+ can be demoted in turn (the hierarchy of RES-16); no cycle is possible.
32
+ 5. Eviction is a verdict, never a size cut. Live entries are evicted only by
33
+ the noise tribunal (Slice B rule, never modulated by pressure);
34
+ consolidated entries only below a salience floor and old enough; demoted
35
+ sources only with their summary (cascade). A live entry above the floor
36
+ survives any pressure.
37
+ 6. Budgets are setpoints. Per scope, bytes (non-evicted entries + payloads
38
+ alive) and read latency (entries touched per read, p95) are regulated
39
+ variables; pressure in [0, 1] rises with the distance to the setpoint and
40
+ acts on rhythms only (consolidation batch, eviction floor), never on the
41
+ rules above.
42
+ 7. An explicit, deterministic rhythm. Phases fire on the injected logical
43
+ clock every P admitted writes, from a hashed configuration and a seed;
44
+ two runs with the same configuration and event sequence write identical
45
+ logs. Every phase appends one telemetry snapshot.
46
+ 8. The H.M. check precedes the commit. A sentinel set of contracted facts
47
+ (the frozen H.M. generator) lives in the journal; every phase is planned
48
+ first, the sentinel probe is replayed on the plan, and the phase applies
49
+ only if contracted recall holds and the negative control does not rise.
50
+ Otherwise the phase is refused and recorded, pressure stays. No rollback
51
+ exists (invariant 2), so the check comes first -- as the breaker re routes
52
+ instead of killing. The protocol's thresholds are untouched.
53
+
54
+ 9. The journal survives a full process restart (ADR-007 Decision 8, founder,
55
+ 2026-09-12). A scheduler opened on a journal continues it: the persistent
56
+ memory is rebuilt from the consolidation order, the sentinels are found
57
+ again through their events, the phase and write counters resume. The test
58
+ that counts runs session B in a new process started from the disk alone,
59
+ sealed and clear (`test_the_journal_survives_a_full_process_restart`).
60
+
61
+ Design choices taken by the founder (2026-09-12): reference counting on
62
+ identity; the summary indexed under its source cues; refuse before commit; K,
63
+ P and the similarity radius in the hashed configuration; persistence declared
64
+ per component, sealed at rest per scope with the hub's QJE1 framing.
65
+
66
+ Pure Python over numpy; no torch. The persistent memory here is a stand-in
67
+ (`PersistentMemory`, a modern Hopfield store over cues); the C2 layer of
68
+ ADR-006 replaces it behind the same two calls when the journal is wired into
69
+ `train.py`. Written from scratch (our filon).
70
+ """
71
+ from __future__ import annotations
72
+
73
+ import json
74
+ import math
75
+ from dataclasses import dataclass, asdict, field
76
+
77
+ import numpy as np
78
+
79
+ from cortex_data import _hash_obj
80
+ from cortex_c2b import (Journal, Entry, STATE_LIVE, STATE_CONSOLIDATED, STATE_DEMOTED,
81
+ STATE_EVICTED, SCHEMA_SUMMARY, CUE_DIM, JournalReadOnly)
82
+ from cortex_c2b.write_path import WritePath, WriteReport, noise_candidates
83
+ from cortex_c2b.read_path import JournalPath
84
+ from cortex_c2b.hm_protocol import generate_facts, Fact
85
+
86
+
87
+ # ---------------------------------------------------------------------------- #
88
+ # Configuration -- hashed: whatever changes a result is in the hash (ADR-001/005) #
89
+ # ---------------------------------------------------------------------------- #
90
+ @dataclass(frozen=True)
91
+ class LifecycleConfig:
92
+ period_writes: int = 16 # P: one phase every P admitted writes
93
+ consolidate_batch: int = 8 # entries replayed per phase at pressure 0 (x2 at pressure 1)
94
+ replay_error_max: float = 0.05 # cosine distance reconstruction <-> cue that verifies a replay
95
+ demote_k: int = 4 # K: sources per summary
96
+ demote_min_cosine: float = 0.90 # similarity radius inside a group (pairwise cosine >= this)
97
+ evict_floor: float = 0.20 # salience floor for consolidated entries at pressure 0
98
+ evict_floor_max: float = 0.95 # the same floor at pressure 1
99
+ evict_min_age: float = 0.0 # logical age (now - t_written) before a consolidated entry is a candidate
100
+ tribunal_floor: float = 0.20 # Slice B rule for LIVE entries -- never modulated
101
+ tribunal_min_age: float = 0.0
102
+ decay: float = 0.90 # salience factor per phase on consolidated/demoted, contracted exempt
103
+ bytes_setpoint: int | None = None # regulated: non-evicted entries + payloads alive
104
+ latency_setpoint: float | None = None # regulated: p95 entries touched per read
105
+ band: float = 0.25 # pressure 0 at setpoint*(1-band), 1 at the setpoint
106
+ sentinel_facts: int = 20 # contracted facts planted at start (0 disables the check)
107
+ sentinel_negctrl: int = 10 # never planted facts probed at every phase
108
+ sentinel_seed: int = 0
109
+ seed: int = 0
110
+
111
+ def config_hash(self) -> str:
112
+ return _hash_obj(asdict(self))
113
+
114
+
115
+ # ---------------------------------------------------------------------------- #
116
+ # The persistent associative memory -- the "weights" side, as a stand-in #
117
+ # ---------------------------------------------------------------------------- #
118
+ class PersistentMemory:
119
+ """Modern-Hopfield store over cues: replay adds a pattern, reconstruct returns
120
+ the softmax weighted combination of stored patterns for a query. A replay
121
+ is verified by reading back the cue itself. Interference between near
122
+ duplicates makes a replay fail verification -- an honest capacity effect,
123
+ reported, never hidden. The C2 layer takes over behind these two calls."""
124
+
125
+ def __init__(self, dim: int = CUE_DIM, beta: float = 16.0):
126
+ self.dim, self.beta = dim, beta
127
+ self._K = np.zeros((0, dim), dtype=np.float32)
128
+
129
+ def __len__(self):
130
+ return int(self._K.shape[0])
131
+
132
+ @staticmethod
133
+ def _unit(v) -> np.ndarray:
134
+ v = np.asarray(v, dtype=np.float32)
135
+ return v / (np.linalg.norm(v) + 1e-8)
136
+
137
+ def reconstruct(self, cue) -> np.ndarray | None:
138
+ if len(self) == 0:
139
+ return None
140
+ q = self._unit(cue)
141
+ logits = self.beta * (self._K @ q)
142
+ w = np.exp(logits - logits.max()); w /= w.sum()
143
+ return w @ self._K
144
+
145
+ def replay(self, cue) -> float:
146
+ """Store the pattern and return the verification error (cosine distance
147
+ between the reconstruction and the cue). The caller decides on the
148
+ declared threshold; a failed replay is withdrawn from the store."""
149
+ q = self._unit(cue)
150
+ self._K = np.vstack([self._K, q[None, :]])
151
+ r = self.reconstruct(q)
152
+ err = float(np.clip(1.0 - float(r @ q / (np.linalg.norm(r) + 1e-8)), 0.0, 1.0))
153
+ return err
154
+
155
+ def withdraw_last(self) -> None:
156
+ self._K = self._K[:-1]
157
+
158
+
159
+ # ---------------------------------------------------------------------------- #
160
+ # A phase plan -- computed first, checked, then applied (or refused) #
161
+ # ---------------------------------------------------------------------------- #
162
+ @dataclass
163
+ class PhasePlan:
164
+ consolidate: list[str] = field(default_factory=list)
165
+ demote: list[list[str]] = field(default_factory=list)
166
+ evict: list[str] = field(default_factory=list) # verdicts (tribunal + salience), cascade included
167
+ pressure: float = 0.0
168
+ floor_eff: float = 0.0
169
+ batch_eff: int = 0
170
+
171
+ def evict_set(self) -> set[str]:
172
+ return set(self.evict)
173
+
174
+
175
+ class LifecycleScheduler:
176
+ """The sleep of the journal. Wire it above a Journal, its WritePath and its
177
+ JournalPath; route admitted writes through `write()` (or call `on_write`
178
+ after your own). Phases fire every P admitted writes on the logical clock
179
+ you pass as `now`; call `phase(now)` yourself for an explicit sleep.
180
+
181
+ Nothing here touches `Entry.state` or `Entry.salience` directly: the store's
182
+ primitives do, and each one is an event (invariant 1)."""
183
+
184
+ def __init__(self, journal: Journal, write_path: WritePath, journal_path: JournalPath,
185
+ cfg: LifecycleConfig | None = None, memory: PersistentMemory | None = None,
186
+ now: float = 0.0):
187
+ self.j, self.wp, self.jp = journal, write_path, journal_path
188
+ self.cfg = cfg or LifecycleConfig()
189
+ self.memory = memory if memory is not None else PersistentMemory(dim=journal.cue_dim) # an empty memory is falsy
190
+ self.replay_failed = 0 # telemetry only; the log's phase snapshots carry the totals
191
+ self.last_phase: dict | None = None
192
+ # ---- Decision 8: a scheduler opened on a journal continues it, it does not restart it
193
+ for eid in journal.consolidated_order: # the persistent memory, in consolidation order
194
+ self.memory.replay(journal._entries[eid].cue) # verified when it was consolidated
195
+ self.phases = journal.phase_count
196
+ self.refused = journal.refused_count
197
+ # the sentinel set: contracted facts from the frozen H.M. generator (invariant 8)
198
+ self.sentinels: list[tuple[str, Fact]] = []
199
+ self.negctrl: list[Fact] = []
200
+ if self.cfg.sentinel_facts > 0:
201
+ if journal.sentinels_recorded:
202
+ facts, _ = generate_facts(self.cfg.sentinel_facts, seed=self.cfg.sentinel_seed)
203
+ self.negctrl, _ = generate_facts(self.cfg.sentinel_negctrl, seed=self.cfg.sentinel_seed + 10_000)
204
+ for i, eid in journal.sentinels_recorded:
205
+ if i >= len(facts) or eid not in journal._entries:
206
+ raise ValueError("sentinel events do not match this configuration's sentinel set")
207
+ self.sentinels.append((eid, facts[i]))
208
+ else:
209
+ self._plant_sentinels(now)
210
+ sentinel_ids = {eid for eid, _ in self.sentinels}
211
+ self.writes_since_phase = sum(1 for eid in journal.writes_since_last_phase
212
+ if eid not in sentinel_ids
213
+ and journal._entries[eid].schema_id != SCHEMA_SUMMARY)
214
+
215
+ # ------------------------------------------------------------------ writes --
216
+ def write(self, cue, payload: bytes, schema_id: str, now: float):
217
+ """The one entry point in the lifecycle world: gated write, index, rhythm.
218
+ A journal in read only mode refuses the write and says so in the report
219
+ (Decision 9); a `stop` policy lets StorageExhausted propagate."""
220
+ try:
221
+ rep = self.wp.write(cue, payload, schema_id, now=now)
222
+ except JournalReadOnly as e:
223
+ return WriteReport(admitted=False, surprise=float("nan"), reason=f"refused, {e}")
224
+ if rep.admitted:
225
+ self.jp.on_write(rep.entry)
226
+ self.on_write(rep.entry, now)
227
+ return rep
228
+
229
+ def on_write(self, entry: Entry, now: float) -> dict | None:
230
+ """Count an admitted write; fire a phase every P (invariant 7)."""
231
+ self.writes_since_phase += 1
232
+ if self.writes_since_phase >= self.cfg.period_writes:
233
+ self.writes_since_phase = 0
234
+ return self.phase(now)
235
+ return None
236
+
237
+ def contract(self, entry_id: str, now: float | None = None) -> None:
238
+ """Credit an entry to full salience under a contract: decay never touches it."""
239
+ self.j.set_salience(entry_id, 1.0, why="contract", now=now)
240
+
241
+ # -------------------------------------------------------------- sentinels --
242
+ def _plant_sentinels(self, now: float) -> None:
243
+ facts, _ = generate_facts(self.cfg.sentinel_facts, seed=self.cfg.sentinel_seed)
244
+ self.negctrl, _ = generate_facts(self.cfg.sentinel_negctrl, seed=self.cfg.sentinel_seed + 10_000)
245
+ for i, f in enumerate(facts):
246
+ rep = self.wp.write(f.cue, f.statement.encode(), f.schema, now=now)
247
+ if rep.admitted:
248
+ self.jp.on_write(rep.entry)
249
+ self.contract(rep.entry.entry_id, now)
250
+ self.j.mark_sentinel(i, rep.entry.entry_id) # an event: a restart finds them again
251
+ self.sentinels.append((rep.entry.entry_id, f))
252
+
253
+ def _hit(self, fact: Fact) -> tuple[str | None, bytes | None]:
254
+ """The effective entry a probe of this fact lands on, and its payload."""
255
+ hits = self.jp.retrieve(fact.cue, k=1)
256
+ if not hits:
257
+ return None, None
258
+ e, payload, _ = hits[0]
259
+ return e.entry_id, payload
260
+
261
+ def sentinel_probe(self, plan: PhasePlan | None = None) -> tuple[float, float]:
262
+ """Contracted recall and negative-control rate through the real read path,
263
+ with a plan overlaid: a hit that the plan would evict counts as lost. A
264
+ plan can only remove entries, so it can never raise the negative
265
+ control; the guard is kept because the rule says so."""
266
+ gone = plan.evict_set() if plan else set()
267
+ if not self.sentinels:
268
+ return 1.0, 0.0
269
+ recalled = 0
270
+ for eid, f in self.sentinels:
271
+ hit, payload = self._hit(f)
272
+ if hit is not None and hit not in gone and f.attr in payload.decode("utf-8", errors="ignore"):
273
+ recalled += 1
274
+ halluc = 0
275
+ for f in self.negctrl:
276
+ hit, payload = self._hit(f)
277
+ if hit is not None and hit not in gone and f.attr in payload.decode("utf-8", errors="ignore"):
278
+ halluc += 1
279
+ return recalled / len(self.sentinels), (halluc / len(self.negctrl) if self.negctrl else 0.0)
280
+
281
+ # --------------------------------------------------------------- pressure --
282
+ @staticmethod
283
+ def _pressure_of(usage: float, setpoint: float | None, band: float) -> float:
284
+ if not setpoint or setpoint <= 0:
285
+ return 0.0
286
+ return float(np.clip((usage / setpoint - (1.0 - band)) / band, 0.0, 1.0))
287
+
288
+ def pressure(self) -> dict:
289
+ b = self.j.regulated_bytes()
290
+ lat = self.jp.index.p95_touched()
291
+ pb = self._pressure_of(b, self.cfg.bytes_setpoint, self.cfg.band)
292
+ pl = self._pressure_of(lat, self.cfg.latency_setpoint, self.cfg.band)
293
+ # Decision 9, invariant 2: free disk space is regulated too -- pressure rises as the
294
+ # free space approaches the declared floor (0 at floor*(1+band), 1 at the floor)
295
+ floor, free = self.j.disk_free_floor_bytes, self.j.disk_free()
296
+ pd = 0.0
297
+ if floor and free is not None:
298
+ pd = float(np.clip((floor * (1.0 + self.cfg.band) - free) / (floor * self.cfg.band), 0.0, 1.0))
299
+ return {"bytes": b, "latency_p95": lat, "disk_free": free, "pressure_bytes": pb,
300
+ "pressure_latency": pl, "pressure_disk": pd, "pressure": max(pb, pl, pd)}
301
+
302
+ # ------------------------------------------------------------------- plan --
303
+ def plan(self, now: float) -> PhasePlan:
304
+ cfg, J = self.cfg, self.j
305
+ pr = self.pressure()["pressure"]
306
+ p = PhasePlan(pressure=pr,
307
+ floor_eff=cfg.evict_floor + pr * (cfg.evict_floor_max - cfg.evict_floor),
308
+ batch_eff=int(math.ceil(cfg.consolidate_batch * (1.0 + pr))))
309
+ # 3. consolidate: salience first, then primacy (oldest first), deterministic
310
+ live = [(eid, e) for eid, e in J._entries.items() if e.state == STATE_LIVE]
311
+ live.sort(key=lambda t: (-t[1].salience, t[1].t_written, t[0]))
312
+ p.consolidate = [eid for eid, _ in live[: p.batch_eff]]
313
+ # 4. demote: K similar consolidated entries per group -- similarity means the
314
+ # index neighbourhood (a bucket in any table) AND an exact pairwise cosine
315
+ # above the declared radius; deterministic order; never a scan of the store
316
+ consolidated = [eid for eid, e in sorted(J._entries.items()) if e.state == STATE_CONSOLIDATED]
317
+ cset, grouped = set(consolidated), set()
318
+ for eid in consolidated:
319
+ if eid in grouped:
320
+ continue
321
+ cue = np.asarray(J._entries[eid].cue, dtype=np.float32)
322
+ scored = []
323
+ for c in self.jp.index.candidates(cue):
324
+ if c in cset and c not in grouped and c != eid:
325
+ v = np.asarray(J._entries[c].cue, dtype=np.float32)
326
+ cos = float(cue @ v / (np.linalg.norm(cue) * np.linalg.norm(v) + 1e-8))
327
+ if cos >= cfg.demote_min_cosine:
328
+ scored.append((-cos, c))
329
+ members = [eid] + [c for _, c in sorted(scored)[: cfg.demote_k - 1]]
330
+ if len(members) == cfg.demote_k and self._min_pairwise_cosine(members) >= cfg.demote_min_cosine:
331
+ p.demote.append(members); grouped.update(members)
332
+ # 5. evict: verdicts only
333
+ evict = list(noise_candidates(J, cfg.tribunal_floor, cfg.tribunal_min_age, now)) # live, the tribunal
334
+ for eid, e in sorted(J._entries.items()):
335
+ if (e.state == STATE_CONSOLIDATED and eid not in grouped and e.salience < p.floor_eff
336
+ and (now - e.t_written) >= cfg.evict_min_age):
337
+ evict.append(eid)
338
+ for eid in list(evict): # cascade, whatever the branch: a summary
339
+ for src in J.summary_sources.get(eid, []): # takes its sources with it (the store
340
+ if J._entries[src].state != STATE_EVICTED and src not in evict: # enforces the same rule)
341
+ evict.append(src)
342
+ for src, sid in sorted(J.summary_of.items()): # repair: a demoted source whose summary is
343
+ if (J._entries[sid].state == STATE_EVICTED and J._entries[src].state == STATE_DEMOTED
344
+ and src not in evict): # already gone (a cascade cut by a storage
345
+ evict.append(src) # failure, policy stop) is evicted now
346
+ p.evict = evict
347
+ return p
348
+
349
+ def _min_pairwise_cosine(self, ids: list[str]) -> float:
350
+ V = np.stack([np.asarray(self.j._entries[i].cue, dtype=np.float32) for i in ids])
351
+ V = V / (np.linalg.norm(V, axis=1, keepdims=True) + 1e-8)
352
+ S = V @ V.T
353
+ return float(S[np.triu_indices(len(ids), k=1)].min()) if len(ids) > 1 else 1.0
354
+
355
+ # ------------------------------------------------------------------ phase --
356
+ def phase(self, now: float) -> dict:
357
+ """One sleep: plan, check the sentinels on the plan, then apply or refuse."""
358
+ cfg = self.cfg
359
+ p = self.plan(now)
360
+ recall_before, neg_before = self.sentinel_probe(None)
361
+ recall_after, neg_after = self.sentinel_probe(p)
362
+ ok = (recall_after >= 1.0) and (neg_after <= neg_before)
363
+ rec = {"phase": self.phases + 1, "now": now, "config_hash": cfg.config_hash(),
364
+ "pressure": round(p.pressure, 4), "floor_eff": round(p.floor_eff, 4),
365
+ "batch_eff": p.batch_eff, "planned": {"consolidate": len(p.consolidate),
366
+ "demote_groups": len(p.demote), "evict": len(p.evict)},
367
+ "sentinel_recall": round(recall_after, 4), "sentinel_negctrl": round(neg_after, 4),
368
+ "refused": not ok}
369
+ if not ok:
370
+ self.refused += 1
371
+ rec["reason"] = ("contracted recall would fall to %.3f" % recall_after if recall_after < 1.0
372
+ else "negative control would rise")
373
+ rec.update(self._log_telemetry())
374
+ self.j.phase_event(rec)
375
+ self.phases += 1
376
+ self.last_phase = rec
377
+ return rec
378
+ applied = self._apply(p, now)
379
+ self.j.decay(cfg.decay) # one event; contracted exempt
380
+ rec["applied"] = applied
381
+ rec.update(self._log_telemetry())
382
+ self.j.phase_event(rec)
383
+ self.phases += 1
384
+ self.last_phase = rec
385
+ return rec
386
+
387
+ def _apply(self, p: PhasePlan, now: float) -> dict:
388
+ J, cfg = self.j, self.cfg
389
+ consolidated, failed = 0, 0
390
+ for eid in p.consolidate: # 3. replay, verify, then transition
391
+ err = self.memory.replay(J._entries[eid].cue)
392
+ if err <= cfg.replay_error_max:
393
+ J.transition(eid, STATE_CONSOLIDATED); consolidated += 1
394
+ else:
395
+ self.memory.withdraw_last(); failed += 1
396
+ self.replay_failed += failed
397
+ demoted = 0
398
+ for group in p.demote: # 4. K similar -> one summary
399
+ self._demote(group, now); demoted += 1
400
+ evicted = 0
401
+ for eid in p.evict: # 5. verdicts (cascade already listed)
402
+ if J._entries[eid].state != STATE_EVICTED:
403
+ J.transition(eid, STATE_EVICTED); evicted += 1
404
+ return {"consolidated": consolidated, "replay_failed": failed,
405
+ "demoted_groups": demoted, "evicted": evicted}
406
+
407
+ def _demote(self, group: list[str], now: float) -> Entry:
408
+ J = self.j
409
+ cues = np.stack([np.asarray(J._entries[i].cue, dtype=np.float32) for i in group])
410
+ centroid = cues.mean(axis=0); centroid = centroid / (np.linalg.norm(centroid) + 1e-8)
411
+ level = 1 + max(J.summary_level.get(i, 0) for i in group)
412
+ payload = json.dumps({"kind": SCHEMA_SUMMARY, "level": level,
413
+ "pointers": [J._entries[i].pointer for i in group],
414
+ "sources": list(group),
415
+ "digest": [round(float(x), 3) for x in centroid[:8]]},
416
+ separators=(",", ":")).encode()
417
+ salience = max(J._entries[i].salience for i in group)
418
+ summary = J.write(centroid, payload, salience=salience, schema_id=SCHEMA_SUMMARY, now=now)
419
+ self.jp.on_write(summary)
420
+ J.demote(summary.entry_id, list(group), level=level) # the link (an event)
421
+ for src in group:
422
+ J.transition(src, STATE_DEMOTED) # each a transition event
423
+ self.jp.index.set_alias(src, summary.entry_id) # reachable from every source cue
424
+ if any(i in J.contracted for i in group):
425
+ self.contract(summary.entry_id, now) # a contract survives its demotion
426
+ return summary
427
+
428
+ # -------------------------------------------------------------- telemetry --
429
+ def _telemetry(self) -> dict:
430
+ snap = self.j.snapshot()
431
+ pr = self.pressure()
432
+ return {"bytes": pr["bytes"], "latency_p95": round(pr["latency_p95"], 2),
433
+ "disk_free": pr["disk_free"], "pressure_disk": round(pr["pressure_disk"], 4),
434
+ "storage_mode": self.j.mode, "unpersisted_events": len(self.j.spill),
435
+ "scope_hash": self.j.scope_config_hash(),
436
+ "pressure_bytes": round(pr["pressure_bytes"], 4),
437
+ "pressure_latency": round(pr["pressure_latency"], 4),
438
+ "live": snap[STATE_LIVE], "consolidated": snap[STATE_CONSOLIDATED],
439
+ "demoted": snap[STATE_DEMOTED], "evicted": snap[STATE_EVICTED],
440
+ "summaries": snap["summaries"], "contracted": snap["contracted"],
441
+ "memory_patterns": len(self.memory), "phases_refused": self.refused,
442
+ "replay_failed_total": self.replay_failed}
443
+
444
+ def _log_telemetry(self) -> dict:
445
+ """What goes into the phase event: everything but the raw free disk bytes, an
446
+ environment reading that would break the byte identity of two identical
447
+ runs (invariant 7). The disk pressure stays: it is a function of the
448
+ declared floor, zero when none is declared."""
449
+ t = self._telemetry(); t.pop("disk_free", None)
450
+ return t
451
+
452
+ def snapshot(self) -> dict:
453
+ """The RES-17 snapshot for C6 telemetry / the run record's standard_suite."""
454
+ return {"config_hash": self.cfg.config_hash(), "phases": self.phases, **self._telemetry()}