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.
- cortex_c2/__init__.py +161 -0
- cortex_c2/consolidation.py +146 -0
- cortex_c2/router_ablation.py +254 -0
- cortex_c2/router_v2.py +109 -0
- cortex_c2b/__init__.py +724 -0
- cortex_c2b/companion_demo.py +203 -0
- cortex_c2b/crypto.py +102 -0
- cortex_c2b/external_arm.py +591 -0
- cortex_c2b/hm_lm.py +983 -0
- cortex_c2b/hm_protocol.py +308 -0
- cortex_c2b/lifecycle.py +454 -0
- cortex_c2b/lm_bridge.py +338 -0
- cortex_c2b/organ_use.py +254 -0
- cortex_c2b/read_path.py +218 -0
- cortex_c2b/reconstruction_probe.py +471 -0
- cortex_c2b/write_path.py +176 -0
- cortex_data/__init__.py +439 -0
- cortex_data/relay.py +174 -0
- cortex_eval/__init__.py +20 -0
- cortex_eval/ablation_mqar.py +187 -0
- cortex_eval/confirmation_run.py +133 -0
- cortex_eval/domain_map.py +127 -0
- cortex_eval/external_baseline.py +148 -0
- cortex_eval/ingest_confirmation.py +194 -0
- cortex_eval/mqar.py +122 -0
- cortex_eval/multiseed.py +148 -0
- cortex_eval/recurrent_base.py +197 -0
- cortex_eval/recurrent_ladder.py +159 -0
- cortex_eval/resumable_ladder.py +231 -0
- cortex_eval/resumable_rb.py +207 -0
- cortex_eval/run_mqar.py +127 -0
- cortex_eval/serial_position.py +116 -0
- quantum_cortex-1.0.0.dist-info/METADATA +209 -0
- quantum_cortex-1.0.0.dist-info/RECORD +41 -0
- quantum_cortex-1.0.0.dist-info/WHEEL +5 -0
- quantum_cortex-1.0.0.dist-info/licenses/AUTHORS.md +30 -0
- quantum_cortex-1.0.0.dist-info/licenses/LICENSE +73 -0
- quantum_cortex-1.0.0.dist-info/licenses/LICENSE-DOCS.md +34 -0
- quantum_cortex-1.0.0.dist-info/licenses/NOTICE +50 -0
- quantum_cortex-1.0.0.dist-info/top_level.txt +5 -0
- train.py +885 -0
cortex_c2b/__init__.py
ADDED
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
"""cortex_c2b — the persistent tier (ADR-007), Slice A: the store.
|
|
2
|
+
|
|
3
|
+
The journal is the episodic organ: weights = semantic and procedural; journal =
|
|
4
|
+
episodic. It lives OUTSIDE the weights and survives the session boundary — the
|
|
5
|
+
thing the H.M. protocol measures. Slice A ships the store and its three laws:
|
|
6
|
+
|
|
7
|
+
Size law — an entry is ≤ 1 KB: (cue, pointer, salience, schema_id, timestamps,
|
|
8
|
+
state). Content is NEVER duplicated into the journal: the payload
|
|
9
|
+
lives in a content-addressed payload store (cortex_data hashing
|
|
10
|
+
reused) and the entry carries only its hash.
|
|
11
|
+
Read law — retrieval is never a scan. Reads go through an index keyed by cue
|
|
12
|
+
(a sub-linear structure); an O(N) scan over entries is prohibited
|
|
13
|
+
BY TEST (test_no_scan fails if a read touches every entry).
|
|
14
|
+
Lifecycle — every entry carries its RES-17 (type,state) cell:
|
|
15
|
+
live → consolidated → demoted → evicted. Slice A stores and
|
|
16
|
+
transitions the state; the scheduling (RES-9) is Slice E.
|
|
17
|
+
|
|
18
|
+
Deliberately NOT in Slice A: the write path (DG separation, CA1 surprise, the
|
|
19
|
+
two-stage noise gate — Slice B), the router path (Slice C), the H.M. protocol
|
|
20
|
+
(Slice D), consolidation/eviction scheduling (Slice E). The store is the
|
|
21
|
+
substrate they all need; it is built first so each can be tested against it.
|
|
22
|
+
|
|
23
|
+
Persistence is a property PER COMPONENT (ADR-007 Decision 8, founder,
|
|
24
|
+
2026-09-12): "survives the session boundary" means after a full process stop
|
|
25
|
+
and relaunch, not within one process. Each component declares its lifecycle
|
|
26
|
+
(`lifecycle_declaration()`):
|
|
27
|
+
structure -- the append-only log (`journal.jsonl`): entries, states,
|
|
28
|
+
saliences, links, references, phases;
|
|
29
|
+
bytes -- the content-addressed payload directory next to the log
|
|
30
|
+
(`journal.payloads/<pointer>`), one file per payload,
|
|
31
|
+
released at the last reference;
|
|
32
|
+
index -- recomputed at replay from the log with a declared seed
|
|
33
|
+
(`JournalPath(journal, seed)`), aliases from the links;
|
|
34
|
+
associative memory -- CA3 (write path) and the persistent memory (lifecycle)
|
|
35
|
+
rebuilt from their own events in the log today; checkpointed
|
|
36
|
+
with the weights once the C2 layer takes over.
|
|
37
|
+
At rest, log lines and payload files are encrypted per scope with the hub's
|
|
38
|
+
QJE1 framing (AES-256-GCM; `cortex_c2b.crypto`) when a key is given; the key is
|
|
39
|
+
an explicit argument, never read implicitly (key management deferred, hub 010).
|
|
40
|
+
|
|
41
|
+
Written from scratch (our filon). ANN over cues is Slice C's job; Slice A uses
|
|
42
|
+
an exact keyed index so the read law is enforced without a heavy dependency.
|
|
43
|
+
"""
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import errno
|
|
47
|
+
import json
|
|
48
|
+
import shutil
|
|
49
|
+
import time
|
|
50
|
+
from collections import deque
|
|
51
|
+
from dataclasses import dataclass, field, asdict
|
|
52
|
+
from pathlib import Path
|
|
53
|
+
|
|
54
|
+
import numpy as np
|
|
55
|
+
|
|
56
|
+
from cortex_data import _hash_obj # content hashing reused (ADR-005)
|
|
57
|
+
from cortex_c2b.crypto import (seal, open_sealed, is_sealed, sealed_overhead, seal_line, open_line,
|
|
58
|
+
MAGIC)
|
|
59
|
+
|
|
60
|
+
# --- lifecycle states (RES-17 cell) -----------------------------------------
|
|
61
|
+
STATE_LIVE = "live"
|
|
62
|
+
STATE_CONSOLIDATED = "consolidated"
|
|
63
|
+
STATE_DEMOTED = "demoted"
|
|
64
|
+
STATE_EVICTED = "evicted"
|
|
65
|
+
_TRANSITIONS = {
|
|
66
|
+
STATE_LIVE: {STATE_CONSOLIDATED, STATE_EVICTED},
|
|
67
|
+
STATE_CONSOLIDATED: {STATE_DEMOTED, STATE_EVICTED},
|
|
68
|
+
STATE_DEMOTED: {STATE_EVICTED},
|
|
69
|
+
STATE_EVICTED: set(),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
SCHEMA_SUMMARY = "summary" # the schema_id of a demotion summary (Slice E)
|
|
73
|
+
|
|
74
|
+
# --- storage policy (ADR-007 Decision 9) --------------------------------------
|
|
75
|
+
POLICY_STOP = "stop" # a storage failure raises StorageExhausted; the caller decides (measurement runs)
|
|
76
|
+
POLICY_READ_ONLY = "read_only" # writes are refused (JournalReadOnly), reads continue, the state stays durable
|
|
77
|
+
POLICY_MEMORY = "memory" # writes continue in a bounded spill buffer, flushed when the disk returns; declared not durable
|
|
78
|
+
POLICIES = (POLICY_STOP, POLICY_READ_ONLY, POLICY_MEMORY)
|
|
79
|
+
MODE_DURABLE, MODE_READ_ONLY, MODE_MEMORY = "durable", "read_only", "memory"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class StorageDegraded(OSError):
|
|
83
|
+
"""Base: the journal could not persist. Never silent (invariant 1)."""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class StorageExhausted(StorageDegraded):
|
|
87
|
+
"""Policy `stop`: the journal is consistent, nothing half written, the caller decides."""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class JournalReadOnly(StorageDegraded):
|
|
91
|
+
"""Policy `read_only` (or a full spill buffer): this write is refused; reads continue."""
|
|
92
|
+
|
|
93
|
+
ENTRY_MAX_BYTES = 1024 # the size law
|
|
94
|
+
CUE_DIM = 64 # fixed-size cue (matryoshka-truncatable later)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def content_hash(payload: bytes) -> str:
|
|
98
|
+
"""Pointer = content hash of the payload (never the payload itself)."""
|
|
99
|
+
import hashlib
|
|
100
|
+
return hashlib.sha256(payload).hexdigest()[:16]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class Entry:
|
|
105
|
+
"""One journal entry — the ≤1 KB retention-law object."""
|
|
106
|
+
cue: list[float] # fixed-size address embedding (CUE_DIM floats)
|
|
107
|
+
pointer: str # content hash of the payload (in the payload store)
|
|
108
|
+
salience: float
|
|
109
|
+
schema_id: str # span-contract tag (RES-8)
|
|
110
|
+
t_written: float
|
|
111
|
+
t_last_read: float
|
|
112
|
+
state: str = STATE_LIVE
|
|
113
|
+
entry_id: str = ""
|
|
114
|
+
|
|
115
|
+
def to_json(self) -> str:
|
|
116
|
+
return json.dumps(asdict(self), separators=(",", ":"))
|
|
117
|
+
|
|
118
|
+
def size_bytes(self) -> int:
|
|
119
|
+
return len(self.to_json().encode("utf-8"))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class PayloadStore:
|
|
123
|
+
"""Content-addressed payload storage. Same bytes → same hash → stored once.
|
|
124
|
+
The journal never holds content; it holds pointers here.
|
|
125
|
+
|
|
126
|
+
`directory` (optional): one file per payload, named by its pointer, next to
|
|
127
|
+
the journal log -- the bytes' own lifecycle (Decision 8). With a `key` each
|
|
128
|
+
file is sealed (QJE1 binary framing, the pointer as associated data, so a
|
|
129
|
+
file cannot be swapped under another pointer). Without a directory the
|
|
130
|
+
store is memory only -- and says so in `lifecycle_declaration()`."""
|
|
131
|
+
|
|
132
|
+
def __init__(self, directory: str | Path | None = None, key: bytes | None = None):
|
|
133
|
+
self._blobs: dict[str, bytes] = {} # plaintext cache
|
|
134
|
+
self._sizes: dict[str, int] = {} # pointer -> plaintext size, known without reading
|
|
135
|
+
self._pending: set[str] = set() # registered in memory, not yet on disk (memory mode)
|
|
136
|
+
self._dir = Path(directory) if directory else None
|
|
137
|
+
self._key = key
|
|
138
|
+
if self._dir:
|
|
139
|
+
self._dir.mkdir(parents=True, exist_ok=True) # may raise: the journal applies its policy
|
|
140
|
+
for f in self._dir.iterdir():
|
|
141
|
+
if f.is_file() and len(f.name) == 16:
|
|
142
|
+
with open(f, "rb") as fh:
|
|
143
|
+
head = fh.read(len(MAGIC))
|
|
144
|
+
self._sizes[f.name] = f.stat().st_size - (sealed_overhead() if head == MAGIC else 0)
|
|
145
|
+
|
|
146
|
+
def put(self, payload: bytes, durable: bool = True) -> str:
|
|
147
|
+
"""Durable first: the file reaches the disk before the pointer is registered,
|
|
148
|
+
so a failure leaves nothing half registered (Decision 9, invariant 4).
|
|
149
|
+
`durable=False` registers in memory only and marks the pointer pending
|
|
150
|
+
(memory mode); `flush_pending()` writes it later."""
|
|
151
|
+
h = content_hash(payload)
|
|
152
|
+
if h in self._sizes:
|
|
153
|
+
self._blobs.setdefault(h, payload)
|
|
154
|
+
return h
|
|
155
|
+
if self._dir and durable:
|
|
156
|
+
self._write_file(h, payload) # may raise OSError
|
|
157
|
+
elif self._dir:
|
|
158
|
+
self._pending.add(h)
|
|
159
|
+
self._sizes[h] = len(payload)
|
|
160
|
+
self._blobs[h] = payload
|
|
161
|
+
return h
|
|
162
|
+
|
|
163
|
+
def _write_file(self, h: str, payload: bytes) -> None:
|
|
164
|
+
blob = seal(self._key, payload, h.encode()) if self._key else payload
|
|
165
|
+
tmp = self._dir / (h + ".tmp")
|
|
166
|
+
try:
|
|
167
|
+
tmp.write_bytes(blob); tmp.replace(self._dir / h) # never a half-written file
|
|
168
|
+
except OSError:
|
|
169
|
+
tmp.unlink(missing_ok=True); raise
|
|
170
|
+
|
|
171
|
+
def flush_pending(self) -> int:
|
|
172
|
+
"""Write every pending payload (memory mode recovery). Raises on the first failure."""
|
|
173
|
+
n = 0
|
|
174
|
+
for h in sorted(self._pending):
|
|
175
|
+
self._write_file(h, self._blobs[h]); self._pending.discard(h); n += 1
|
|
176
|
+
return n
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def pending(self) -> int:
|
|
180
|
+
return len(self._pending)
|
|
181
|
+
|
|
182
|
+
def get(self, pointer: str) -> bytes:
|
|
183
|
+
if pointer in self._blobs:
|
|
184
|
+
return self._blobs[pointer]
|
|
185
|
+
if pointer not in self._sizes or not self._dir:
|
|
186
|
+
raise KeyError(pointer)
|
|
187
|
+
blob = (self._dir / pointer).read_bytes()
|
|
188
|
+
if is_sealed(blob):
|
|
189
|
+
if self._key is None:
|
|
190
|
+
raise ValueError(f"payload {pointer} is sealed and no key was given")
|
|
191
|
+
payload = open_sealed(self._key, blob, pointer.encode()) # InvalidTag on tampering / wrong key
|
|
192
|
+
else:
|
|
193
|
+
payload = blob # legacy clear file, coexists
|
|
194
|
+
if content_hash(payload) != pointer:
|
|
195
|
+
raise ValueError(f"integrity: payload {pointer} does not match its pointer")
|
|
196
|
+
self._blobs[pointer] = payload
|
|
197
|
+
return payload
|
|
198
|
+
|
|
199
|
+
def release(self, pointer: str) -> bool:
|
|
200
|
+
"""Drop the bytes behind a pointer. Called by the journal only when the
|
|
201
|
+
last non-evicted reference is gone (Slice E, reference counting)."""
|
|
202
|
+
existed = pointer in self._sizes
|
|
203
|
+
self._blobs.pop(pointer, None); self._sizes.pop(pointer, None); self._pending.discard(pointer)
|
|
204
|
+
if self._dir:
|
|
205
|
+
try:
|
|
206
|
+
(self._dir / pointer).unlink(missing_ok=True)
|
|
207
|
+
except OSError:
|
|
208
|
+
pass # a release that cannot delete is retried at the next open
|
|
209
|
+
return existed
|
|
210
|
+
|
|
211
|
+
def __contains__(self, pointer: str) -> bool:
|
|
212
|
+
return pointer in self._sizes
|
|
213
|
+
|
|
214
|
+
def __len__(self):
|
|
215
|
+
return len(self._sizes)
|
|
216
|
+
|
|
217
|
+
def total_bytes(self) -> int:
|
|
218
|
+
return sum(self._sizes.values())
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def persistent(self) -> bool:
|
|
222
|
+
return self._dir is not None
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def sealed(self) -> bool:
|
|
226
|
+
return self._key is not None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class Journal:
|
|
230
|
+
"""Append-only journal with a keyed index. Reads never scan.
|
|
231
|
+
|
|
232
|
+
`path` (optional): an append-only JSONL file; every write appends one line,
|
|
233
|
+
nothing is rewritten (the ledger discipline). State transitions are appended
|
|
234
|
+
as events, so the file is a full provenance log (RES-17: the object's history).
|
|
235
|
+
|
|
236
|
+
Decision 8 -- sealed by default: an on-disk journal needs a `key` (32 bytes);
|
|
237
|
+
`plaintext=True` declares a test scope explicitly; a memory-only journal
|
|
238
|
+
(no path) needs neither. Opening without either fails loudly.
|
|
239
|
+
|
|
240
|
+
Decision 9 -- durable first, and a declared storage policy per scope:
|
|
241
|
+
every mutation reaches the log (and the payload its file) BEFORE memory
|
|
242
|
+
changes, so a failure leaves no half state. When the disk fails (no space,
|
|
243
|
+
no permission, or free space under the declared floor), `policy` decides:
|
|
244
|
+
`stop` raises StorageExhausted; `read_only` refuses the write and keeps
|
|
245
|
+
reading; `memory` keeps writing into a bounded spill buffer, declared not
|
|
246
|
+
durable, flushed with a `storage_restored` event when the disk returns.
|
|
247
|
+
The policy, the floor, the sealing and the spill bound are the scope's
|
|
248
|
+
configuration (`scope_config_hash()`).
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
def __init__(self, path: str | Path | None = None, cue_dim: int = CUE_DIM,
|
|
252
|
+
key: bytes | None = None, payload_dir: str | Path | None = None,
|
|
253
|
+
plaintext: bool = False, policy: str = POLICY_READ_ONLY,
|
|
254
|
+
disk_free_floor_bytes: int | None = None, spill_max_events: int = 10_000,
|
|
255
|
+
disk_check_every: int = 16):
|
|
256
|
+
if policy not in POLICIES:
|
|
257
|
+
raise ValueError(f"policy must be one of {POLICIES}, got {policy!r}")
|
|
258
|
+
self.cue_dim = cue_dim
|
|
259
|
+
self._path = Path(path) if path else None
|
|
260
|
+
if self._path is not None and key is None and not plaintext:
|
|
261
|
+
raise ValueError("an on-disk journal is sealed by default (ADR-007 D8): pass key=<32 bytes>, "
|
|
262
|
+
"or plaintext=True to declare a test scope explicitly")
|
|
263
|
+
self._key = key
|
|
264
|
+
self.plaintext = bool(plaintext) if self._path is not None else False
|
|
265
|
+
self.policy = policy
|
|
266
|
+
self.disk_free_floor_bytes = disk_free_floor_bytes
|
|
267
|
+
self.spill_max_events = int(spill_max_events)
|
|
268
|
+
self._disk_check_every = max(1, int(disk_check_every))
|
|
269
|
+
self._persist_count = 0
|
|
270
|
+
# ---- storage state (Decision 9) ----
|
|
271
|
+
self.mode = MODE_DURABLE if self._path is None else MODE_DURABLE
|
|
272
|
+
self.degraded_reason: str | None = None
|
|
273
|
+
self.spill: deque[str] = deque() # encoded lines not yet on disk (memory mode); a deque: O(1) at both ends
|
|
274
|
+
self.refused_writes = 0 # read_only mode: mutations refused since the failure
|
|
275
|
+
self.gap_since: float | None = None # logical time of the first failure of the current gap
|
|
276
|
+
self._last_now: float | None = None
|
|
277
|
+
self.restorations: list[dict] = [] # storage_restored records (this object and the log)
|
|
278
|
+
# ---- the store ----
|
|
279
|
+
if payload_dir is None and self._path is not None:
|
|
280
|
+
payload_dir = self._path.with_name(self._path.stem + ".payloads") # the bytes, next to the log
|
|
281
|
+
self._entries: dict[str, Entry] = {} # entry_id -> Entry
|
|
282
|
+
self._by_cue_key: dict[str, list[str]] = {} # cue key -> entry ids (keyed index)
|
|
283
|
+
self._reads_touched = 0 # instrumentation for the no-scan test
|
|
284
|
+
# ---- what the associative memories and the scheduler rebuild from (Decision 8) ----
|
|
285
|
+
self.ca3_order: list[tuple[str, dict]] = [] # (entry_id, meta) in write-path admission order
|
|
286
|
+
self.consolidated_order: list[str] = [] # entry ids in consolidation order
|
|
287
|
+
self.sentinels_recorded: list[tuple[int, str]] = [] # (fact index, entry_id)
|
|
288
|
+
self.phase_count = 0
|
|
289
|
+
self.refused_count = 0
|
|
290
|
+
self.writes_since_last_phase: list[str] = []
|
|
291
|
+
# ---- Slice E state, every item rebuilt from the log on replay ----
|
|
292
|
+
self._refs: dict[str, set[str]] = {} # pointer -> non-evicted entry ids referencing it
|
|
293
|
+
self.summary_sources: dict[str, list[str]] = {} # summary id -> its K source ids
|
|
294
|
+
self.summary_of: dict[str, str] = {} # source id -> summary id
|
|
295
|
+
self.summary_level: dict[str, int] = {} # summary id -> hierarchy level (1 = of entries)
|
|
296
|
+
self.contracted: set[str] = set() # ids credited under a contract (never decayed)
|
|
297
|
+
# ---- open: the payload directory, the log, the writability probe ----
|
|
298
|
+
try:
|
|
299
|
+
self.payloads = PayloadStore(payload_dir, key)
|
|
300
|
+
except OSError as e:
|
|
301
|
+
self.payloads = PayloadStore(None, key) # memory only until the disk answers
|
|
302
|
+
self._on_open_failure(e)
|
|
303
|
+
else:
|
|
304
|
+
if self._path is not None:
|
|
305
|
+
try:
|
|
306
|
+
self._probe_writable()
|
|
307
|
+
except OSError as e:
|
|
308
|
+
self._on_open_failure(e) # the files stay readable
|
|
309
|
+
if self._path and self._path.exists():
|
|
310
|
+
self._replay()
|
|
311
|
+
self._collect_orphans()
|
|
312
|
+
|
|
313
|
+
# ---- scope configuration -----------------------------------------------------
|
|
314
|
+
def scope_config_hash(self) -> str:
|
|
315
|
+
"""Whatever changes the journal's behaviour on disk is in the hash (Decision 9)."""
|
|
316
|
+
return _hash_obj({"policy": self.policy, "sealed": self._key is not None, "plaintext": self.plaintext,
|
|
317
|
+
"disk_free_floor_bytes": self.disk_free_floor_bytes,
|
|
318
|
+
"spill_max_events": self.spill_max_events, "cue_dim": self.cue_dim,
|
|
319
|
+
"on_disk": self._path is not None})
|
|
320
|
+
|
|
321
|
+
# ---- keyed index: an exact bucket over a quantised cue (sub-linear reads) ----
|
|
322
|
+
@staticmethod
|
|
323
|
+
def _cue_key(cue) -> str:
|
|
324
|
+
c = np.asarray(cue, dtype=np.float32)
|
|
325
|
+
# coarse quantisation of the cue -> a bucket key; exact, cheap, sub-linear.
|
|
326
|
+
# Slice C replaces this with ANN over cues; the contract (never scan) holds.
|
|
327
|
+
return _hash_obj(np.round(c[:8], 1).tolist())
|
|
328
|
+
|
|
329
|
+
# ---- write ----------------------------------------------------------------
|
|
330
|
+
def write(self, cue, payload: bytes, salience: float, schema_id: str,
|
|
331
|
+
now: float | None = None, ca3: dict | None = None) -> Entry:
|
|
332
|
+
"""Durable first: payload file, then the log line, then memory. `ca3` (the
|
|
333
|
+
write path's separation meta) rides on the write event so CA3 is rebuilt
|
|
334
|
+
from one line, never from two that could be split by a failure."""
|
|
335
|
+
cue = np.asarray(cue, dtype=np.float32)
|
|
336
|
+
assert cue.shape == (self.cue_dim,), f"cue must be ({self.cue_dim},)"
|
|
337
|
+
now = time.time() if now is None else now
|
|
338
|
+
self._last_now = now
|
|
339
|
+
if self.mode == MODE_READ_ONLY:
|
|
340
|
+
self._retry_or_refuse() # the disk may be back
|
|
341
|
+
pointer = self._put_payload(payload) # content stored once, by hash
|
|
342
|
+
entry = Entry(cue=[round(float(x), 4) for x in cue], pointer=pointer,
|
|
343
|
+
salience=float(salience), schema_id=schema_id,
|
|
344
|
+
t_written=now, t_last_read=now, state=STATE_LIVE)
|
|
345
|
+
entry.entry_id = _hash_obj({"p": pointer, "t": now, "s": schema_id})
|
|
346
|
+
sz = entry.size_bytes()
|
|
347
|
+
if sz > ENTRY_MAX_BYTES:
|
|
348
|
+
raise ValueError(f"entry violates the size law: {sz} B > {ENTRY_MAX_BYTES} B")
|
|
349
|
+
ev = {"ev": "write", "entry": asdict(entry)}
|
|
350
|
+
if ca3 is not None:
|
|
351
|
+
ev["ca3"] = dict(ca3)
|
|
352
|
+
self._persist(ev) # may raise; memory untouched then
|
|
353
|
+
self._entries[entry.entry_id] = entry
|
|
354
|
+
self._by_cue_key.setdefault(self._cue_key(cue), []).append(entry.entry_id)
|
|
355
|
+
self._refs.setdefault(pointer, set()).add(entry.entry_id)
|
|
356
|
+
self.writes_since_last_phase.append(entry.entry_id)
|
|
357
|
+
if ca3 is not None:
|
|
358
|
+
self.ca3_order.append((entry.entry_id, dict(ca3)))
|
|
359
|
+
return entry
|
|
360
|
+
|
|
361
|
+
def _put_payload(self, payload: bytes) -> str:
|
|
362
|
+
if self.mode == MODE_MEMORY:
|
|
363
|
+
return self.payloads.put(payload, durable=False)
|
|
364
|
+
try:
|
|
365
|
+
self._check_disk_floor()
|
|
366
|
+
return self.payloads.put(payload, durable=True)
|
|
367
|
+
except OSError as e:
|
|
368
|
+
self._on_storage_failure(e) # raises for stop / read_only
|
|
369
|
+
return self.payloads.put(payload, durable=False) # memory policy: pending
|
|
370
|
+
|
|
371
|
+
# ---- read (never a scan) --------------------------------------------------
|
|
372
|
+
def read_by_cue(self, cue, now: float | None = None) -> list[Entry]:
|
|
373
|
+
"""Retrieve entries whose cue falls in the same bucket. Touches only that
|
|
374
|
+
bucket -- never the whole journal. `_reads_touched` counts entries visited
|
|
375
|
+
so the no-scan test can assert sub-linear behaviour."""
|
|
376
|
+
key = self._cue_key(np.asarray(cue, dtype=np.float32))
|
|
377
|
+
ids = self._by_cue_key.get(key, [])
|
|
378
|
+
now = time.time() if now is None else now
|
|
379
|
+
out = []
|
|
380
|
+
for i in ids:
|
|
381
|
+
e = self._entries[i]
|
|
382
|
+
self._reads_touched += 1
|
|
383
|
+
if e.state != STATE_EVICTED:
|
|
384
|
+
e.t_last_read = now
|
|
385
|
+
out.append(e)
|
|
386
|
+
return out
|
|
387
|
+
|
|
388
|
+
def get(self, entry_id: str) -> Entry:
|
|
389
|
+
self._reads_touched += 1
|
|
390
|
+
return self._entries[entry_id]
|
|
391
|
+
|
|
392
|
+
# ---- lifecycle (RES-17 transitions) --------------------------------------
|
|
393
|
+
def transition(self, entry_id: str, new_state: str) -> Entry:
|
|
394
|
+
e = self._entries[entry_id]
|
|
395
|
+
if new_state not in _TRANSITIONS[e.state]:
|
|
396
|
+
raise ValueError(f"illegal transition {e.state} -> {new_state}")
|
|
397
|
+
self._persist({"ev": "transition", "entry_id": entry_id, "to": new_state})
|
|
398
|
+
e.state = new_state
|
|
399
|
+
if new_state == STATE_CONSOLIDATED:
|
|
400
|
+
self.consolidated_order.append(entry_id)
|
|
401
|
+
if new_state == STATE_EVICTED:
|
|
402
|
+
self._release_refs(entry_id)
|
|
403
|
+
for src in self.summary_sources.get(entry_id, []): # cascade: a summary takes its
|
|
404
|
+
if self._entries[src].state != STATE_EVICTED: # sources with it -- a demoted
|
|
405
|
+
self.transition(src, STATE_EVICTED) # source is never left unreachable
|
|
406
|
+
return e
|
|
407
|
+
|
|
408
|
+
# ---- Slice E primitives: each one an event, so the log replays them ------
|
|
409
|
+
def set_salience(self, entry_id: str, value: float, why: str = "credit",
|
|
410
|
+
now: float | None = None) -> Entry:
|
|
411
|
+
"""Salience is lifecycle state: every change is an event (invariant 1).
|
|
412
|
+
`why == "contract"` marks the entry as contracted: scheduled decay never
|
|
413
|
+
touches it."""
|
|
414
|
+
e = self._entries[entry_id]
|
|
415
|
+
value = float(min(1.0, max(0.0, value)))
|
|
416
|
+
self._persist({"ev": "salience", "entry_id": entry_id, "to": value, "why": why})
|
|
417
|
+
e.salience = value
|
|
418
|
+
if why == "contract":
|
|
419
|
+
self.contracted.add(entry_id)
|
|
420
|
+
return e
|
|
421
|
+
|
|
422
|
+
def decay(self, factor: float, states=(STATE_CONSOLIDATED, STATE_DEMOTED)) -> int:
|
|
423
|
+
"""Scheduled decay (ADR-007 D1) as ONE event; contracted entries exempt."""
|
|
424
|
+
self._persist({"ev": "decay", "factor": float(factor), "states": list(states)})
|
|
425
|
+
return self._apply_decay(factor, tuple(states))
|
|
426
|
+
|
|
427
|
+
def _apply_decay(self, factor: float, states) -> int:
|
|
428
|
+
n = 0
|
|
429
|
+
for eid, e in self._entries.items():
|
|
430
|
+
if e.state in states and eid not in self.contracted:
|
|
431
|
+
e.salience = round(e.salience * factor, 6); n += 1
|
|
432
|
+
return n
|
|
433
|
+
|
|
434
|
+
def demote(self, summary_id: str, source_ids: list[str], level: int = 1) -> None:
|
|
435
|
+
"""Record the link summary <- sources (ADR-007 D4: keeping pointers). The
|
|
436
|
+
summary holds a reference to every source payload, so those bytes live
|
|
437
|
+
as long as the summary does. The state transitions of the sources are
|
|
438
|
+
separate `transition` events (invariant 1)."""
|
|
439
|
+
if summary_id not in self._entries:
|
|
440
|
+
raise KeyError(summary_id)
|
|
441
|
+
for sid in source_ids:
|
|
442
|
+
if sid in self.summary_of:
|
|
443
|
+
raise ValueError(f"{sid} already belongs to summary {self.summary_of[sid]}")
|
|
444
|
+
self._persist({"ev": "demote", "summary": summary_id, "sources": list(source_ids),
|
|
445
|
+
"level": int(level)})
|
|
446
|
+
self._link_summary(summary_id, list(source_ids), level)
|
|
447
|
+
|
|
448
|
+
def _link_summary(self, summary_id: str, source_ids: list[str], level: int) -> None:
|
|
449
|
+
self.summary_sources[summary_id] = source_ids
|
|
450
|
+
self.summary_level[summary_id] = level
|
|
451
|
+
for sid in source_ids:
|
|
452
|
+
self.summary_of[sid] = summary_id
|
|
453
|
+
self._refs.setdefault(self._entries[sid].pointer, set()).add(summary_id)
|
|
454
|
+
|
|
455
|
+
def _release_refs(self, entry_id: str) -> None:
|
|
456
|
+
"""Reference counting on identity (the pointer), never on similarity.
|
|
457
|
+
A payload is released when no non-evicted entry (or summary) points to it."""
|
|
458
|
+
e = self._entries[entry_id]
|
|
459
|
+
ptrs = [e.pointer] + [self._entries[s].pointer for s in self.summary_sources.get(entry_id, [])]
|
|
460
|
+
for p in ptrs:
|
|
461
|
+
holders = self._refs.get(p)
|
|
462
|
+
if holders is None:
|
|
463
|
+
continue
|
|
464
|
+
holders.discard(entry_id)
|
|
465
|
+
if not holders:
|
|
466
|
+
del self._refs[p]
|
|
467
|
+
self.payloads.release(p)
|
|
468
|
+
|
|
469
|
+
def phase_event(self, record: dict) -> None:
|
|
470
|
+
"""Telemetry snapshot of a lifecycle phase, appended to the log. Replay only
|
|
471
|
+
counts it (phases, refusals, writes since the last phase): it describes
|
|
472
|
+
state, it does not change it."""
|
|
473
|
+
self._persist({"ev": "phase", **record})
|
|
474
|
+
self._count_phase(record)
|
|
475
|
+
|
|
476
|
+
def _count_phase(self, record: dict) -> None:
|
|
477
|
+
self.phase_count += 1
|
|
478
|
+
if record.get("refused"):
|
|
479
|
+
self.refused_count += 1
|
|
480
|
+
self.writes_since_last_phase = []
|
|
481
|
+
|
|
482
|
+
def mark_ca3(self, entry_id: str, meta: dict) -> None:
|
|
483
|
+
"""Kept for logs written before 2026-09-12; the write path now puts the
|
|
484
|
+
meta on the write event itself (one line, never split by a failure)."""
|
|
485
|
+
self._persist({"ev": "ca3", "entry_id": entry_id, **meta})
|
|
486
|
+
self.ca3_order.append((entry_id, dict(meta)))
|
|
487
|
+
|
|
488
|
+
def mark_sentinel(self, index: int, entry_id: str) -> None:
|
|
489
|
+
self._persist({"ev": "sentinel", "i": int(index), "entry_id": entry_id})
|
|
490
|
+
self.sentinels_recorded.append((int(index), entry_id))
|
|
491
|
+
|
|
492
|
+
def references(self, pointer: str) -> set[str]:
|
|
493
|
+
return set(self._refs.get(pointer, ()))
|
|
494
|
+
|
|
495
|
+
def regulated_bytes(self) -> int:
|
|
496
|
+
"""The bytes the budget regulates: non-evicted entries + the payloads alive.
|
|
497
|
+
Evicted entries are tombstones in memory; the append-only log is
|
|
498
|
+
provenance and grows by design (its retention is a separate decision)."""
|
|
499
|
+
return (sum(e.size_bytes() for e in self._entries.values() if e.state != STATE_EVICTED)
|
|
500
|
+
+ self.payloads.total_bytes())
|
|
501
|
+
|
|
502
|
+
# ---- introspection (RES-17 snapshot) -------------------------------------
|
|
503
|
+
def snapshot(self) -> dict:
|
|
504
|
+
counts = {s: 0 for s in _TRANSITIONS}
|
|
505
|
+
for e in self._entries.values():
|
|
506
|
+
counts[e.state] += 1
|
|
507
|
+
return {"entries": len(self._entries), "payloads": len(self.payloads),
|
|
508
|
+
"payload_bytes": self.payloads.total_bytes(),
|
|
509
|
+
"journal_bytes": sum(e.size_bytes() for e in self._entries.values()),
|
|
510
|
+
"regulated_bytes": self.regulated_bytes(),
|
|
511
|
+
"summaries": len(self.summary_sources), "contracted": len(self.contracted),
|
|
512
|
+
"storage_mode": self.mode, "unpersisted_events": len(self.spill),
|
|
513
|
+
**counts}
|
|
514
|
+
|
|
515
|
+
def __len__(self):
|
|
516
|
+
return len(self._entries)
|
|
517
|
+
|
|
518
|
+
# ---- storage: durable first, then the declared policy (Decision 9) ----------
|
|
519
|
+
def disk_free(self) -> int | None:
|
|
520
|
+
if self._path is None:
|
|
521
|
+
return None
|
|
522
|
+
try:
|
|
523
|
+
return int(shutil.disk_usage(self._path.parent).free)
|
|
524
|
+
except OSError:
|
|
525
|
+
return None
|
|
526
|
+
|
|
527
|
+
def _check_disk_floor(self) -> None:
|
|
528
|
+
"""The proactive wall: under the declared floor, act as if the disk were full."""
|
|
529
|
+
if self.disk_free_floor_bytes is None or self._path is None:
|
|
530
|
+
return
|
|
531
|
+
self._persist_count += 1
|
|
532
|
+
if self._persist_count % self._disk_check_every != 1 and self._disk_check_every > 1:
|
|
533
|
+
return
|
|
534
|
+
free = self.disk_free()
|
|
535
|
+
if free is not None and free < self.disk_free_floor_bytes:
|
|
536
|
+
raise OSError(errno.ENOSPC, f"free space {free} B under the declared floor {self.disk_free_floor_bytes} B")
|
|
537
|
+
|
|
538
|
+
def _probe_writable(self) -> None:
|
|
539
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
540
|
+
with open(self._path, "a", encoding="utf-8"):
|
|
541
|
+
pass
|
|
542
|
+
|
|
543
|
+
def _encode(self, ev: dict) -> str:
|
|
544
|
+
line = json.dumps(ev, separators=(",", ":"))
|
|
545
|
+
return seal_line(self._key, line) if self._key is not None else line # QJE1, per line, as on the hub
|
|
546
|
+
|
|
547
|
+
def _write_line(self, line: str) -> None:
|
|
548
|
+
with open(self._path, "a", encoding="utf-8") as fh:
|
|
549
|
+
fh.write(line + "\n")
|
|
550
|
+
|
|
551
|
+
def _persist(self, ev: dict) -> None:
|
|
552
|
+
"""The line reaches the disk before memory changes. On failure the policy
|
|
553
|
+
applies: stop raises, read_only refuses, memory spills (bounded)."""
|
|
554
|
+
if self._path is None:
|
|
555
|
+
return
|
|
556
|
+
line = self._encode(ev)
|
|
557
|
+
if self.mode == MODE_MEMORY:
|
|
558
|
+
try:
|
|
559
|
+
self._flush_spill() # the disk may be back
|
|
560
|
+
except OSError:
|
|
561
|
+
self._spill(line)
|
|
562
|
+
return
|
|
563
|
+
try:
|
|
564
|
+
self._check_disk_floor()
|
|
565
|
+
self._write_line(line)
|
|
566
|
+
except OSError as e:
|
|
567
|
+
self._on_storage_failure(e) # raises for stop / read_only
|
|
568
|
+
self._spill(line) # memory policy
|
|
569
|
+
return
|
|
570
|
+
if self.mode == MODE_READ_ONLY:
|
|
571
|
+
self._restored(MODE_READ_ONLY)
|
|
572
|
+
|
|
573
|
+
def _on_storage_failure(self, e: OSError) -> None:
|
|
574
|
+
reason = f"{e.strerror or e}"
|
|
575
|
+
if self.policy == POLICY_STOP:
|
|
576
|
+
raise StorageExhausted(e.errno or errno.EIO, f"storage failed, policy stop: {reason}") from e
|
|
577
|
+
if self.gap_since is None:
|
|
578
|
+
self.gap_since = self._last_now
|
|
579
|
+
self.degraded_reason = reason
|
|
580
|
+
if self.policy == POLICY_READ_ONLY:
|
|
581
|
+
self.mode = MODE_READ_ONLY
|
|
582
|
+
self.refused_writes += 1
|
|
583
|
+
raise JournalReadOnly(e.errno or errno.EIO, f"journal read only ({reason}); this write is refused") from e
|
|
584
|
+
self.mode = MODE_MEMORY # policy memory: the caller spills
|
|
585
|
+
|
|
586
|
+
def _spill(self, line: str) -> None:
|
|
587
|
+
if len(self.spill) >= self.spill_max_events:
|
|
588
|
+
self.mode = MODE_READ_ONLY
|
|
589
|
+
self.refused_writes += 1
|
|
590
|
+
raise JournalReadOnly(errno.ENOSPC, f"spill buffer full ({self.spill_max_events} events); this write is refused")
|
|
591
|
+
self.spill.append(line)
|
|
592
|
+
|
|
593
|
+
def _retry_or_refuse(self) -> None:
|
|
594
|
+
"""read_only mode, a new write: probe the disk once; if it answers, resume."""
|
|
595
|
+
try:
|
|
596
|
+
self._check_disk_floor()
|
|
597
|
+
self._probe_writable()
|
|
598
|
+
except OSError as e:
|
|
599
|
+
self.refused_writes += 1
|
|
600
|
+
raise JournalReadOnly(e.errno or errno.EIO, f"journal read only ({self.degraded_reason}); this write is refused") from e
|
|
601
|
+
|
|
602
|
+
def _flush_spill(self) -> None:
|
|
603
|
+
"""Memory mode recovery: pending payloads, then the spilled lines in order, then
|
|
604
|
+
the restoration record. Raises on the first failure, nothing lost."""
|
|
605
|
+
self._check_disk_floor()
|
|
606
|
+
self.payloads.flush_pending()
|
|
607
|
+
while self.spill:
|
|
608
|
+
self._write_line(self.spill[0]); self.spill.popleft() # O(1); a list.pop(0) here was O(n) per event
|
|
609
|
+
self._restored(MODE_MEMORY)
|
|
610
|
+
|
|
611
|
+
def _restored(self, from_mode: str) -> None:
|
|
612
|
+
rec = {"ev": "storage_restored", "from_mode": from_mode, "reason": self.degraded_reason,
|
|
613
|
+
"since": self.gap_since, "until": self._last_now,
|
|
614
|
+
"refused_writes": self.refused_writes if from_mode == MODE_READ_ONLY else 0}
|
|
615
|
+
self.mode = MODE_DURABLE
|
|
616
|
+
self.degraded_reason, self.gap_since, self.refused_writes = None, None, 0
|
|
617
|
+
try:
|
|
618
|
+
self._write_line(self._encode(rec))
|
|
619
|
+
except OSError:
|
|
620
|
+
pass # the disk went again; the next write will see it
|
|
621
|
+
self.restorations.append(rec)
|
|
622
|
+
|
|
623
|
+
def _on_open_failure(self, e: OSError) -> None:
|
|
624
|
+
reason = f"{e.strerror or e}"
|
|
625
|
+
if self.policy == POLICY_STOP:
|
|
626
|
+
raise StorageExhausted(e.errno or errno.EACCES, f"cannot open the journal on disk, policy stop: {reason}") from e
|
|
627
|
+
self.degraded_reason = reason
|
|
628
|
+
self.gap_since = None
|
|
629
|
+
if self.policy == POLICY_READ_ONLY:
|
|
630
|
+
if not (self._path.exists() and os_readable(self._path)):
|
|
631
|
+
raise JournalReadOnly(e.errno or errno.EACCES, f"cannot open the journal and nothing to read: {reason}") from e
|
|
632
|
+
self.mode = MODE_READ_ONLY
|
|
633
|
+
else:
|
|
634
|
+
self.mode = MODE_MEMORY
|
|
635
|
+
|
|
636
|
+
def _collect_orphans(self) -> None:
|
|
637
|
+
"""A payload file no entry references (a write interrupted between the file
|
|
638
|
+
and the line) is garbage, released at open."""
|
|
639
|
+
for p in [p for p in list(self.payloads._sizes) if p not in self._refs]:
|
|
640
|
+
self.payloads.release(p)
|
|
641
|
+
|
|
642
|
+
# ---- append-only persistence ---------------------------------------------
|
|
643
|
+
def _replay(self):
|
|
644
|
+
"""Rebuild in-memory state from the append-only log: entries, states,
|
|
645
|
+
saliences, summary links, payload references, the CA3 and sentinel
|
|
646
|
+
events, the phase counters. Payload BYTES are not in the log (the size
|
|
647
|
+
law keeps content out of it): they live in the payload directory next
|
|
648
|
+
to it, opened by `PayloadStore` (Decision 8). Before 2026-09-12 this
|
|
649
|
+
docstring claimed the payloads were re-registered from the log; they
|
|
650
|
+
were not -- recorded here so the correction stays visible."""
|
|
651
|
+
for raw in self._path.read_text(encoding="utf-8").splitlines():
|
|
652
|
+
if not raw.strip():
|
|
653
|
+
continue
|
|
654
|
+
ev = json.loads(open_line(self._key, raw)) # a clear line passes, a sealed one needs the key
|
|
655
|
+
if ev["ev"] == "write":
|
|
656
|
+
d = ev["entry"]
|
|
657
|
+
e = Entry(**d)
|
|
658
|
+
self._entries[e.entry_id] = e
|
|
659
|
+
self._by_cue_key.setdefault(self._cue_key(e.cue), []).append(e.entry_id)
|
|
660
|
+
self._refs.setdefault(e.pointer, set()).add(e.entry_id)
|
|
661
|
+
self.writes_since_last_phase.append(e.entry_id)
|
|
662
|
+
if "ca3" in ev:
|
|
663
|
+
self.ca3_order.append((e.entry_id, dict(ev["ca3"])))
|
|
664
|
+
elif ev["ev"] == "transition":
|
|
665
|
+
self._entries[ev["entry_id"]].state = ev["to"]
|
|
666
|
+
if ev["to"] == STATE_CONSOLIDATED:
|
|
667
|
+
self.consolidated_order.append(ev["entry_id"])
|
|
668
|
+
if ev["to"] == STATE_EVICTED:
|
|
669
|
+
self._release_refs(ev["entry_id"])
|
|
670
|
+
elif ev["ev"] == "salience":
|
|
671
|
+
self._entries[ev["entry_id"]].salience = float(ev["to"])
|
|
672
|
+
if ev.get("why") == "contract":
|
|
673
|
+
self.contracted.add(ev["entry_id"])
|
|
674
|
+
elif ev["ev"] == "decay":
|
|
675
|
+
self._apply_decay(float(ev["factor"]), tuple(ev["states"]))
|
|
676
|
+
elif ev["ev"] == "demote":
|
|
677
|
+
self._link_summary(ev["summary"], list(ev["sources"]), int(ev.get("level", 1)))
|
|
678
|
+
elif ev["ev"] == "phase":
|
|
679
|
+
self._count_phase(ev) # telemetry: counted, not applied
|
|
680
|
+
elif ev["ev"] == "ca3":
|
|
681
|
+
self.ca3_order.append((ev["entry_id"], {k: v for k, v in ev.items() if k not in ("ev", "entry_id")}))
|
|
682
|
+
elif ev["ev"] == "sentinel":
|
|
683
|
+
self.sentinels_recorded.append((int(ev["i"]), ev["entry_id"]))
|
|
684
|
+
elif ev["ev"] == "storage_restored":
|
|
685
|
+
self.restorations.append({k: v for k, v in ev.items() if k != "ev"})
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def os_readable(path: Path) -> bool:
|
|
689
|
+
try:
|
|
690
|
+
with open(path, "rb"):
|
|
691
|
+
return True
|
|
692
|
+
except OSError:
|
|
693
|
+
return False
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def lifecycle_declaration(journal: "Journal | None" = None) -> dict:
|
|
697
|
+
"""Decision 8: persistence is a property per component; each declares its own.
|
|
698
|
+
With a journal, the declaration reflects that journal's actual setup."""
|
|
699
|
+
has = journal is not None # never `if journal`: an empty journal is falsy
|
|
700
|
+
on_disk = has and journal._path is not None
|
|
701
|
+
sealed = has and journal._key is not None
|
|
702
|
+
durable = on_disk and journal.mode != MODE_MEMORY # a memory-mode journal never claims (invariant 1)
|
|
703
|
+
storage = {"mode": journal.mode if has else None, "policy": journal.policy if has else None,
|
|
704
|
+
"degraded_reason": journal.degraded_reason if has else None,
|
|
705
|
+
"unpersisted_events": len(journal.spill) if has else 0,
|
|
706
|
+
"plaintext_scope": has and journal.plaintext}
|
|
707
|
+
return {
|
|
708
|
+
"storage": storage,
|
|
709
|
+
"structure": {"what": "entries, states, saliences, links, references, phases",
|
|
710
|
+
"lifecycle": "append-only log, replayed at open",
|
|
711
|
+
"survives_restart": durable, "encrypted_at_rest": sealed},
|
|
712
|
+
"bytes": {"what": "the payloads (content), one file per pointer",
|
|
713
|
+
"lifecycle": "content-addressed directory next to the log, released at the last reference",
|
|
714
|
+
"survives_restart": has and journal.payloads.persistent and durable,
|
|
715
|
+
"encrypted_at_rest": has and journal.payloads.sealed},
|
|
716
|
+
"index": {"what": "the cue index (buckets) and the demotion aliases",
|
|
717
|
+
"lifecycle": "recomputed at open from the log, with a declared seed",
|
|
718
|
+
"survives_restart": durable, "encrypted_at_rest": None},
|
|
719
|
+
"associative_memory": {"what": "CA3 codes (write path) and the persistent memory (lifecycle)",
|
|
720
|
+
"lifecycle": "rebuilt at open from their own events (ca3, consolidation order); "
|
|
721
|
+
"checkpointed with the weights once the C2 layer takes over -- the "
|
|
722
|
+
"language model arm re declares this component (ADR-007 D8, named boundary)",
|
|
723
|
+
"survives_restart": durable, "encrypted_at_rest": None},
|
|
724
|
+
}
|