From fa47b98d18a67acf7346bc774c13fb5ddda48290 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Tue, 11 Aug 2026 22:26:05 -0500 Subject: [PATCH] engram tiered storage M1: on-disk paged store format + round-trip tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-contained paged store (lang/runtime/engram_store.{c,h}): 16KiB slotted pages, u32 TLV self-describing records (forward-compatible), overflow chains, B+-tree id-index + from/to adjacency, page free-list, tombstones, double superblock + crc recovery. Not yet wired to activation (M3). 33/33 tests pass (ASan/UBSan clean); 5k nodes/20k edges round-trip bit-exact incl 768xf32 emb + hebb; store 25MB vs 64MB JSON. Format is final — see design §2.4. --- engram/test/run_store_tests.sh | 13 + engram/test/test_store.c | 439 +++++++++++++ lang/runtime/engram_store.c | 1084 ++++++++++++++++++++++++++++++++ lang/runtime/engram_store.h | 135 ++++ 4 files changed, 1671 insertions(+) create mode 100755 engram/test/run_store_tests.sh create mode 100644 engram/test/test_store.c create mode 100644 lang/runtime/engram_store.c create mode 100644 lang/runtime/engram_store.h diff --git a/engram/test/run_store_tests.sh b/engram/test/run_store_tests.sh new file mode 100755 index 0000000..52af9bd --- /dev/null +++ b/engram/test/run_store_tests.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# M1 paged-store gate. Pure C (NOT elb/elc). Writes only under /tmp. +set -e +HERE="$(cd "$(dirname "$0")" && pwd)" +SRC="$HERE/../../lang/runtime/engram_store.c" +BIN="/tmp/test_store.$$" +echo "compiling: gcc test_store.c engram_store.c" +gcc -O2 -Wall -Wextra -std=c11 "$HERE/test_store.c" "$SRC" -o "$BIN" +"$BIN" +rc=$? +rm -f "$BIN" +rm -rf /tmp/engram-store-test-* +exit $rc diff --git a/engram/test/test_store.c b/engram/test/test_store.c new file mode 100644 index 0000000..0aab6c2 --- /dev/null +++ b/engram/test/test_store.c @@ -0,0 +1,439 @@ +/* test_store.c — M1 gate for the engram paged store (engram_store.{c,h}). + * + * Pure C. Build: gcc -O2 test_store.c ../../lang/runtime/engram_store.c -o test_store + * Writes ONLY under a throwaway /tmp dir. Never touches ~/.neuron or live ports. + * + * Covers §7 M1 gates: round-trip (5k nodes / 20k edges, all fields, emb bit-exact, + * hebb, >page content), TLV forward-compat, overflow chains, B+-tree indexes + * across splits, free-list reuse, and corruption/superblock recovery. + */ +#include "../../lang/runtime/engram_store.h" + +#include +#include +#include +#include +#include +#include +#include + +static int g_pass = 0, g_fail = 0; +static void ok(const char* name, int cond){ + printf(" [%s] %s\n", cond ? "PASS" : "FAIL", name); + if (cond) g_pass++; else g_fail++; +} + +static char g_dir[512]; +static void mk_dir(void){ + snprintf(g_dir, sizeof g_dir, "/tmp/engram-store-test-%d", (int)getpid()); + mkdir(g_dir, 0700); +} +static void path_in(char* out, size_t cap, const char* name){ + snprintf(out, cap, "%s/%s", g_dir, name); +} +static long file_size(const char* p){ struct stat st; return stat(p,&st)==0 ? (long)st.st_size : -1; } + +/* ── deterministic RNG so oracle nodes/edges regenerate bit-exact ─────────── */ +static uint64_t xs(uint64_t* s){ uint64_t x=*s; x^=x<<13; x^=x>>7; x^=x<<17; *s=x; return x; } +static uint64_t node_seed(int i){ return 0x9E3779B97F4A7C15ULL ^ ((uint64_t)(i+1)*0xD1B54A32D192ED03ULL); } +static uint64_t edge_seed(int i){ return 0xC2B2AE3D27D4EB4FULL ^ ((uint64_t)(i+1)*0x165667B19E3779F9ULL); } + +static char* rnd_str(uint64_t* st, size_t len){ + char* s = (char*)malloc(len + 1); + for (size_t i=0;ipage content to force overflow chains. */ +#define NODE_COUNT 5000 +#define EDGE_COUNT 20000 +#define EMB_DIM 768 + +static void gen_node(int i, StoreNode* n){ + memset(n, 0, sizeof *n); + uint64_t st = node_seed(i); + char id[32]; snprintf(id, sizeof id, "node-%d", i); + n->id = strdup(id); + size_t clen = (i % 500 == 0) ? (size_t)(17000 + (xs(&st) % 6000)) : (size_t)(xs(&st) % 300); + n->content = rnd_str(&st, clen); + n->node_type = rnd_str(&st, 4 + (xs(&st) % 8)); + n->label = (i % 2) ? rnd_str(&st, 3 + (xs(&st) % 10)) : NULL; + n->tier = rnd_str(&st, 4 + (xs(&st) % 6)); + n->tags = rnd_str(&st, xs(&st) % 40); + n->metadata = (i % 3) ? rnd_str(&st, xs(&st) % 60) : NULL; + n->salience = (double)(xs(&st) % 1000000) / 997.0; + n->importance = (double)(xs(&st) % 1000000) / 131.0; + n->confidence = (double)(xs(&st) % 1000000) / 733.0; + n->temporal_decay_rate = (double)(xs(&st) % 1000000) / 101.0; + n->activation_count = (int64_t)(xs(&st) % 100000); + n->last_activated = (int64_t)xs(&st); + n->created_at = (int64_t)(1600000000000LL + i); + n->updated_at = (int64_t)xs(&st); + n->background_activation = (double)(xs(&st) % 1000000) / 17.0; + n->working_memory_weight = (double)(xs(&st) % 1000000) / 29.0; + n->suppression_count = (int32_t)(xs(&st) % 50); + n->layer_id = (uint32_t)(xs(&st) % 5); + for (int k=0;kaccess_ts[k] = (int64_t)xs(&st); + n->access_head = (int32_t)(xs(&st) % STORE_BLL_K); + n->access_filled = (int32_t)(xs(&st) % (STORE_BLL_K + 1)); + n->wm_anchor = (double)(xs(&st) % 1000000) / 3.0; + n->emb = (float*)malloc(EMB_DIM * sizeof(float)); + for (int k=0;kemb[k], &u, 4); } + n->emb_dim = EMB_DIM; +} + +static void gen_edge(int i, StoreEdge* e){ + memset(e, 0, sizeof *e); + uint64_t st = edge_seed(i); + char id[32], from[32], to[32]; + snprintf(id, sizeof id, "edge-%d", i); + snprintf(from, sizeof from, "node-%d", (int)(xs(&st) % NODE_COUNT)); + snprintf(to, sizeof to, "node-%d", (int)(xs(&st) % NODE_COUNT)); + e->id = strdup(id); e->from_id = strdup(from); e->to_id = strdup(to); + e->relation = rnd_str(&st, 3 + (xs(&st) % 12)); + e->metadata = (i % 4) ? rnd_str(&st, xs(&st) % 40) : NULL; + e->weight = (double)(xs(&st) % 1000000) / 111.0; + e->hebb = (double)(xs(&st) % 1000000) / 1000000.0; /* the learned field */ + e->confidence = (double)(xs(&st) % 1000000) / 777.0; + e->created_at = (int64_t)(1600000000000LL + i); + e->updated_at = (int64_t)xs(&st); + e->last_fired = (int64_t)xs(&st); + e->inhibitory = (int32_t)(xs(&st) % 2); + e->layer_id = (uint32_t)(xs(&st) % 5); +} + +static int streq(const char* a, const char* b){ + if (!a && !b) return 1; + if (!a || !b) return 0; + return strcmp(a,b)==0; +} +static int cmp_node(const StoreNode* a, const StoreNode* b){ + if (!streq(a->id,b->id) || !streq(a->content,b->content) || + !streq(a->node_type,b->node_type) || !streq(a->label,b->label) || + !streq(a->tier,b->tier) || !streq(a->tags,b->tags) || + !streq(a->metadata,b->metadata)) return 0; + if (a->salience!=b->salience || a->importance!=b->importance || + a->confidence!=b->confidence || a->temporal_decay_rate!=b->temporal_decay_rate || + a->activation_count!=b->activation_count || a->last_activated!=b->last_activated || + a->created_at!=b->created_at || a->updated_at!=b->updated_at || + a->background_activation!=b->background_activation || + a->working_memory_weight!=b->working_memory_weight || + a->suppression_count!=b->suppression_count || a->layer_id!=b->layer_id || + a->access_head!=b->access_head || a->access_filled!=b->access_filled || + a->wm_anchor!=b->wm_anchor || a->emb_dim!=b->emb_dim) return 0; + for (int k=0;kaccess_ts[k]!=b->access_ts[k]) return 0; + if ((a->emb==NULL) != (b->emb==NULL)) return 0; + if (a->emb && memcmp(a->emb, b->emb, (size_t)a->emb_dim*4)!=0) return 0; + return 1; +} +static int cmp_edge(const StoreEdge* a, const StoreEdge* b){ + if (!streq(a->id,b->id) || !streq(a->from_id,b->from_id) || !streq(a->to_id,b->to_id) || + !streq(a->relation,b->relation) || !streq(a->metadata,b->metadata)) return 0; + if (a->weight!=b->weight || a->hebb!=b->hebb || a->confidence!=b->confidence || + a->created_at!=b->created_at || a->updated_at!=b->updated_at || + a->last_fired!=b->last_fired || a->inhibitory!=b->inhibitory || + a->layer_id!=b->layer_id) return 0; + return 1; +} +static void free_node_fields(StoreNode* n){ + free(n->id); free(n->content); free(n->node_type); free(n->label); + free(n->tier); free(n->tags); free(n->metadata); free(n->emb); free(n->unknown); +} +static void free_edge_fields(StoreEdge* e){ + free(e->id); free(e->from_id); free(e->to_id); free(e->relation); free(e->metadata); free(e->unknown); +} + +/* Flip one byte in the store file at (page*PAGE_SIZE + off). */ +static void flip_byte(const char* path, uint64_t page, size_t off){ + int fd = open(path, O_RDWR); + uint8_t b; off_t at = (off_t)page*STORE_PAGE_SIZE + off; + pread(fd, &b, 1, at); b ^= 0xFF; pwrite(fd, &b, 1, at); close(fd); +} + +/* ════════════════════════════════════════════════════════════════════════ */ + +static void test_roundtrip(void){ + printf("\n== round-trip: %d nodes + %d edges, all fields, emb bit-exact ==\n", NODE_COUNT, EDGE_COUNT); + char path[600]; path_in(path, sizeof path, "roundtrip.store"); + unlink(path); + EngramPagedStore* s = store_create(path); + ok("store_create", s != NULL); + if (!s) return; + + for (int i=0;i 0); + + for (int i=0;i= 1); + printf(" store_check reported %d corrupt page(s)\n", bad); + store_close(s); + + /* fresh store, corrupt superblock 0, must recover via mirror superblock 1 */ + char p2[600]; path_in(p2, sizeof p2, "sbrec.store"); + unlink(p2); + s = store_create(p2); + StoreNode n; gen_node(42,&n); store_put_node(s,&n); + store_close(s); + /* trash magic + crc region of page 0 */ + flip_byte(p2, 0, 0); flip_byte(p2, 0, 1); flip_byte(p2, 0, 90); + s = store_open(p2); + ok("open recovers via mirror superblock (page 1)", s != NULL); + if (s){ + StoreNode g; int r = store_get_node(s, "node-42", &g); + ok("data intact after superblock recovery", r==1 && cmp_node(&n,&g)); + if (r==1) store_node_free(&g); + store_close(s); + } + free_node_fields(&n); +} + +int main(void){ + mk_dir(); + printf("engram_store M1 test harness — dir=%s\n", g_dir); + test_roundtrip(); + test_forward_compat(); + test_overflow(); + test_index_splits(); + test_freelist(); + test_corruption(); + printf("\n================ %d passed, %d failed ================\n", g_pass, g_fail); + return g_fail ? 1 : 0; +} diff --git a/lang/runtime/engram_store.c b/lang/runtime/engram_store.c new file mode 100644 index 0000000..8ee843a --- /dev/null +++ b/lang/runtime/engram_store.c @@ -0,0 +1,1084 @@ +/* engram_store.c — M1 paged store: writer/reader for the FINAL on-disk format. + * + * Self-contained. Standard libs only. See engram_store.h and the design doc + * (engram-tiered-storage-engine.md §2/§4/§7). Write-through page I/O via + * pread/pwrite (no buffer pool yet — that is M4). Every mutation immediately + * lands on disk; store_sync/close rewrite both superblocks and fsync. + * + * On-disk layout (all integers little-endian): + * page 0,1 : superblock + mirror (recovery picks higher valid sb_seq) + * page N : generic 32-byte page header, then type-specific body + * + * Generic page header (32 bytes): + * +0 u64 page_id + * +8 u8 page_type (NODE|EDGE|INDEX|OVERFLOW|FREE) + * +9 u8 reserved + * +10 u16 slot_count (slotted pages) / nkeys (index pages) + * +12 u16 free_bytes (slotted pages: contiguous heap gap) + * +14 u16 reserved + * +16 u64 lsn (LSN of last write — ARIES redo idempotency, M2) + * +24 u32 crc32 (over the whole page with these 4 bytes zeroed) + * +28 u32 reserved + * + * Slotted page (NODE/EDGE): slot directory grows from +32 (6 bytes each: + * [u16 off][u16 len][u16 flags]); records grow down from PAGE_SIZE. + * + * On-page record: [u16 rec_len][u8 rec_ver][u8 rec_flags] then either the TLV + * field body (inline) or, if REC_OVERFLOW, a stub [u64 head_page][u64 total]. + * + * TLV field: [u8 tag][u32 len][bytes]. NOTE: the design §2 sketch says u16 len; + * node content can exceed 64 KiB (tests use 100 KiB), so the field length MUST + * be u32. This is the one format decision beyond §2 — flagged in the report. + * + * Overflow page: header, then +32 [u64 next_page][u32 chunk_len][payload...]. + * + * Index page (B+-tree, u64 keys, duplicates allowed, fixed payload per tree): + * +32 u8 is_leaf; +36 u64 next_leaf; +44 entries. + * leaf entry = [u64 key][payload]; payload = primary(10) or adjacency(11). + * internal = nkeys u64 keys, then (nkeys+1) u64 child page ids. + */ +#include "engram_store.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* STORE_BLL_K must track ENGRAM_BLL_K in el_runtime.c. */ +typedef char store__bll_k_check[(STORE_BLL_K == 10) ? 1 : -1]; + +#define STORE_HDR 32u /* generic page header size */ +#define SLOT_SIZE 6u /* [u16 off][u16 len][u16 flags] */ +#define SLOT_LIVE 1u +#define SLOT_DEAD 2u + +#define REC_HDR 4u /* [u16 rec_len][u8 ver][u8 flags] */ +#define REC_VER 1u +#define REC_OVERFLOW 1u /* rec_flags bit0: body is in an overflow chain */ + +#define MAX_INLINE_BODY (STORE_PAGE_SIZE - STORE_HDR - SLOT_SIZE - REC_HDR) + +/* Index page geometry. */ +#define IDX_LEAF_OFF 32u +#define IDX_NEXT_OFF 36u +#define IDX_ENT_OFF 44u +#define IDX_BODY (STORE_PAGE_SIZE - IDX_ENT_OFF) + +/* Overflow page geometry. */ +#define OVF_NEXT_OFF 32u +#define OVF_LEN_OFF 40u +#define OVF_DATA_OFF 44u +#define OVF_CHUNK (STORE_PAGE_SIZE - OVF_DATA_OFF) + +/* Superblock layout (page 0 and mirror page 1). */ +#define SB_MAGIC_OFF 0u +#define SB_FMT_OFF 8u +#define SB_PGSZ_OFF 12u +#define SB_PGCNT_OFF 16u +#define SB_FREE_OFF 24u +#define SB_ROOT_OFF 32u +#define SB_ADJ_OFF 40u +#define SB_LAYER_OFF 48u +#define SB_CKPT_OFF 56u +#define SB_SEQ_OFF 64u +#define SB_UUID_OFF 72u /* 16 bytes */ +#define SB_CRC_OFF 88u + +/* Which index tree. */ +enum { TREE_PRIMARY = 0, TREE_ADJ = 1 }; +#define PRIMARY_PAYLOAD 10u /* [u64 page_id][u16 slot] */ +#define ADJ_PAYLOAD 11u /* [u8 dir][u64 page_id][u16 slot] */ +#define ADJ_DIR_FROM 0u +#define ADJ_DIR_TO 1u +#define MAX_PAYLOAD 16u + +/* Node/Edge TLV tag namespaces (per-kind; page type also disambiguates). */ +enum { + NT_ID = 1, NT_CONTENT, NT_NODE_TYPE, NT_LABEL, NT_TIER, NT_TAGS, NT_METADATA, + NT_SALIENCE, NT_IMPORTANCE, NT_CONFIDENCE, NT_DECAY, + NT_ACT_COUNT, NT_LAST_ACT, NT_CREATED, NT_UPDATED, + NT_BG_ACT, NT_WM_WEIGHT, NT_SUPPRESS, NT_LAYER, + NT_ACCESS_TS, NT_ACCESS_HEAD, NT_ACCESS_FILLED, NT_WM_ANCHOR, + NT_EMB, NT_EMB_DIM +}; +enum { + ET_ID = 1, ET_FROM, ET_TO, ET_RELATION, ET_METADATA, + ET_WEIGHT, ET_HEBB, ET_CONFIDENCE, ET_CREATED, ET_UPDATED, ET_LAST_FIRED, + ET_INHIBITORY, ET_LAYER +}; + +struct EngramPagedStore { + int fd; + char path[1024]; + uint32_t format_version; + uint32_t page_size; + uint64_t page_count; + uint64_t free_list_head; /* page id of first FREE page, 0 = none */ + uint64_t root_index_page; /* primary id->loc B+-tree root */ + uint64_t adj_index_page; /* from/to adjacency B+-tree root */ + uint64_t layer_registry_page; + uint64_t last_checkpoint_lsn; /* 0 in M1 (no checkpoints yet) */ + uint64_t sb_seq; + uint8_t uuid[16]; + uint64_t next_lsn; + uint64_t cur_node_page; /* last NODE page with room, 0 = none */ + uint64_t cur_edge_page; /* last EDGE page with room, 0 = none */ + int leaf_max; /* test hook; 0 = natural */ + int int_max; +}; + +/* ── little-endian scalar codecs ──────────────────────────────────────────── */ +static void put_u16(uint8_t* p, uint16_t v){ p[0]=(uint8_t)v; p[1]=(uint8_t)(v>>8); } +static void put_u32(uint8_t* p, uint32_t v){ for(int i=0;i<4;i++) p[i]=(uint8_t)(v>>(8*i)); } +static void put_u64(uint8_t* p, uint64_t v){ for(int i=0;i<8;i++) p[i]=(uint8_t)(v>>(8*i)); } +static uint16_t get_u16(const uint8_t* p){ return (uint16_t)(p[0] | (p[1]<<8)); } +static uint32_t get_u32(const uint8_t* p){ uint32_t v=0; for(int i=0;i<4;i++) v|=(uint32_t)p[i]<<(8*i); return v; } +static uint64_t get_u64(const uint8_t* p){ uint64_t v=0; for(int i=0;i<8;i++) v|=(uint64_t)p[i]<<(8*i); return v; } +static void put_f64(uint8_t* p, double d){ uint64_t u; memcpy(&u,&d,8); put_u64(p,u); } +static double get_f64(const uint8_t* p){ uint64_t u=get_u64(p); double d; memcpy(&d,&u,8); return d; } + +/* ── crc32 (IEEE 802.3, reflected, poly 0xEDB88320); matches eg_crc32 in + * el_runtime.c so integrity checks are consistent cross-module. + * crc32("") == 0, crc32("123456789") == 0xCBF43926. ─────────────────────── */ +static uint32_t s_crc_table[256]; +static int s_crc_ready = 0; +static void crc_init(void){ + for (uint32_t i=0;i<256;i++){ + uint32_t c=i; + for (int k=0;k<8;k++) c = (c&1) ? (0xEDB88320u ^ (c>>1)) : (c>>1); + s_crc_table[i]=c; + } + s_crc_ready=1; +} +static uint32_t crc32_buf(const void* data, size_t n){ + if (!s_crc_ready) crc_init(); + const uint8_t* p = (const uint8_t*)data; + uint32_t c = 0xFFFFFFFFu; + for (size_t i=0;i> 8); + return c ^ 0xFFFFFFFFu; +} + +/* FNV-1a 64-bit over a NUL-terminated id; the B+-tree key. Exact id is verified + * against the record on read, so hash collisions are correctness-safe. */ +static uint64_t id_hash(const char* s){ + uint64_t h = 1469598103934665603ULL; + for (; *s; ++s){ h ^= (uint8_t)*s; h *= 1099511628211ULL; } + return h; +} + +/* ── raw page I/O ─────────────────────────────────────────────────────────── */ +static int page_read(EngramPagedStore* s, uint64_t id, uint8_t* buf){ + off_t off = (off_t)id * STORE_PAGE_SIZE; + ssize_t r = pread(s->fd, buf, STORE_PAGE_SIZE, off); + if (r != (ssize_t)STORE_PAGE_SIZE) return -1; + return 0; +} +static int page_write_raw(EngramPagedStore* s, uint64_t id, const uint8_t* buf){ + off_t off = (off_t)id * STORE_PAGE_SIZE; + ssize_t w = pwrite(s->fd, buf, STORE_PAGE_SIZE, off); + if (w != (ssize_t)STORE_PAGE_SIZE) return -1; + return 0; +} +/* Stamp lsn + crc into a generic page header, then write. */ +static int page_write(EngramPagedStore* s, uint64_t id, uint8_t* buf){ + put_u64(buf + 0, id); + put_u64(buf + 16, ++s->next_lsn); + put_u32(buf + 24, 0); + uint32_t crc = crc32_buf(buf, STORE_PAGE_SIZE); + put_u32(buf + 24, crc); + return page_write_raw(s, id, buf); +} +static int page_crc_ok(const uint8_t* buf){ + uint8_t tmp[STORE_PAGE_SIZE]; + memcpy(tmp, buf, STORE_PAGE_SIZE); + uint32_t stored = get_u32(tmp + 24); + put_u32(tmp + 24, 0); + return crc32_buf(tmp, STORE_PAGE_SIZE) == stored; +} + +/* Allocate a page: reuse a FREE page if available, else extend the file. */ +static uint64_t page_alloc(EngramPagedStore* s, uint8_t type){ + uint8_t buf[STORE_PAGE_SIZE]; + uint64_t id; + if (s->free_list_head){ + id = s->free_list_head; + if (page_read(s, id, buf) != 0) return 0; + s->free_list_head = get_u64(buf + OVF_NEXT_OFF); /* next_free stashed here */ + } else { + id = s->page_count++; + } + memset(buf, 0, STORE_PAGE_SIZE); + buf[8] = type; + if (type == STORE_PT_NODE || type == STORE_PT_EDGE){ + put_u16(buf + 10, 0); + put_u16(buf + 12, (uint16_t)(STORE_PAGE_SIZE - STORE_HDR)); /* all heap free */ + } + if (page_write(s, id, buf) != 0) return 0; + return id; +} +/* Return a now-empty page to the free list. */ +static int page_free(EngramPagedStore* s, uint64_t id){ + uint8_t buf[STORE_PAGE_SIZE]; + memset(buf, 0, STORE_PAGE_SIZE); + buf[8] = STORE_PT_FREE; + put_u64(buf + OVF_NEXT_OFF, s->free_list_head); + if (page_write(s, id, buf) != 0) return -1; + s->free_list_head = id; + return 0; +} + +/* ── slotted page helpers (operate on a page already in `buf`) ────────────── */ +static int slp_count(const uint8_t* buf){ return get_u16(buf + 10); } +static int slp_free(const uint8_t* buf){ return get_u16(buf + 12); } +static void slp_slot(const uint8_t* buf, int i, uint16_t* off, uint16_t* len, uint16_t* fl){ + const uint8_t* p = buf + STORE_HDR + (size_t)i*SLOT_SIZE; + *off = get_u16(p); *len = get_u16(p+2); *fl = get_u16(p+4); +} +static void slp_set_slot(uint8_t* buf, int i, uint16_t off, uint16_t len, uint16_t fl){ + uint8_t* p = buf + STORE_HDR + (size_t)i*SLOT_SIZE; + put_u16(p, off); put_u16(p+2, len); put_u16(p+4, fl); +} +static int slp_live_count(const uint8_t* buf){ + int n = slp_count(buf), live = 0; + for (int i=0;i=reclen){ + memcpy(buf + o, rec, reclen); + slp_set_slot(buf, i, o, reclen, SLOT_LIVE); + return i; + } + } + int freeb = slp_free(buf); + if ((int)reclen + (int)SLOT_SIZE > freeb) return -1; + uint16_t heap_top = (uint16_t)(STORE_HDR + n*SLOT_SIZE + freeb); + uint16_t off = (uint16_t)(heap_top - reclen); + memcpy(buf + off, rec, reclen); + slp_set_slot(buf, n, off, reclen, SLOT_LIVE); + put_u16(buf + 10, (uint16_t)(n+1)); + put_u16(buf + 12, (uint16_t)(freeb - reclen - SLOT_SIZE)); + return n; +} + +/* ── overflow chains ──────────────────────────────────────────────────────── */ +static uint64_t ovf_write_chain(EngramPagedStore* s, const uint8_t* blob, size_t len){ + uint64_t head = 0, prev = 0; + size_t off = 0; + do { + uint64_t id = page_alloc(s, STORE_PT_OVERFLOW); + if (!id) return 0; + if (!head) head = id; + if (prev){ + uint8_t pbuf[STORE_PAGE_SIZE]; + if (page_read(s, prev, pbuf)!=0) return 0; + put_u64(pbuf + OVF_NEXT_OFF, id); + if (page_write(s, prev, pbuf)!=0) return 0; + } + uint8_t buf[STORE_PAGE_SIZE]; + memset(buf, 0, STORE_PAGE_SIZE); + buf[8] = STORE_PT_OVERFLOW; + size_t chunk = len - off; if (chunk > OVF_CHUNK) chunk = OVF_CHUNK; + put_u64(buf + OVF_NEXT_OFF, 0); + put_u32(buf + OVF_LEN_OFF, (uint32_t)chunk); + memcpy(buf + OVF_DATA_OFF, blob + off, chunk); + if (page_write(s, id, buf)!=0) return 0; + prev = id; off += chunk; + } while (off < len); + return head; +} +static uint8_t* ovf_read_chain(EngramPagedStore* s, uint64_t head, size_t total){ + uint8_t* out = (uint8_t*)malloc(total ? total : 1); + if (!out) return NULL; + size_t off = 0; uint64_t id = head; + while (id){ + uint8_t buf[STORE_PAGE_SIZE]; + if (page_read(s, id, buf)!=0){ free(out); return NULL; } + uint32_t chunk = get_u32(buf + OVF_LEN_OFF); + if (off + chunk > total){ free(out); return NULL; } + memcpy(out + off, buf + OVF_DATA_OFF, chunk); + off += chunk; + id = get_u64(buf + OVF_NEXT_OFF); + } + if (off != total){ free(out); return NULL; } + return out; +} + +/* ── growable byte buffer for TLV assembly ────────────────────────────────── */ +typedef struct { uint8_t* p; size_t len, cap; } Buf; +static int buf_reserve(Buf* b, size_t extra){ + if (b->len + extra <= b->cap) return 0; + size_t nc = b->cap ? b->cap*2 : 256; + while (nc < b->len + extra) nc *= 2; + uint8_t* np = (uint8_t*)realloc(b->p, nc); + if (!np) return -1; + b->p = np; b->cap = nc; return 0; +} +static int buf_raw(Buf* b, const void* data, size_t n){ + if (buf_reserve(b, n)) return -1; + memcpy(b->p + b->len, data, n); b->len += n; return 0; +} +static int tlv_field(Buf* b, uint8_t tag, const void* data, uint32_t len){ + if (buf_reserve(b, 5 + len)) return -1; + b->p[b->len++] = tag; + put_u32(b->p + b->len, len); b->len += 4; + if (len){ memcpy(b->p + b->len, data, len); b->len += len; } + return 0; +} +static int tlv_str(Buf* b, uint8_t tag, const char* s){ + if (!s) return 0; /* NULL string omitted → reader defaults NULL */ + return tlv_field(b, tag, s, (uint32_t)strlen(s)); +} +static int tlv_f64(Buf* b, uint8_t tag, double v){ uint8_t t[8]; put_f64(t,v); return tlv_field(b,tag,t,8); } +static int tlv_i64(Buf* b, uint8_t tag, int64_t v){ uint8_t t[8]; put_u64(t,(uint64_t)v); return tlv_field(b,tag,t,8); } +static int tlv_i32(Buf* b, uint8_t tag, int32_t v){ uint8_t t[4]; put_u32(t,(uint32_t)v); return tlv_field(b,tag,t,4); } +static int tlv_u32(Buf* b, uint8_t tag, uint32_t v){ uint8_t t[4]; put_u32(t,v); return tlv_field(b,tag,t,4); } + +/* ── node/edge serialization to a TLV body ────────────────────────────────── */ +static uint8_t* node_serialize(const StoreNode* n, size_t* out_len){ + Buf b = {0,0,0}; + tlv_str(&b, NT_ID, n->id); + tlv_str(&b, NT_CONTENT, n->content); + tlv_str(&b, NT_NODE_TYPE, n->node_type); + tlv_str(&b, NT_LABEL, n->label); + tlv_str(&b, NT_TIER, n->tier); + tlv_str(&b, NT_TAGS, n->tags); + tlv_str(&b, NT_METADATA, n->metadata); + tlv_f64(&b, NT_SALIENCE, n->salience); + tlv_f64(&b, NT_IMPORTANCE, n->importance); + tlv_f64(&b, NT_CONFIDENCE, n->confidence); + tlv_f64(&b, NT_DECAY, n->temporal_decay_rate); + tlv_i64(&b, NT_ACT_COUNT, n->activation_count); + tlv_i64(&b, NT_LAST_ACT, n->last_activated); + tlv_i64(&b, NT_CREATED, n->created_at); + tlv_i64(&b, NT_UPDATED, n->updated_at); + tlv_f64(&b, NT_BG_ACT, n->background_activation); + tlv_f64(&b, NT_WM_WEIGHT, n->working_memory_weight); + tlv_i32(&b, NT_SUPPRESS, n->suppression_count); + tlv_u32(&b, NT_LAYER, n->layer_id); + { uint8_t at[STORE_BLL_K*8]; + for (int i=0;iaccess_ts[i]); + tlv_field(&b, NT_ACCESS_TS, at, sizeof at); } + tlv_i32(&b, NT_ACCESS_HEAD, n->access_head); + tlv_i32(&b, NT_ACCESS_FILLED, n->access_filled); + tlv_f64(&b, NT_WM_ANCHOR, n->wm_anchor); + if (n->emb && n->emb_dim > 0){ + uint8_t* eb = (uint8_t*)malloc((size_t)n->emb_dim*4); + for (int i=0;iemb_dim;i++){ uint32_t u; memcpy(&u,&n->emb[i],4); put_u32(eb+i*4,u); } + tlv_field(&b, NT_EMB, eb, (uint32_t)n->emb_dim*4); + free(eb); + tlv_i32(&b, NT_EMB_DIM, n->emb_dim); + } + if (n->unknown && n->unknown_len) buf_raw(&b, n->unknown, n->unknown_len); + *out_len = b.len; + return b.p; +} +static uint8_t* edge_serialize(const StoreEdge* e, size_t* out_len){ + Buf b = {0,0,0}; + tlv_str(&b, ET_ID, e->id); + tlv_str(&b, ET_FROM, e->from_id); + tlv_str(&b, ET_TO, e->to_id); + tlv_str(&b, ET_RELATION, e->relation); + tlv_str(&b, ET_METADATA, e->metadata); + tlv_f64(&b, ET_WEIGHT, e->weight); + tlv_f64(&b, ET_HEBB, e->hebb); + tlv_f64(&b, ET_CONFIDENCE, e->confidence); + tlv_i64(&b, ET_CREATED, e->created_at); + tlv_i64(&b, ET_UPDATED, e->updated_at); + tlv_i64(&b, ET_LAST_FIRED, e->last_fired); + tlv_i32(&b, ET_INHIBITORY, e->inhibitory); + tlv_u32(&b, ET_LAYER, e->layer_id); + if (e->unknown && e->unknown_len) buf_raw(&b, e->unknown, e->unknown_len); + *out_len = b.len; + return b.p; +} + +/* ── TLV parse helpers ────────────────────────────────────────────────────── */ +static char* dup_str(const uint8_t* p, uint32_t len){ + char* s = (char*)malloc(len + 1); + if (!s) return NULL; + memcpy(s, p, len); s[len] = 0; return s; +} +static void node_parse(const uint8_t* body, size_t len, StoreNode* n){ + memset(n, 0, sizeof *n); + Buf unk = {0,0,0}; + size_t i = 0; + while (i + 5 <= len){ + uint8_t tag = body[i]; + uint32_t flen = get_u32(body + i + 1); + if (i + 5 + flen > len) break; + const uint8_t* v = body + i + 5; + switch (tag){ + case NT_ID: n->id = dup_str(v, flen); break; + case NT_CONTENT: n->content = dup_str(v, flen); break; + case NT_NODE_TYPE: n->node_type = dup_str(v, flen); break; + case NT_LABEL: n->label = dup_str(v, flen); break; + case NT_TIER: n->tier = dup_str(v, flen); break; + case NT_TAGS: n->tags = dup_str(v, flen); break; + case NT_METADATA: n->metadata = dup_str(v, flen); break; + case NT_SALIENCE: n->salience = get_f64(v); break; + case NT_IMPORTANCE: n->importance = get_f64(v); break; + case NT_CONFIDENCE: n->confidence = get_f64(v); break; + case NT_DECAY: n->temporal_decay_rate = get_f64(v); break; + case NT_ACT_COUNT: n->activation_count = (int64_t)get_u64(v); break; + case NT_LAST_ACT: n->last_activated = (int64_t)get_u64(v); break; + case NT_CREATED: n->created_at = (int64_t)get_u64(v); break; + case NT_UPDATED: n->updated_at = (int64_t)get_u64(v); break; + case NT_BG_ACT: n->background_activation = get_f64(v); break; + case NT_WM_WEIGHT: n->working_memory_weight = get_f64(v); break; + case NT_SUPPRESS: n->suppression_count = (int32_t)get_u32(v); break; + case NT_LAYER: n->layer_id = get_u32(v); break; + case NT_ACCESS_TS: + for (int k=0;kaccess_ts[k] = (int64_t)get_u64(v + k*8); + break; + case NT_ACCESS_HEAD: n->access_head = (int32_t)get_u32(v); break; + case NT_ACCESS_FILLED: n->access_filled = (int32_t)get_u32(v); break; + case NT_WM_ANCHOR: n->wm_anchor = get_f64(v); break; + case NT_EMB: { + int32_t dim = (int32_t)(flen/4); + n->emb = (float*)malloc(flen ? flen : 4); + for (int32_t k=0;kemb[k],&u,4); } + if (n->emb_dim == 0) n->emb_dim = dim; /* honour explicit dim if seen */ + break; } + case NT_EMB_DIM: n->emb_dim = (int32_t)get_u32(v); break; + default: + /* unknown tag: preserve verbatim */ + buf_raw(&unk, body + i, 5 + flen); + break; + } + i += 5 + flen; + } + n->unknown = unk.p; n->unknown_len = unk.len; +} +static void edge_parse(const uint8_t* body, size_t len, StoreEdge* e){ + memset(e, 0, sizeof *e); + Buf unk = {0,0,0}; + size_t i = 0; + while (i + 5 <= len){ + uint8_t tag = body[i]; + uint32_t flen = get_u32(body + i + 1); + if (i + 5 + flen > len) break; + const uint8_t* v = body + i + 5; + switch (tag){ + case ET_ID: e->id = dup_str(v, flen); break; + case ET_FROM: e->from_id = dup_str(v, flen); break; + case ET_TO: e->to_id = dup_str(v, flen); break; + case ET_RELATION: e->relation = dup_str(v, flen); break; + case ET_METADATA: e->metadata = dup_str(v, flen); break; + case ET_WEIGHT: e->weight = get_f64(v); break; + case ET_HEBB: e->hebb = get_f64(v); break; + case ET_CONFIDENCE: e->confidence = get_f64(v); break; + case ET_CREATED: e->created_at = (int64_t)get_u64(v); break; + case ET_UPDATED: e->updated_at = (int64_t)get_u64(v); break; + case ET_LAST_FIRED: e->last_fired = (int64_t)get_u64(v); break; + case ET_INHIBITORY: e->inhibitory = (int32_t)get_u32(v); break; + case ET_LAYER: e->layer_id = get_u32(v); break; + default: + buf_raw(&unk, body + i, 5 + flen); + break; + } + i += 5 + flen; + } + e->unknown = unk.p; e->unknown_len = unk.len; +} + +/* ── B+-tree ──────────────────────────────────────────────────────────────── */ +static uint32_t tree_payload(int tree){ return tree==TREE_PRIMARY ? PRIMARY_PAYLOAD : ADJ_PAYLOAD; } +static uint64_t tree_root(EngramPagedStore* s, int tree){ + return tree==TREE_PRIMARY ? s->root_index_page : s->adj_index_page; +} +static void tree_set_root(EngramPagedStore* s, int tree, uint64_t id){ + if (tree==TREE_PRIMARY) s->root_index_page = id; else s->adj_index_page = id; +} +static int leaf_max_entries(EngramPagedStore* s, uint32_t payload){ + int nat = (int)(IDX_BODY / (8 + payload)); + if (s->leaf_max > 0 && s->leaf_max < nat) return s->leaf_max; + return nat; +} +static int int_max_keys(EngramPagedStore* s){ + /* keys*8 + (keys+1)*8 <= IDX_BODY → keys <= IDX_BODY/8 - 1 */ + int nat = (int)(IDX_BODY / 8) - 1; + if (s->int_max > 0 && s->int_max < nat) return s->int_max; + return nat; +} + +/* Recursive insert. Returns 0 (no split) or 1 (split; sep_key and new_page set). */ +static int btree_insert(EngramPagedStore* s, int tree, uint64_t page_id, + uint64_t key, const uint8_t* payload, + uint64_t* sep_key, uint64_t* new_page){ + uint32_t pl = tree_payload(tree); + uint32_t esz = 8 + pl; + uint8_t buf[STORE_PAGE_SIZE]; + if (page_read(s, page_id, buf)!=0) return -1; + int nkeys = get_u16(buf + 10); + int is_leaf = buf[IDX_LEAF_OFF]; + + if (is_leaf){ + /* find insert position (after equal keys → stable duplicates) */ + int pos = 0; + while (pos < nkeys){ + uint64_t k = get_u64(buf + IDX_ENT_OFF + (size_t)pos*esz); + if (k > key) break; + pos++; + } + int lmax = leaf_max_entries(s, pl); + if (nkeys + 1 <= lmax){ + uint8_t* base = buf + IDX_ENT_OFF; + memmove(base + (size_t)(pos+1)*esz, base + (size_t)pos*esz, + (size_t)(nkeys - pos)*esz); + put_u64(base + (size_t)pos*esz, key); + memcpy(base + (size_t)pos*esz + 8, payload, pl); + put_u16(buf + 10, (uint16_t)(nkeys+1)); + return page_write(s, page_id, buf)==0 ? 0 : -1; + } + /* split: build the full sorted list in a temp, halve it */ + int total = nkeys + 1; + uint8_t* all = (uint8_t*)malloc((size_t)total*esz); + if (!all) return -1; + memcpy(all, buf + IDX_ENT_OFF, (size_t)pos*esz); + put_u64(all + (size_t)pos*esz, key); + memcpy(all + (size_t)pos*esz + 8, payload, pl); + memcpy(all + (size_t)(pos+1)*esz, buf + IDX_ENT_OFF + (size_t)pos*esz, + (size_t)(nkeys - pos)*esz); + int left = total/2, right = total - left; + uint64_t old_next = get_u64(buf + IDX_NEXT_OFF); + uint64_t rid = page_alloc(s, STORE_PT_INDEX); + if (!rid){ free(all); return -1; } + uint8_t rbuf[STORE_PAGE_SIZE]; + if (page_read(s, rid, rbuf)!=0){ free(all); return -1; } + rbuf[IDX_LEAF_OFF] = 1; + put_u16(rbuf + 10, (uint16_t)right); + put_u64(rbuf + IDX_NEXT_OFF, old_next); + memcpy(rbuf + IDX_ENT_OFF, all + (size_t)left*esz, (size_t)right*esz); + /* left page keeps first `left` entries, points to right */ + buf[IDX_LEAF_OFF] = 1; + put_u16(buf + 10, (uint16_t)left); + put_u64(buf + IDX_NEXT_OFF, rid); + memcpy(buf + IDX_ENT_OFF, all, (size_t)left*esz); + *sep_key = get_u64(all + (size_t)left*esz); + free(all); + if (page_write(s, rid, rbuf)!=0) return -1; + if (page_write(s, page_id, buf)!=0) return -1; + *new_page = rid; + return 1; + } + + /* internal: keys at IDX_ENT_OFF (nkeys u64), children after (nkeys+1 u64) */ + uint8_t* K = buf + IDX_ENT_OFF; + uint8_t* C = buf + IDX_ENT_OFF + (size_t)nkeys*8; + int ci = 0; + while (ci < nkeys && key >= get_u64(K + (size_t)ci*8)) ci++; + uint64_t child = get_u64(C + (size_t)ci*8); + uint64_t csep, cnew; + int sp = btree_insert(s, tree, child, key, payload, &csep, &cnew); + if (sp < 0) return -1; + if (sp == 0) return 0; + + /* child split → insert (csep, cnew) at position ci / ci+1 */ + int nk = nkeys + 1; + uint64_t* keys = (uint64_t*)malloc((size_t)nk*8); + uint64_t* kids = (uint64_t*)malloc((size_t)(nk+1)*8); + if (!keys || !kids){ free(keys); free(kids); return -1; } + for (int i=0;ici;i--) keys[i] = keys[i-1]; + keys[ci] = csep; + for (int i=nkeys+1;i>ci+1;i--) kids[i] = kids[i-1]; + kids[ci+1] = cnew; + + int imax = int_max_keys(s); + if (nk <= imax){ + put_u16(buf + 10, (uint16_t)nk); + uint8_t* K2 = buf + IDX_ENT_OFF; + uint8_t* C2 = buf + IDX_ENT_OFF + (size_t)nk*8; + for (int i=0;i' here — not + * the '>=' of insert routing — then scan forward across next_leaf. */ + int ci = 0; + while (ci < nkeys && target > get_u64(K + (size_t)ci*8)) ci++; + id = get_u64(C + (size_t)ci*8); + } + Buf acc = {0,0,0}; + for (;;){ + int nkeys = get_u16(buf + 10); + int past = 0; + for (int i=0;i target){ past = 1; break; } + if (buf_raw(&acc, buf + IDX_ENT_OFF + (size_t)i*esz + 8, pl)){ free(acc.p); return -1; } + } + if (past) break; + uint64_t nxt = get_u64(buf + IDX_NEXT_OFF); + if (!nxt) break; + if (page_read(s, nxt, buf)!=0){ free(acc.p); return -1; } + } + *out = acc.p; *count = acc.len / pl; + return 0; +} + +/* ── superblock ───────────────────────────────────────────────────────────── */ +static int sb_write_one(EngramPagedStore* s, uint64_t page){ + uint8_t buf[STORE_PAGE_SIZE]; + memset(buf, 0, STORE_PAGE_SIZE); + memcpy(buf + SB_MAGIC_OFF, STORE_MAGIC, 8); /* 7 chars + NUL */ + put_u32(buf + SB_FMT_OFF, s->format_version); + put_u32(buf + SB_PGSZ_OFF, s->page_size); + put_u64(buf + SB_PGCNT_OFF, s->page_count); + put_u64(buf + SB_FREE_OFF, s->free_list_head); + put_u64(buf + SB_ROOT_OFF, s->root_index_page); + put_u64(buf + SB_ADJ_OFF, s->adj_index_page); + put_u64(buf + SB_LAYER_OFF, s->layer_registry_page); + put_u64(buf + SB_CKPT_OFF, s->last_checkpoint_lsn); + put_u64(buf + SB_SEQ_OFF, s->sb_seq); + memcpy(buf + SB_UUID_OFF, s->uuid, 16); + put_u32(buf + SB_CRC_OFF, 0); + uint32_t crc = crc32_buf(buf, STORE_PAGE_SIZE); + put_u32(buf + SB_CRC_OFF, crc); + return page_write_raw(s, page, buf); +} +static int sb_load_one(EngramPagedStore* s, uint64_t page, uint8_t* buf, uint64_t* seq_out){ + if (page_read(s, page, buf)!=0) return -1; + if (memcmp(buf + SB_MAGIC_OFF, STORE_MAGIC, 8)!=0) return -1; + uint32_t stored = get_u32(buf + SB_CRC_OFF); + uint8_t tmp[STORE_PAGE_SIZE]; + memcpy(tmp, buf, STORE_PAGE_SIZE); + put_u32(tmp + SB_CRC_OFF, 0); + if (crc32_buf(tmp, STORE_PAGE_SIZE) != stored) return -1; + *seq_out = get_u64(buf + SB_SEQ_OFF); + return 0; +} +static void sb_apply(EngramPagedStore* s, const uint8_t* buf){ + s->format_version = get_u32(buf + SB_FMT_OFF); + s->page_size = get_u32(buf + SB_PGSZ_OFF); + s->page_count = get_u64(buf + SB_PGCNT_OFF); + s->free_list_head = get_u64(buf + SB_FREE_OFF); + s->root_index_page = get_u64(buf + SB_ROOT_OFF); + s->adj_index_page = get_u64(buf + SB_ADJ_OFF); + s->layer_registry_page= get_u64(buf + SB_LAYER_OFF); + s->last_checkpoint_lsn= get_u64(buf + SB_CKPT_OFF); + s->sb_seq = get_u64(buf + SB_SEQ_OFF); + memcpy(s->uuid, buf + SB_UUID_OFF, 16); +} + +int store_sync(EngramPagedStore* s){ + if (!s) return -1; + s->sb_seq++; + /* write primary, fsync, then mirror, fsync — so a torn write of one leaves + * the other valid; recovery prefers the higher valid sb_seq. */ + if (sb_write_one(s, 0)!=0) return -1; + if (fsync(s->fd)!=0) return -1; + if (sb_write_one(s, 1)!=0) return -1; + if (fsync(s->fd)!=0) return -1; + return 0; +} + +/* ── lifecycle ────────────────────────────────────────────────────────────── */ +static void gen_uuid(uint8_t out[16]){ + int fd = open("/dev/urandom", O_RDONLY); + if (fd >= 0){ ssize_t r = read(fd, out, 16); close(fd); if (r == 16) return; } + uint64_t t = (uint64_t)time(NULL) ^ ((uint64_t)getpid() << 32); + for (int i=0;i<16;i++){ t = t*6364136223846793005ULL + 1442695040888963407ULL; out[i] = (uint8_t)(t>>56); } +} +static uint64_t idx_alloc_leaf(EngramPagedStore* s){ + uint64_t id = page_alloc(s, STORE_PT_INDEX); + if (!id) return 0; + uint8_t buf[STORE_PAGE_SIZE]; + if (page_read(s, id, buf)!=0) return 0; + buf[IDX_LEAF_OFF] = 1; + put_u16(buf + 10, 0); + put_u64(buf + IDX_NEXT_OFF, 0); + if (page_write(s, id, buf)!=0) return 0; + return id; +} + +EngramPagedStore* store_create(const char* path){ + if (!s_crc_ready) crc_init(); + struct stat st; + if (stat(path, &st) == 0){ errno = EEXIST; return NULL; } + EngramPagedStore* s = (EngramPagedStore*)calloc(1, sizeof *s); + if (!s) return NULL; + s->fd = open(path, O_RDWR | O_CREAT | O_EXCL, 0600); + if (s->fd < 0){ free(s); return NULL; } + snprintf(s->path, sizeof s->path, "%s", path); + s->format_version = STORE_FORMAT_VERSION; + s->page_size = STORE_PAGE_SIZE; + s->page_count = 2; /* pages 0,1 are the superblock + mirror */ + s->free_list_head = 0; + s->next_lsn = 0; + s->sb_seq = 0; + gen_uuid(s->uuid); + /* reserve pages 0,1 on disk */ + uint8_t zero[STORE_PAGE_SIZE]; memset(zero, 0, sizeof zero); + if (page_write_raw(s, 0, zero)!=0 || page_write_raw(s, 1, zero)!=0){ close(s->fd); free(s); return NULL; } + s->root_index_page = idx_alloc_leaf(s); + s->adj_index_page = idx_alloc_leaf(s); + s->layer_registry_page = page_alloc(s, STORE_PT_NODE); /* reserved; unused in M1 */ + if (!s->root_index_page || !s->adj_index_page || !s->layer_registry_page){ + close(s->fd); free(s); return NULL; + } + if (store_sync(s)!=0){ close(s->fd); free(s); return NULL; } + return s; +} + +EngramPagedStore* store_open(const char* path){ + if (!s_crc_ready) crc_init(); + EngramPagedStore* s = (EngramPagedStore*)calloc(1, sizeof *s); + if (!s) return NULL; + s->fd = open(path, O_RDWR); + if (s->fd < 0){ free(s); return NULL; } + snprintf(s->path, sizeof s->path, "%s", path); + uint8_t b0[STORE_PAGE_SIZE], b1[STORE_PAGE_SIZE]; + uint64_t s0 = 0, s1 = 0; + int ok0 = sb_load_one(s, 0, b0, &s0) == 0; + int ok1 = sb_load_one(s, 1, b1, &s1) == 0; + if (!ok0 && !ok1){ close(s->fd); free(s); return NULL; } + const uint8_t* pick; + if (ok0 && ok1) pick = (s0 >= s1) ? b0 : b1; + else pick = ok0 ? b0 : b1; + sb_apply(s, pick); + if (s->page_size != STORE_PAGE_SIZE){ close(s->fd); free(s); return NULL; } + s->next_lsn = s->sb_seq; /* resume LSNs above the last synced value */ + s->cur_node_page = 0; + s->cur_edge_page = 0; + return s; +} + +int store_close(EngramPagedStore* s){ + if (!s) return -1; + int rc = store_sync(s); + if (s->fd >= 0) close(s->fd); + free(s); + return rc; +} + +/* ── put node/edge ────────────────────────────────────────────────────────── */ +/* Build the on-page record (inline or overflow stub) from a TLV body. */ +static int build_record(EngramPagedStore* s, const uint8_t* body, size_t blen, + uint8_t** rec_out, uint16_t* reclen_out){ + if (blen <= MAX_INLINE_BODY){ + uint16_t reclen = (uint16_t)(REC_HDR + blen); + uint8_t* rec = (uint8_t*)malloc(reclen); + if (!rec) return -1; + put_u16(rec, reclen); rec[2] = REC_VER; rec[3] = 0; + memcpy(rec + REC_HDR, body, blen); + *rec_out = rec; *reclen_out = reclen; + return 0; + } + uint64_t head = ovf_write_chain(s, body, blen); + if (!head) return -1; + uint16_t reclen = REC_HDR + 16; + uint8_t* rec = (uint8_t*)malloc(reclen); + if (!rec) return -1; + put_u16(rec, reclen); rec[2] = REC_VER; rec[3] = REC_OVERFLOW; + put_u64(rec + REC_HDR, head); + put_u64(rec + REC_HDR + 8, (uint64_t)blen); + *rec_out = rec; *reclen_out = reclen; + return 0; +} +/* Place a record into a NODE or EDGE page; returns (page_id,slot). Uses/creates + * the current page of that type; on-page dead-slot reuse handled by slp_put. */ +static int place_record(EngramPagedStore* s, uint8_t ptype, const uint8_t* rec, + uint16_t reclen, uint64_t* page_id_out, uint16_t* slot_out){ + uint64_t* cur = (ptype==STORE_PT_NODE) ? &s->cur_node_page : &s->cur_edge_page; + uint8_t buf[STORE_PAGE_SIZE]; + if (*cur){ + if (page_read(s, *cur, buf)==0){ + int slot = slp_put(buf, rec, reclen); + if (slot >= 0){ + if (page_write(s, *cur, buf)!=0) return -1; + *page_id_out = *cur; *slot_out = (uint16_t)slot; return 0; + } + } + } + uint64_t pid = page_alloc(s, ptype); + if (!pid) return -1; + if (page_read(s, pid, buf)!=0) return -1; + int slot = slp_put(buf, rec, reclen); + if (slot < 0) return -1; /* record too big for an empty page */ + if (page_write(s, pid, buf)!=0) return -1; + *cur = pid; + *page_id_out = pid; *slot_out = (uint16_t)slot; + return 0; +} + +int store_put_node(EngramPagedStore* s, const StoreNode* n){ + if (!s || !n || !n->id) return -1; + size_t blen; uint8_t* body = node_serialize(n, &blen); + if (!body) return -1; + uint8_t* rec; uint16_t reclen; + if (build_record(s, body, blen, &rec, &reclen)){ free(body); return -1; } + free(body); + uint64_t pid; uint16_t slot; + if (place_record(s, STORE_PT_NODE, rec, reclen, &pid, &slot)){ free(rec); return -1; } + free(rec); + uint8_t payload[PRIMARY_PAYLOAD]; + put_u64(payload, pid); put_u16(payload + 8, slot); + return btree_put(s, TREE_PRIMARY, id_hash(n->id), payload); +} + +int store_put_edge(EngramPagedStore* s, const StoreEdge* e){ + if (!s || !e || !e->id || !e->from_id || !e->to_id) return -1; + size_t blen; uint8_t* body = edge_serialize(e, &blen); + if (!body) return -1; + uint8_t* rec; uint16_t reclen; + if (build_record(s, body, blen, &rec, &reclen)){ free(body); return -1; } + free(body); + uint64_t pid; uint16_t slot; + if (place_record(s, STORE_PT_EDGE, rec, reclen, &pid, &slot)){ free(rec); return -1; } + free(rec); + /* primary index: edge id → loc */ + uint8_t pp[PRIMARY_PAYLOAD]; + put_u64(pp, pid); put_u16(pp + 8, slot); + if (btree_put(s, TREE_PRIMARY, id_hash(e->id), pp)!=0) return -1; + /* adjacency: from_id and to_id → edge loc */ + uint8_t af[ADJ_PAYLOAD]; af[0]=ADJ_DIR_FROM; put_u64(af+1,pid); put_u16(af+9,slot); + if (btree_put(s, TREE_ADJ, id_hash(e->from_id), af)!=0) return -1; + uint8_t at[ADJ_PAYLOAD]; at[0]=ADJ_DIR_TO; put_u64(at+1,pid); put_u16(at+9,slot); + if (btree_put(s, TREE_ADJ, id_hash(e->to_id), at)!=0) return -1; + return 0; +} + +/* ── read a record body (resolving overflow) at (page,slot). Returns malloc'd + * body + length, and whether the slot was live. ─────────────────────────── */ +static int read_body(EngramPagedStore* s, uint64_t page, uint16_t slot, + uint8_t** body_out, size_t* blen_out, int* live_out){ + uint8_t buf[STORE_PAGE_SIZE]; + if (page_read(s, page, buf)!=0) return -1; + if (slot >= slp_count(buf)) return -1; + uint16_t off,len,fl; slp_slot(buf, slot, &off, &len, &fl); + *live_out = (fl == SLOT_LIVE); + if (len < REC_HDR) return -1; + uint8_t rec_flags = buf[off + 3]; + if (rec_flags & REC_OVERFLOW){ + uint64_t head = get_u64(buf + off + REC_HDR); + uint64_t total = get_u64(buf + off + REC_HDR + 8); + uint8_t* body = ovf_read_chain(s, head, (size_t)total); + if (!body) return -1; + *body_out = body; *blen_out = (size_t)total; + } else { + uint16_t reclen = get_u16(buf + off); + size_t blen = reclen - REC_HDR; + uint8_t* body = (uint8_t*)malloc(blen ? blen : 1); + if (!body) return -1; + memcpy(body, buf + off + REC_HDR, blen); + *body_out = body; *blen_out = blen; + } + return 0; +} + +int store_get_node(EngramPagedStore* s, const char* id, StoreNode* out){ + if (!s || !id || !out) return -1; + uint8_t* locs; size_t n; + if (btree_lookup(s, TREE_PRIMARY, id_hash(id), &locs, &n)!=0) return -1; + int found = 0; + for (size_t i=0;itombstoned = 0; found = 1; + } else store_node_free(&cand); + } else store_node_free(&cand); + } + free(locs); + return found ? 1 : 0; +} + +int store_tombstone(EngramPagedStore* s, const char* id){ + if (!s || !id) return -1; + uint8_t* locs; size_t n; + if (btree_lookup(s, TREE_PRIMARY, id_hash(id), &locs, &n)!=0) return -1; + int hit = 0; + for (size_t i=0;i= slp_count(buf)) continue; + uint16_t off,len,fl; slp_slot(buf, slot, &off, &len, &fl); + if (fl != SLOT_LIVE) continue; + /* verify id matches this record before killing it */ + uint8_t* body; size_t blen; int live; + if (read_body(s, page, slot, &body, &blen, &live)!=0) continue; + StoreNode cand; node_parse(body, blen, &cand); free(body); + int match = cand.id && strcmp(cand.id, id)==0; + store_node_free(&cand); + if (!match) continue; + slp_set_slot(buf, slot, off, len, SLOT_DEAD); + if (page_write(s, page, buf)!=0){ free(locs); return -1; } + hit = 1; + /* if the page is now empty, reclaim it to the free list */ + if (page_read(s, page, buf)==0 && slp_live_count(buf)==0){ + if (s->cur_node_page == page) s->cur_node_page = 0; + if (s->cur_edge_page == page) s->cur_edge_page = 0; + page_free(s, page); + } + } + free(locs); + return hit ? 0 : 0; /* absent id is a no-op success */ +} + +static int get_edges_dir(EngramPagedStore* s, const char* id, uint8_t want_dir, + StoreEdge** out, size_t* n){ + *out = NULL; *n = 0; + uint8_t* locs; size_t cnt; + if (btree_lookup(s, TREE_ADJ, id_hash(id), &locs, &cnt)!=0) return -1; + StoreEdge* arr = NULL; size_t used = 0, cap = 0; + for (size_t i=0;ipage_count; id++){ + if (page_read(s, id, buf)!=0) return -1; + if (id == 0 || id == 1){ + uint32_t stored = get_u32(buf + SB_CRC_OFF); + uint8_t tmp[STORE_PAGE_SIZE]; memcpy(tmp, buf, STORE_PAGE_SIZE); + put_u32(tmp + SB_CRC_OFF, 0); + int okmagic = memcmp(buf + SB_MAGIC_OFF, STORE_MAGIC, 8)==0; + if (!okmagic || crc32_buf(tmp, STORE_PAGE_SIZE) != stored) bad++; + } else { + if (flags & STORE_CHECK_CRC){ if (!page_crc_ok(buf)) bad++; } + } + } + return bad; +} + +/* ── ownership helpers ────────────────────────────────────────────────────── */ +void store_node_free(StoreNode* n){ + if (!n) return; + free(n->id); free(n->content); free(n->node_type); free(n->label); + free(n->tier); free(n->tags); free(n->metadata); free(n->emb); free(n->unknown); + memset(n, 0, sizeof *n); +} +void store_edge_free(StoreEdge* e){ + if (!e) return; + free(e->id); free(e->from_id); free(e->to_id); free(e->relation); + free(e->metadata); free(e->unknown); + memset(e, 0, sizeof *e); +} +void store_edges_free(StoreEdge* arr, size_t n){ + if (!arr) return; + for (size_t i=0;ileaf_max = leaf_max; s->int_max = internal_max; +} +uint64_t store_page_count(const EngramPagedStore* s){ return s ? s->page_count : 0; } diff --git a/lang/runtime/engram_store.h b/lang/runtime/engram_store.h new file mode 100644 index 0000000..c791da0 --- /dev/null +++ b/lang/runtime/engram_store.h @@ -0,0 +1,135 @@ +/* engram_store.h — M1 of the engram tiered storage engine. + * + * The FINAL on-disk paged store format: superblock (+ mirror), slotted pages, + * self-describing TLV records, overflow chains, and two B+-tree indexes + * (primary id->loc, adjacency from_id/to_id->edge-locs) over a free-listed + * page file. See docs/architecture/design/engram-tiered-storage-engine.md §2. + * + * This is a self-contained module (plain C, standard libs only). It defines its + * own serializable views of a node/edge (StoreNode/StoreEdge) that mirror every + * persisted field of EngramNode/EngramEdge in el_runtime.c. M3 maps between the + * live runtime structs and these; M1 does not touch el_runtime.c. + * + * Format id: magic "ENGST01", format_version 1. This format is PERMANENT — the + * TLV record scheme means new fields never force a migration. + */ +#ifndef ENGRAM_STORE_H +#define ENGRAM_STORE_H + +#include +#include + +/* Fixed for the life of a store; recorded in the superblock. */ +#define STORE_PAGE_SIZE 16384u +#define STORE_MAGIC "ENGST01" /* 7 chars + NUL stored in an 8-byte field */ +#define STORE_FORMAT_VERSION 1u + +/* Ring-buffer length for ACT-R base-level access timestamps. + * MUST equal ENGRAM_BLL_K in el_runtime.c (currently 10). Static-checked in .c. */ +#define STORE_BLL_K 10 + +/* Page types (page header byte). */ +enum { + STORE_PT_NODE = 1, + STORE_PT_EDGE = 2, + STORE_PT_INDEX = 3, + STORE_PT_OVERFLOW = 4, + STORE_PT_FREE = 5 +}; + +/* store_check flags. */ +#define STORE_CHECK_CRC 1u + +/* ── Serializable node view: every persisted EngramNode field ─────────────── */ +typedef struct StoreNode { + char* id; + char* content; + char* node_type; + char* label; + char* tier; + char* tags; + char* metadata; + double salience; + double importance; + double confidence; + double temporal_decay_rate; + int64_t activation_count; + int64_t last_activated; + int64_t created_at; + int64_t updated_at; + double background_activation; + double working_memory_weight; + int32_t suppression_count; + uint32_t layer_id; + int64_t access_ts[STORE_BLL_K]; + int32_t access_head; + int32_t access_filled; + double wm_anchor; + float* emb; /* owned; NULL if not embedded */ + int32_t emb_dim; + /* Forward-compat: raw bytes of any TLV fields the reader did not recognise, + * concatenated verbatim ([tag][u32 len][bytes]...). Re-emitted on write so + * an old reader never drops a newer writer's fields. */ + uint8_t* unknown; + size_t unknown_len; + int tombstoned; /* set by store_get_* if the located record is dead */ + /* hebb_elig / hebb_elig_ts are DELIBERATELY NOT persisted (see EngramNode). */ +} StoreNode; + +/* ── Serializable edge view: every persisted EngramEdge field ─────────────── */ +typedef struct StoreEdge { + char* id; + char* from_id; + char* to_id; + char* relation; + char* metadata; + double weight; + double hebb; + double confidence; + int64_t created_at; + int64_t updated_at; + int64_t last_fired; + int32_t inhibitory; + uint32_t layer_id; + uint8_t* unknown; + size_t unknown_len; + int tombstoned; +} StoreEdge; + +typedef struct EngramPagedStore EngramPagedStore; + +/* Lifecycle. */ +EngramPagedStore* store_create(const char* path); /* fails if file exists */ +EngramPagedStore* store_open(const char* path); /* recovers via mirror SB */ +int store_close(EngramPagedStore* s); /* syncs + frees */ +int store_sync(EngramPagedStore* s); /* fsync + rewrite both superblocks */ + +/* Nodes. store_get_node returns 1 on hit (fills *out, caller store_node_free), + * 0 if absent or tombstoned, <0 on error. */ +int store_put_node(EngramPagedStore* s, const StoreNode* n); +int store_get_node(EngramPagedStore* s, const char* id, StoreNode* out); +int store_tombstone(EngramPagedStore* s, const char* id); + +/* Edges. *out is malloc'd (store_edges_free); *n set to count. */ +int store_put_edge(EngramPagedStore* s, const StoreEdge* e); +int store_get_edges_from(EngramPagedStore* s, const char* from_id, StoreEdge** out, size_t* n); +int store_get_edges_to(EngramPagedStore* s, const char* to_id, StoreEdge** out, size_t* n); + +/* Integrity: verify every page's crc (and both superblocks). Returns the number + * of corrupt pages (0 = clean), or <0 on I/O error. */ +int store_check(EngramPagedStore* s, unsigned flags); + +/* Ownership helpers. */ +void store_node_free(StoreNode* n); +void store_edge_free(StoreEdge* e); +void store_edges_free(StoreEdge* arr, size_t n); + +/* Test-only hook (NOT a format property — B+-tree nodes are self-describing via + * their stored key count). Caps entries/keys per index node to force splits on + * small datasets. 0 = natural full-page fanout. */ +void store__set_btree_order(EngramPagedStore* s, int leaf_max, int internal_max); + +/* Introspection for tests/tools. */ +uint64_t store_page_count(const EngramPagedStore* s); + +#endif /* ENGRAM_STORE_H */