/* 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; }