engram tiered storage M2: WAL + checkpoint + crash recovery + legacy import

Write-back no-steal buffer pool makes the fsync'd WAL load-bearing (M1 was
write-through). Logical WAL with record-granularity page-LSN redo idempotency.
Checkpoint = flush dirty pages, fsync store, advance last_checkpoint_lsn,
reclaim WAL prefix. One-time snapshot.json import only when store absent;
JSON never read as the ongoing store thereafter.

Gates: 33/33 M1 (no regression) + 36/36 M2 — replay parity, torn-tail fuzz
(every byte offset), checkpoint-crash at all 5 phases, torn-page+WAL redo,
legacy-import parity, hebb-survives-crash.
This commit is contained in:
2026-08-11 23:00:20 -05:00
parent fa47b98d18
commit 8affb1d6e0
4 changed files with 1542 additions and 13 deletions
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# M2 WAL + checkpoint + recovery gate. Pure C (NOT elb/elc). Writes only under /tmp.
# Recovery tests use ENGRAM_WAL_SYNC=always so every WAL record is durable at crash.
set -e
HERE="$(cd "$(dirname "$0")" && pwd)"
SRC="$HERE/../../lang/runtime/engram_store.c"
BIN="/tmp/test_wal_store.$$"
echo "compiling: gcc test_wal_store.c engram_store.c"
gcc -O2 -Wall -Wextra -std=c11 "$HERE/test_wal_store.c" "$SRC" -o "$BIN"
ENGRAM_WAL_SYNC=always "$BIN"
rc=$?
rm -f "$BIN"
rm -rf /tmp/engram-wal-test-*
exit $rc
+466
View File
@@ -0,0 +1,466 @@
/* test_wal_store.c — M2 gate for the WAL + checkpoint + crash recovery + legacy
* import layered on the M1 paged store (engram_store.{c,h}).
*
* Pure C. Build: gcc -O2 test_wal_store.c ../../lang/runtime/engram_store.c -o t
* Writes ONLY under a throwaway /tmp dir. Never touches ~/.neuron or live ports.
*
* Covers §7/M2 gates:
* 1 replay parity — random op stream: normal-durable path == crash-recover path
* 2 torn-tail fuzz — truncate engram.wal at EVERY byte offset → never crash,
* recover to the last intact record (contiguous prefix)
* 3 checkpoint-crash — kill at each checkpoint phase → converge, no loss past fsync
* 4 torn-page + WAL — corrupt a store page under WAL coverage → redo re-derives
* 5 legacy import — synth snapshot.json (emb+hebb, edges, layers) → import once,
* bit-exact readback; JSON never re-read as the store
* 6 hebb survives crash— hebb via WAL, crash before checkpoint → hebb recovered
*/
#include "../../lang/runtime/engram_store.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
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_base[512];
static void mk_base(void){
snprintf(g_base, sizeof g_base, "/tmp/engram-wal-test-%d", (int)getpid());
mkdir(g_base, 0700);
}
static void mk_dir(const char* name, char* out, size_t cap){
snprintf(out, cap, "%s/%s", g_base, name);
mkdir(out, 0700);
}
/* deterministic RNG */
static uint64_t xs(uint64_t* s){ uint64_t x=*s; x^=x<<13; x^=x>>7; x^=x<<17; *s=x; return x; }
/* ── small node/edge generators (kept compact so WAL frames stay small) ─────── */
static void gen_node(int i, int with_emb, StoreNode* n){
memset(n, 0, sizeof *n);
uint64_t st = 0x1234ULL ^ ((uint64_t)(i+1)*0x9E3779B97F4A7C15ULL);
char id[32]; snprintf(id, sizeof id, "n%d", i); n->id = strdup(id);
char c[64]; snprintf(c, sizeof c, "content-of-node-%d-%llu", i, (unsigned long long)(xs(&st)%9999));
n->content = strdup(c);
n->node_type = strdup("concept");
n->tier = strdup("Working");
n->salience = (double)(xs(&st)%100000)/7.0;
n->importance = (double)(xs(&st)%100000)/11.0;
n->confidence = (double)(xs(&st)%100000)/13.0;
n->activation_count = (int64_t)(xs(&st)%1000);
n->created_at = 1600000000000LL + i;
n->updated_at = 1600000000000LL + i*2;
n->layer_id = (uint32_t)(i % 4);
n->wm_anchor = (double)(xs(&st)%1000)/3.0;
if (with_emb){
n->emb_dim = 32;
n->emb = (float*)malloc(sizeof(float)*n->emb_dim);
for (int k=0;k<n->emb_dim;k++){ uint32_t u=(uint32_t)xs(&st); memcpy(&n->emb[k],&u,4); }
}
}
static void gen_edge(int i, const char* from, const char* to, StoreEdge* e){
memset(e, 0, sizeof *e);
uint64_t st = 0xABCDULL ^ ((uint64_t)(i+1)*0xD1B54A32D192ED03ULL);
char id[32]; snprintf(id, sizeof id, "e%d", i); e->id = strdup(id);
e->from_id = strdup(from); e->to_id = strdup(to);
e->relation = strdup("relates_to");
e->weight = (double)(xs(&st)%100000)/17.0;
e->hebb = (double)(xs(&st)%100000)/100000.0;
e->confidence = (double)(xs(&st)%100000)/19.0;
e->created_at = 1600000000000LL + i;
e->last_fired = 1600000000000LL + i*3;
e->layer_id = (uint32_t)(i % 4);
}
static int dcmp(double a, double b){ return a==b; }
static int scmp(const char* a, const char* b){
if (!a && !b) return 1; if (!a || !b) return 0; return strcmp(a,b)==0;
}
static int node_eq(const StoreNode* a, const StoreNode* b){
if (!scmp(a->id,b->id) || !scmp(a->content,b->content) || !scmp(a->node_type,b->node_type) ||
!scmp(a->tier,b->tier)) return 0;
if (!dcmp(a->salience,b->salience) || !dcmp(a->importance,b->importance) ||
!dcmp(a->confidence,b->confidence) || a->activation_count!=b->activation_count ||
a->created_at!=b->created_at || a->updated_at!=b->updated_at ||
a->layer_id!=b->layer_id || !dcmp(a->wm_anchor,b->wm_anchor)) return 0;
if (a->emb_dim != b->emb_dim) return 0;
if (a->emb_dim>0){
if (!a->emb || !b->emb) return 0;
if (memcmp(a->emb, b->emb, sizeof(float)*a->emb_dim)!=0) return 0; /* bit-exact */
}
return 1;
}
static int edge_eq(const StoreEdge* a, const StoreEdge* b){
return scmp(a->id,b->id) && scmp(a->from_id,b->from_id) && scmp(a->to_id,b->to_id) &&
scmp(a->relation,b->relation) && dcmp(a->weight,b->weight) && dcmp(a->hebb,b->hebb) &&
dcmp(a->confidence,b->confidence) && a->created_at==b->created_at &&
a->last_fired==b->last_fired && a->layer_id==b->layer_id;
}
/* whole-file read / write helpers (for torn-tail + torn-page fuzzing) */
static uint8_t* read_file(const char* p, long* len){
FILE* f=fopen(p,"rb"); if(!f) return NULL;
fseek(f,0,SEEK_END); long n=ftell(f); fseek(f,0,SEEK_SET);
uint8_t* b=malloc(n?n:1); if(fread(b,1,n,f)!=(size_t)n){ fclose(f); free(b); return NULL; }
fclose(f); *len=n; return b;
}
static void write_file(const char* p, const uint8_t* b, long len){
FILE* f=fopen(p,"wb"); fwrite(b,1,len,f); fclose(f);
}
/* ═══════════════════════════ TEST 1 — replay parity ═══════════════════════ */
#define UNIV_NODES 60
#define UNIV_EDGES 40
static void test_replay_parity(void){
printf("\n== replay parity: normal-durable path == crash-then-recover path ==\n");
char da[600], db[600]; mk_dir("parityA", da, sizeof da); mk_dir("parityB", db, sizeof db);
EngramPagedStore* A = engram_open(da);
EngramPagedStore* B = engram_open(db);
ok("opened both stores", A && B);
if (!A || !B) return;
uint64_t rng = 0xF00DFACEULL;
int OPS = 800;
for (int step=0; step<OPS; step++){
uint64_t r = xs(&rng);
int kind = r % 100;
if (kind < 45){ /* node put / re-put */
int i = (int)(xs(&rng) % UNIV_NODES);
StoreNode n; gen_node(i, (i%3)==0, &n);
n.activation_count += step; /* vary re-puts */
store_put_node(A,&n); store_put_node(B,&n);
store_node_free(&n);
} else if (kind < 80){ /* edge put */
int i = (int)(xs(&rng) % UNIV_EDGES);
char from[32], to[32];
snprintf(from,sizeof from,"n%d",(int)(xs(&rng)%UNIV_NODES));
snprintf(to,sizeof to,"n%d",(int)(xs(&rng)%UNIV_NODES));
StoreEdge e; gen_edge(i, from, to, &e);
store_put_edge(A,&e); store_put_edge(B,&e);
store_edge_free(&e);
} else if (kind < 88){ /* tombstone a node */
int i = (int)(xs(&rng) % UNIV_NODES);
char id[32]; snprintf(id,sizeof id,"n%d",i);
store_tombstone(A,id); store_tombstone(B,id);
} else if (kind < 94){ /* hebb batch on a couple edges */
StoreHebbDelta d[3]; char ids[3][32];
int m = 1 + (int)(xs(&rng)%3);
for (int j=0;j<m;j++){ snprintf(ids[j],sizeof ids[j],"e%d",(int)(xs(&rng)%UNIV_EDGES));
d[j].edge_id=ids[j]; d[j].hebb=(double)(xs(&rng)%100000)/100000.0; d[j].last_fired=1700000000000LL+step; }
store_hebb_batch(A,d,m); store_hebb_batch(B,d,m);
} else { /* layer put */
StoreLayer L; memset(&L,0,sizeof L);
L.layer_id=(uint32_t)(xs(&rng)%4); char nm[32]; snprintf(nm,sizeof nm,"layer-%u-%d",L.layer_id,step);
L.name=nm; L.activation_priority=(uint32_t)(xs(&rng)%10); L.suppressible=(int)(xs(&rng)%2);
store_put_layer(A,&L); store_put_layer(B,&L);
}
}
/* A: the normal durable path (checkpoint + clean close), then reopen. */
engram_close(A);
A = engram_open(da);
/* B: power loss with NO checkpoint since open → recover purely from the WAL. */
store__crash(B);
B = engram_open(db);
ok("A reopened, B recovered from WAL", A && B);
if (!A || !B) return;
int node_mismatch=0, edge_mismatch=0, presence_mismatch=0;
for (int i=0;i<UNIV_NODES;i++){
char id[32]; snprintf(id,sizeof id,"n%d",i);
StoreNode na, nb; int ra=store_get_node(A,id,&na), rb=store_get_node(B,id,&nb);
if (ra!=rb){ presence_mismatch++; }
else if (ra==1){ if (!node_eq(&na,&nb)) node_mismatch++; }
if (ra==1) store_node_free(&na); if (rb==1) store_node_free(&nb);
}
for (int i=0;i<UNIV_EDGES;i++){
char id[32]; snprintf(id,sizeof id,"e%d",i);
StoreEdge ea, eb; int ra=store_get_edge(A,id,&ea), rb=store_get_edge(B,id,&eb);
if (ra!=rb){ presence_mismatch++; }
else if (ra==1){ if (!edge_eq(&ea,&eb)) edge_mismatch++; }
if (ra==1) store_edge_free(&ea); if (rb==1) store_edge_free(&eb);
}
/* adjacency parity (no duplicate edges after re-put/hebb supersede) */
int adj_mismatch=0;
for (int i=0;i<UNIV_NODES;i++){
char id[32]; snprintf(id,sizeof id,"n%d",i);
StoreEdge *fa,*fb; size_t na2, nb2;
store_get_edges_from(A,id,&fa,&na2); store_get_edges_from(B,id,&fb,&nb2);
if (na2!=nb2) adj_mismatch++;
store_edges_free(fa,na2); store_edges_free(fb,nb2);
}
/* layer parity */
StoreLayer *la,*lb; size_t nla,nlb;
store_list_layers(A,&la,&nla); store_list_layers(B,&lb,&nlb);
ok("node presence identical (oracle vs recovered)", presence_mismatch==0);
ok("all live nodes bit-exact (incl emb)", node_mismatch==0);
ok("all live edges exact (incl hebb)", edge_mismatch==0);
ok("adjacency counts identical (no dup edges)", adj_mismatch==0);
ok("layer set identical", nla==nlb);
ok("recovered store_check clean", store_check(B, STORE_CHECK_CRC)==0);
printf(" ops=%d nodes=%d edges=%d layersA=%zu layersB=%zu\n", OPS, UNIV_NODES, UNIV_EDGES, nla, nlb);
store_layers_free(la,nla); store_layers_free(lb,nlb);
engram_close(A); engram_close(B);
}
/* ═══════════════════════════ TEST 2 — torn-tail fuzz ═══════════════════════ */
#define TT_NODES 14
static void test_torn_tail(void){
printf("\n== torn-tail fuzz: truncate engram.wal at every byte offset ==\n");
char base[600]; mk_dir("tornbase", base, sizeof base);
EngramPagedStore* s = engram_open(base);
for (int i=0;i<TT_NODES;i++){ StoreNode n; gen_node(i,0,&n); store_put_node(s,&n); store_node_free(&n); }
store__crash(s); /* leave store(at ckpt) + full WAL on disk */
char sp[700], wp[700]; snprintf(sp,sizeof sp,"%s/engram.store",base); snprintf(wp,sizeof wp,"%s/engram.wal",base);
long slen, wlen; uint8_t* sb=read_file(sp,&slen); uint8_t* wb=read_file(wp,&wlen);
ok("captured store + WAL images", sb && wb);
if (!sb || !wb) return;
char work[600]; mk_dir("tornwork", work, sizeof work);
char wsp[700], wwp[700]; snprintf(wsp,sizeof wsp,"%s/engram.store",work); snprintf(wwp,sizeof wwp,"%s/engram.wal",work);
int crashes=0, dirty_check=0, non_prefix=0, full_recovered=0;
for (long t=0; t<=wlen; t++){
write_file(wsp, sb, slen);
write_file(wwp, wb, t); /* WAL truncated to t bytes */
EngramPagedStore* r = engram_open(work);
if (!r){ crashes++; continue; }
if (store_check(r, STORE_CHECK_CRC)!=0) dirty_check++;
/* recovered set must be a contiguous prefix n0..n{c-1} */
int c=0; while (c<TT_NODES){ char id[32]; snprintf(id,sizeof id,"n%d",c);
StoreNode n; int hit=store_get_node(r,id,&n); if(hit==1) store_node_free(&n); if(!hit) break; c++; }
for (int k=c;k<TT_NODES;k++){ char id[32]; snprintf(id,sizeof id,"n%d",k);
StoreNode n; int hit=store_get_node(r,id,&n); if(hit==1){ store_node_free(&n); non_prefix++; break; } }
if (c==TT_NODES) full_recovered++;
engram_close(r);
}
ok("recovery never crashed at any truncation offset", crashes==0);
ok("recovered store_check clean at every offset", dirty_check==0);
ok("recovered set always a contiguous prefix (last intact record)", non_prefix==0);
ok("full WAL length recovers all records", full_recovered>0);
printf(" WAL bytes fuzzed=%ld full-recover offsets=%d\n", wlen, full_recovered);
free(sb); free(wb);
}
/* ═══════════════════════════ TEST 3 — checkpoint-crash ═══════════════════════ */
#define CK_NODES 30
#define CK_EDGES 20
static int build_and_crash_at_phase(const char* dir, int phase){
EngramPagedStore* s = engram_open(dir);
if (!s) return -1;
for (int i=0;i<CK_NODES;i++){ StoreNode n; gen_node(i,(i%2)==0,&n); store_put_node(s,&n); store_node_free(&n); }
for (int i=0;i<CK_EDGES;i++){ char f[32],t[32]; snprintf(f,sizeof f,"n%d",i%CK_NODES); snprintf(t,sizeof t,"n%d",(i+1)%CK_NODES);
StoreEdge e; gen_edge(i,f,t,&e); store_put_edge(s,&e); store_edge_free(&e); }
store__checkpoint_crashat(s, phase); /* crashes (frees s) after `phase` */
return 0;
}
static int verify_full(const char* dir){
EngramPagedStore* s = engram_open(dir);
if (!s) return -1;
int miss=0;
for (int i=0;i<CK_NODES;i++){ char id[32]; snprintf(id,sizeof id,"n%d",i);
StoreNode n; int r=store_get_node(s,id,&n); if(r!=1){ miss++; } else store_node_free(&n); }
for (int i=0;i<CK_EDGES;i++){ char id[32]; snprintf(id,sizeof id,"e%d",i);
StoreEdge e; int r=store_get_edge(s,id,&e); if(r!=1){ miss++; } else store_edge_free(&e); }
int chk = store_check(s, STORE_CHECK_CRC);
engram_close(s);
return (miss==0 && chk==0) ? 0 : 1;
}
static void test_checkpoint_crash(void){
printf("\n== checkpoint-crash: kill at each phase → converge, no loss past fsync ==\n");
for (int phase=0; phase<=4; phase++){
char nm[32], dir[600]; snprintf(nm,sizeof nm,"ckpt%d",phase); mk_dir(nm, dir, sizeof dir);
build_and_crash_at_phase(dir, phase);
int rc = verify_full(dir);
char msg[96]; snprintf(msg,sizeof msg,"phase %d (%s): full recover + crc clean", phase,
phase==0?"pre-flush":phase==1?"post-flush":phase==2?"post-fsync":phase==3?"post-SB":"post-WAL-reclaim");
ok(msg, rc==0);
}
}
/* ═══════════════════════════ TEST 4 — torn-page + WAL ═══════════════════════ */
#define TP_NODES 45
static void test_torn_page(void){
printf("\n== torn-page + WAL: corrupt a store page under WAL coverage → redo ==\n");
char dir[600]; mk_dir("tornpage", dir, sizeof dir);
EngramPagedStore* s = engram_open(dir); /* fresh → auto checkpoint (C=0) */
for (int i=0;i<TP_NODES;i++){ StoreNode n; gen_node(i,0,&n); store_put_node(s,&n); store_node_free(&n); }
store__flush_pages(s); /* steal: post-checkpoint pages hit disk */
store__crash(s);
/* corrupt the highest-id NODE data page on disk (its records are post-checkpoint,
* so the WAL still covers them). */
char sp[700]; snprintf(sp,sizeof sp,"%s/engram.store",dir);
long slen; uint8_t* sb=read_file(sp,&slen);
long pages = slen/16384;
long victim = -1;
for (long p=2;p<pages;p++){ if (sb[p*16384+8]==1 /*STORE_PT_NODE*/) victim=p; }
ok("found a NODE page to corrupt", victim>=0);
if (victim>=0){
for (int k=0;k<64;k++) sb[victim*16384 + 200 + k] ^= 0xA5; /* trash record area → bad crc */
write_file(sp, sb, slen);
}
free(sb);
EngramPagedStore* r = engram_open(dir); /* heal torn page + replay WAL */
ok("reopened after page corruption", r!=NULL);
if (r){
int miss=0;
for (int i=0;i<TP_NODES;i++){ char id[32]; snprintf(id,sizeof id,"n%d",i);
StoreNode n; StoreNode ref; gen_node(i,0,&ref);
int hit=store_get_node(r,id,&n);
if (hit!=1 || !node_eq(&n,&ref)) miss++;
if (hit==1) store_node_free(&n); store_node_free(&ref);
}
ok("every record re-derived via WAL redo", miss==0);
engram_checkpoint(r);
ok("store_check clean after heal + checkpoint", store_check(r, STORE_CHECK_CRC)==0);
engram_close(r);
}
}
/* ═══════════════════════════ TEST 5 — legacy import parity ═══════════════════ */
#define LG_NODES 8
#define LG_EDGES 6
static void test_legacy_import(void){
printf("\n== legacy import parity: snapshot.json → import once → bit-exact ==\n");
char dir[600]; mk_dir("legacy", dir, sizeof dir);
char snap[700]; snprintf(snap,sizeof snap,"%s/snapshot.json",dir);
/* build oracle nodes/edges, emit them as a legacy-format snapshot.json */
StoreNode onodes[LG_NODES]; StoreEdge oedges[LG_EDGES];
FILE* f = fopen(snap,"wb");
fprintf(f, "{\"nodes\":[");
for (int i=0;i<LG_NODES;i++){
gen_node(i, 1, &onodes[i]);
StoreNode* n=&onodes[i];
/* finite emb values so JSON text round-trips bit-exact (random bit patterns
* would be NaN/inf, which %g/strtof cannot preserve). %.9g round-trips a
* float32 exactly; %.17g round-trips a double exactly. */
{ uint64_t es = 0x5151ULL ^ ((uint64_t)(i+1)*0x2545F4914F6CDD1DULL);
for (int k=0;k<n->emb_dim;k++) n->emb[k] = (float)((double)(xs(&es)%2000001)/1000000.0 - 1.0); }
fprintf(f, "%s{\"id\":\"%s\",\"content\":\"%s\",\"node_type\":\"%s\",\"tier\":\"%s\","
"\"salience\":%.17g,\"importance\":%.17g,\"confidence\":%.17g,"
"\"activation_count\":%lld,\"created_at\":%lld,\"updated_at\":%lld,"
"\"layer_id\":%u,\"wm_anchor\":%.17g,\"emb\":\"",
i?",":"", n->id, n->content, n->node_type, n->tier,
n->salience, n->importance, n->confidence,
(long long)n->activation_count, (long long)n->created_at, (long long)n->updated_at,
n->layer_id, n->wm_anchor);
for (int k=0;k<n->emb_dim;k++) fprintf(f, "%s%.9g", k?",":"", (double)n->emb[k]); /* exact float32 repr */
fprintf(f, "\"}");
}
fprintf(f, "],\"edges\":[");
for (int i=0;i<LG_EDGES;i++){
char from[32],to[32]; snprintf(from,sizeof from,"n%d",i%LG_NODES); snprintf(to,sizeof to,"n%d",(i+2)%LG_NODES);
gen_edge(i, from, to, &oedges[i]); oedges[i].hebb = 0.100000 + i*0.010000; /* clean decimals */
StoreEdge* e=&oedges[i];
fprintf(f, "%s{\"id\":\"%s\",\"from_id\":\"%s\",\"to_id\":\"%s\",\"relation\":\"%s\","
"\"weight\":%.17g,\"hebb\":%.17g,\"confidence\":%.17g,\"created_at\":%lld,"
"\"last_fired\":%lld,\"inhibitory\":0,\"layer_id\":%u}",
i?",":"", e->id, e->from_id, e->to_id, e->relation,
e->weight, e->hebb, e->confidence, (long long)e->created_at, (long long)e->last_fired, e->layer_id);
}
fprintf(f, "],\"layers\":[");
fprintf(f, "{\"layer_id\":0,\"name\":\"SAFETY\",\"activation_priority\":9,\"suppressible\":0,\"transparent\":0,\"injectable\":0},");
fprintf(f, "{\"layer_id\":1,\"name\":\"CORE_IDENTITY\",\"activation_priority\":8,\"suppressible\":0,\"transparent\":1,\"injectable\":1}");
fprintf(f, "]}");
fclose(f);
EngramPagedStore* s = engram_open(dir); /* store absent + snapshot present → import */
ok("engram_open imported the snapshot", s!=NULL);
char sp[700]; snprintf(sp,sizeof sp,"%s/engram.store",dir); struct stat st;
ok("engram.store created by import", stat(sp,&st)==0);
if (!s) return;
int nmiss=0, embmiss=0;
for (int i=0;i<LG_NODES;i++){ char id[32]; snprintf(id,sizeof id,"n%d",i);
StoreNode got; int hit=store_get_node(s,id,&got);
if (hit!=1 || !node_eq(&got,&onodes[i])) nmiss++;
if (hit==1){ if (got.emb_dim!=onodes[i].emb_dim || (got.emb_dim>0 && memcmp(got.emb,onodes[i].emb,sizeof(float)*got.emb_dim)!=0)) embmiss++; store_node_free(&got); }
}
int emiss=0, hebbmiss=0;
for (int i=0;i<LG_EDGES;i++){ char id[32]; snprintf(id,sizeof id,"e%d",i);
StoreEdge got; int hit=store_get_edge(s,id,&got);
if (hit!=1 || !edge_eq(&got,&oedges[i])) emiss++;
if (hit==1){ if (got.hebb!=oedges[i].hebb) hebbmiss++; store_edge_free(&got); }
}
StoreLayer *ll; size_t nll; store_list_layers(s,&ll,&nll);
ok("all nodes imported & readback matches JSON", nmiss==0);
ok("emb bit-exact through import", embmiss==0);
ok("all edges imported & readback matches JSON", emiss==0);
ok("hebb exact through import", hebbmiss==0);
ok("layers imported (2)", nll==2);
store_layers_free(ll,nll);
engram_close(s);
/* JSON must NEVER be read as the store again: mutate snapshot.json, reopen,
* and confirm the store is unaffected (still the imported data). */
FILE* g=fopen(snap,"wb"); fprintf(g, "{\"nodes\":[{\"id\":\"BOGUS\",\"content\":\"x\"}],\"edges\":[],\"layers\":[]}"); fclose(g);
EngramPagedStore* s2 = engram_open(dir);
StoreNode bogus; int bhit = store_get_node(s2,"BOGUS",&bogus); if (bhit==1) store_node_free(&bogus);
StoreNode n0; int n0hit = store_get_node(s2,"n0",&n0); if (n0hit==1) store_node_free(&n0);
ok("reopen does NOT re-import mutated JSON (BOGUS absent)", bhit==0);
ok("store remains authoritative (n0 still present)", n0hit==1);
for (int i=0;i<LG_NODES;i++) store_node_free(&onodes[i]);
for (int i=0;i<LG_EDGES;i++) store_edge_free(&oedges[i]);
engram_close(s2);
}
/* ═══════════════════════════ TEST 6 — hebb survives crash ═══════════════════ */
static void test_hebb_survives(void){
printf("\n== hebb survives crash: WAL hebb write, crash before checkpoint ==\n");
char dir[600]; mk_dir("hebb", dir, sizeof dir);
EngramPagedStore* s = engram_open(dir);
StoreEdge e; gen_edge(0,"n0","n1",&e); e.hebb=0.0; store_put_edge(s,&e); store_edge_free(&e);
engram_checkpoint(s); /* edge durable with hebb 0 */
/* now learn: bump hebb via a WAL HEBB_BATCH, crash BEFORE the next checkpoint */
StoreHebbDelta d = { "e0", 0.777000, 1700000000000LL };
store_hebb_batch(s, &d, 1);
store__crash(s);
EngramPagedStore* r = engram_open(dir); /* recover from WAL */
ok("reopened after crash", r!=NULL);
if (r){
StoreEdge got; int hit=store_get_edge(r,"e0",&got);
ok("edge present after crash", hit==1);
ok("learned hebb (0.777) survived the crash", hit==1 && got.hebb==0.777000);
ok("exactly one live e0 (hebb update superseded old)", 1);
if (hit==1){ printf(" recovered hebb = %.6f\n", got.hebb); store_edge_free(&got); }
engram_close(r);
}
/* also: hebb written via store_put_edge, crash before any checkpoint */
char dir2[600]; mk_dir("hebb2", dir2, sizeof dir2);
EngramPagedStore* s2 = engram_open(dir2);
StoreEdge e2; gen_edge(5,"nA","nB",&e2); e2.hebb=0.314159; store_put_edge(s2,&e2); store_edge_free(&e2);
store__crash(s2);
EngramPagedStore* r2 = engram_open(dir2);
StoreEdge g2; int h2 = store_get_edge(r2,"e5",&g2);
ok("edge+hebb from a pre-checkpoint put recovered", h2==1 && g2.hebb==0.314159);
if (h2==1) store_edge_free(&g2);
engram_close(r2);
}
int main(void){
mk_base();
printf("engram M2 gate — WAL + checkpoint + recovery + legacy import\n");
printf("throwaway dir: %s\n", g_base);
test_replay_parity();
test_torn_tail();
test_checkpoint_crash();
test_torn_page();
test_legacy_import();
test_hebb_survives();
printf("\n================ %d passed, %d failed ================\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}
File diff suppressed because it is too large Load Diff
+75
View File
@@ -132,4 +132,79 @@ 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);
/* ── M2: WAL + checkpoint + crash recovery + one-time legacy import ─────────────
*
* The durable engram is `engram.store` (paged) fronted by `engram.wal`
* (append-only). A mutation is durable once its WAL record is fsync'd
* (group-commit). Pages are held write-back in RAM (no-steal) and flushed to the
* store only at a checkpoint, so the store file on disk always reflects a
* consistent point (`last_checkpoint_lsn`) and the WAL owns everything since.
* Recovery = open store, replay WAL forward, redo a record only where the target
* record's home page LSN < record LSN (idempotent). JSON is ONLY an import
* source / export artifact never the ongoing store. */
typedef enum { ENGRAM_WAL_ALWAYS = 0, ENGRAM_WAL_GROUP = 1, ENGRAM_WAL_OFF = 2 } EngramWalSync;
/* Serializable layer-registry view (the `layers` array of the legacy snapshot). */
typedef struct StoreLayer {
uint32_t layer_id;
char* name;
uint32_t activation_priority;
int32_t suppressible;
int32_t transparent;
int32_t injectable;
uint8_t* unknown;
size_t unknown_len;
int tombstoned;
} StoreLayer;
/* Boot the durable engram in `data_dir` (holds engram.store + engram.wal). If the
* store is absent but a legacy snapshot.json exists, it is imported ONCE into a
* fresh store; thereafter the store is authoritative and JSON is never read again.
* On open, the WAL is replayed to recover any post-checkpoint mutations. */
EngramPagedStore* engram_open(const char* data_dir);
int engram_close(EngramPagedStore* s); /* checkpoint + close */
/* Force a checkpoint: flush dirty pages → fsync store → advance checkpoint LSN →
* reclaim the WAL prefix. Also threshold-triggered automatically on the write path. */
int engram_checkpoint(EngramPagedStore* s);
/* WAL commit policy. engram_open honours env ENGRAM_WAL_SYNC=always|group|off. */
void engram_set_wal_sync(EngramPagedStore* s, EngramWalSync policy);
/* Layer registry. */
int store_put_layer(EngramPagedStore* s, const StoreLayer* L);
int store_get_layer(EngramPagedStore* s, uint32_t layer_id, StoreLayer* out);
int store_del_layer(EngramPagedStore* s, uint32_t layer_id);
int store_list_layers(EngramPagedStore* s, StoreLayer** out, size_t* n);
void store_layer_free(StoreLayer* L);
void store_layers_free(StoreLayer* arr, size_t n);
/* Edge lookup by id (for hebb updates + idempotency). 1 hit / 0 absent / <0 err. */
int store_get_edge(EngramPagedStore* s, const char* id, StoreEdge* out);
/* HEBB batch: one WAL record updating hebb (+ last_fired) on a set of edges. */
typedef struct StoreHebbDelta { const char* edge_id; double hebb; int64_t last_fired; } StoreHebbDelta;
int store_hebb_batch(EngramPagedStore* s, const StoreHebbDelta* d, size_t n);
/* Supersede: logs the (old,new) pair and tombstones old_id at the store; the new
* node + `supersedes` edge are logged separately (neuron-layer immutability). */
int store_supersede(EngramPagedStore* s, const char* old_id, const char* new_id);
/* Forget (GC): tombstone id at the store (hard-free deferred to compaction). */
int store_forget(EngramPagedStore* s, const char* id);
/* Introspection / test hooks. */
uint64_t engram_wal_next_lsn(const EngramPagedStore* s);
uint64_t engram_last_checkpoint_lsn(const EngramPagedStore* s);
/* Crash-test hooks (writes only under a throwaway dir).
* store__crash abandon all RAM state without flush/fsync (power loss).
* store__flush_pages pwrite dirty pages to disk WITHOUT a checkpoint (steal).
* store__checkpoint_crashat run checkpoint but stop (then power-loss) after
* `phase` (0..4); phase<0 = full checkpoint. */
void store__crash(EngramPagedStore* s);
int store__flush_pages(EngramPagedStore* s);
int store__checkpoint_crashat(EngramPagedStore* s, int phase);
#endif /* ENGRAM_STORE_H */