nx_elara_memory.nx source
↩ module page · 317 lines · 17311 B
1// nx_elara_memory.nx -- LONG-TERM RELATIONSHIP MEMORY for the companion (F920/F925 memory axis).
2//
3// The companion mind (nx_elara_state) holds her PRESENT (wardrobe/mood/location/day). This is the other
4// half a real synthetic human needs and every incumbent (Replika's flagship) ships: she REMEMBERS things
5// about YOU and the relationship across time, and RECALLS the relevant ones. Sovereign, first-byte-up.
6//
7// Storage: seg-store plane knowledge/store/elaramem-, one NXR1 record per memory keyed <session>:<ms>
8// {text, kind, ts}. RECALL is keyword-overlap ranked (term match + recency) -- honest IR-lite, not yet
9// semantic embeddings (that is rung 2, when a CPU embed path is wired). SCALABLE: recall maps each segment
10// .docs ONCE and munmaps (the leak-free plane-scan pattern; a naive ss_get loop would crash, seq750).
11//
12// VERBS: remember <session> <text...> [kind] | recall <session> <query...> [max] | selftest
13// license_tier: ORIGINAL No hw writes (Rule 26).
14import "nx_syscalls.nx"
15import "nx_canon_cid.nx"
16import "nx_seg_store.nx"
17const EM_MAGIC_1024: i64 = 1024
18const EM_MAGIC_8192: i64 = 8192
19const EM_MAGIC_4096: i64 = 4096
20const EM_MAGIC_65536: i64 = 65536
21
22const EM_STORE: *u8 = "knowledge/store/elaramem-" as *u8
23const EM_MAXMATCH: i64 = 512
24
25func em_puts(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
26func em_len(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
27func em_cat(d: *u8, o: i64, s: *u8) -> i64 { var p: i64=o; var i: i64=0; while s[i]!=(0 as u8){ d[p]=s[i]; p=p+1; i=i+1 } return p }
28func em_ch(d: *u8, o: i64, c: i64) -> i64 { d[o]=c as u8; return o+1 }
29func em_udec(d: *u8, o: i64, v: i64) -> i64 {
30 if v==0 { d[o]=48 as u8; return o+1 }
31 var div: i64=1; var m: i64=v
32 while m>=10 { div=div*10; m=m/10 }
33 var p: i64=o; var rest: i64=v
34 while div>0 { let dg: i64=rest/div; d[p]=(48+dg) as u8; rest=rest-dg*div; div=div/10; p=p+1 }
35 return p
36}
37func em_lc(c: i64) -> i64 { if c>=65 { if c<=90 { return c+32 } } return c }
38func em_isalnum(c: i64) -> i64 { if c>=48 { if c<=57 { return 1 } } if c>=65 { if c<=90 { return 1 } } if c>=97 { if c<=122 { return 1 } } return 0 }
39func em_r32(p: *u8, o: i64) -> i64 { let a: i64=p[o]; let b: i64=p[o+1]; let c: i64=p[o+2]; let d: i64=p[o+3]; return (((((a<<8)|b)<<8)|c)<<8)|d }
40// bounded NXR1 field reader (DRY note seq751: 4th copy of this walk; canonical lib pending).
41func em_field(rec: *u8, reclen: i64, key: *u8, out: *u8, maxout: i64) -> i64 {
42 if reclen < 8 { return 0-1 }
43 if rec[0] != (78 as u8) { return 0-1 }
44 let klen: i64 = em_len(key)
45 let n: i64 = em_r32(rec, 4)
46 if n < 0 { return 0-1 }
47 if n > EM_MAGIC_1024 { return 0-1 }
48 var off: i64 = 8
49 var i: i64 = 0
50 while i < n {
51 if off+4 > reclen { return 0-1 }
52 let kl: i64 = em_r32(rec, off); off=off+4
53 if kl < 0 { return 0-1 }
54 if off+kl > reclen { return 0-1 }
55 let kp: i64 = off; off=off+kl
56 if off+4 > reclen { return 0-1 }
57 let vl: i64 = em_r32(rec, off); off=off+4
58 if vl < 0 { return 0-1 }
59 if off+vl > reclen { return 0-1 }
60 let vp: i64 = off; off=off+vl
61 if kl == klen {
62 var mm: i64=1; var c: i64=0
63 while c<kl { if rec[kp+c]!=key[c] { mm=0 } c=c+1 }
64 if mm==1 { var cap: i64=vl; if cap>maxout-1 { cap=maxout-1 } var w: i64=0; while w<cap { out[w]=rec[vp+w]; w=w+1 } out[cap]=0 as u8; return cap }
65 }
66 i=i+1
67 }
68 return 0-1
69}
70func em_key_prefix(key: *u8, kl: i64, prefix: *u8) -> i64 {
71 let pl: i64 = em_len(prefix)
72 if pl > kl { return 0 }
73 var i: i64=0
74 while i<pl { if key[i]!=prefix[i] { return 0 } i=i+1 }
75 return 1
76}
77// count query words that appear (case-insensitive substring) in text -> the relevance score.
78func em_score(text: *u8, tn: i64, query: *u8) -> i64 {
79 var score: i64 = 0
80 var ws: i64 = 0
81 var i: i64 = 0
82 while 1==1 {
83 var wend: i64 = 0
84 let c: i64 = query[i] as i64
85 if c==0 { wend=1 } else { if c==32 { wend=1 } }
86 if wend==1 {
87 let wl: i64 = i - ws
88 if wl >= 2 {
89 // does query[ws..i) occur in text (ci)?
90 // WORD-BOUNDARY match (not substring): 'do' must NOT match inside 'adopted'. A query word
91 // matches only bounded by start/end or a non-alphanumeric on both sides.
92 var found: i64 = 0
93 var t: i64 = 0
94 while t + wl <= tn { if found==0 {
95 var mm: i64=1; var k: i64=0
96 while k<wl { if em_lc(text[t+k] as i64) != em_lc(query[ws+k] as i64) { mm=0 } k=k+1 }
97 if mm==1 {
98 var lb: i64=1; if t>0 { if em_isalnum(text[t-1] as i64)==1 { lb=0 } }
99 var rb: i64=1; if t+wl<tn { if em_isalnum(text[t+wl] as i64)==1 { rb=0 } }
100 if lb==1 { if rb==1 { found=1 } }
101 }
102 t=t+1
103 } else { t=tn } }
104 if found==1 { score=score+1 }
105 }
106 ws = i+1
107 }
108 if c==0 { return score }
109 i=i+1
110 }
111 return score
112}
113
114// SALIENCE (F1056, the Nomi 'decides what matters' capability): score a memory's importance at store-time
115// so load-bearing facts persist + rank above trivia. Heuristic (rung 1; a learned model is rung 2), reusing
116// the whole-word scorer: durability-by-kind + user-relevance (you/your) + emotional/relational load-bearing
117// terms. Named weights, not buried magic. 0..100.
118func em_eq(a: *u8, b: *u8) -> i64 { var i: i64=0; while 1==1 { if a[i]!=b[i] { return 0 } if a[i]==(0 as u8) { return 1 } i=i+1 } return 0 }
119const EM_SAL_BASE: i64 = 10
120const EM_SAL_PREF: i64 = 30
121const EM_SAL_FACT: i64 = 22
122const EM_SAL_EVENT: i64 = 8
123const EM_SAL_USER: i64 = 15 // per 'you'/'your' hit
124const EM_SAL_LOAD: i64 = 10 // per load-bearing term
125func em_salience(text: *u8, tn: i64, kind: *u8) -> i64 {
126 var s: i64 = EM_SAL_BASE
127 if em_eq(kind, "preference" as *u8) == 1 { s = s + EM_SAL_PREF } else {
128 if em_eq(kind, "fact" as *u8) == 1 { s = s + EM_SAL_FACT } else { s = s + EM_SAL_EVENT }
129 }
130 s = s + em_score(text, tn, "you your" as *u8) * EM_SAL_USER
131 let lb: i64 = em_score(text, tn, "love favorite important always never birthday family sister brother mother father wife husband daughter son dream afraid hope hate married anniversary" as *u8)
132 s = s + lb * EM_SAL_LOAD
133 if s > 100 { s = 100 }
134 return s
135}
136
137func em_remember(session: *u8, text: *u8, kind: *u8) -> i64 {
138 sys_mkdir("knowledge" as *u8, 511)
139 sys_mkdir("knowledge/store" as *u8, 511)
140 let now: i64 = sys_now_realtime_ms()
141 // NXR1 record {text, kind, ts, sal} -- sal = the store-time salience (F1056, 'decides what matters')
142 let tnn: i64 = em_len(text)
143 let sal: i64 = em_salience(text, tnn, kind)
144 let keys: *i64 = sys_mmap(8*4) as *i64
145 let vals: *i64 = sys_mmap(8*4) as *i64
146 let tsb: *u8 = sys_mmap(32); var to: i64 = em_udec(tsb, 0, now); tsb[to]=0 as u8
147 let salb: *u8 = sys_mmap(32); var so: i64 = em_udec(salb, 0, sal); salb[so]=0 as u8
148 keys[0]="text" as *u8 as i64; vals[0]=text as i64
149 keys[1]="kind" as *u8 as i64; vals[1]=kind as i64
150 keys[2]="ts" as *u8 as i64; vals[2]=tsb as i64
151 keys[3]="sal" as *u8 as i64; vals[3]=salb as i64
152 let rec: *u8 = sys_mmap(EM_MAGIC_8192)
153 let rl: i64 = canon_encode(keys, vals, 4, rec)
154 let kb: *u8 = sys_mmap(256)
155 var ko: i64 = em_cat(kb, 0, session); ko=em_ch(kb, ko, 58); ko=em_udec(kb, ko, now); kb[ko]=0 as u8
156 let w: *i64 = ss_begin_cap(rl + EM_MAGIC_4096)
157 ss_add(w, 1, kb, rec, rl)
158 let segid: i64 = ss_next_segid(EM_STORE)
159 if ss_commit(EM_STORE, w, segid) != 0 { em_puts("{\"error\":\"memory write failed\"}\n" as *u8); return 4 }
160 let out: *u8 = sys_mmap(512)
161 var o: i64 = em_cat(out, 0, "{\"remembered\":\"" as *u8)
162 var ti: i64 = 0; while text[ti]!=(0 as u8) { if text[ti]==(34 as u8) { out[o]=92 as u8; o=o+1 } out[o]=text[ti]; o=o+1; ti=ti+1 }
163 o = em_cat(out, o, "\",\"seg\":" as *u8); o = em_udec(out, o, segid); o = em_cat(out, o, "}" as *u8); o = em_ch(out, o, 10)
164 sys_write(1, out, o)
165 return 0
166}
167
168// recall: map each segment once, score every <session>: memory vs query, print top `max` by score.
169func em_recall(session: *u8, query: *u8, max: i64) -> i64 {
170 let sp: *u8 = sys_mmap(256)
171 var spo: i64 = em_cat(sp, 0, session); sp[spo]=58 as u8; sp[spo+1]=0 as u8 // "<session>:"
172 // manifest
173 let mp: *u8 = sys_mmap(512); var mo: i64 = em_cat(mp, 0, EM_STORE); mo=em_cat(mp, mo, "manifest.txt" as *u8); mp[mo]=0 as u8
174 let szp: *i64 = sys_mmap(16) as *i64
175 let man: *u8 = sys_read_file(mp, szp)
176 // parallel arrays of matches: score, ts, textptr(copied)
177 let sc: *i64 = sys_mmap(8*EM_MAXMATCH) as *i64
178 let tsA: *i64 = sys_mmap(8*EM_MAXMATCH) as *i64
179 let txA: *i64 = sys_mmap(8*EM_MAXMATCH) as *i64
180 let salA: *i64 = sys_mmap(8*EM_MAXMATCH) as *i64
181 let rkA: *i64 = sys_mmap(8*EM_MAXMATCH) as *i64
182 var nm: i64 = 0
183 if (man as i64) != 0 {
184 let mlen: i64 = szp[0]
185 var ls: i64=0; var mi: i64=0
186 while mi <= mlen {
187 var eol: i64=0
188 if mi==mlen { eol=1 } else { if man[mi]==(10 as u8) { eol=1 } }
189 if eol==1 { if mi>ls {
190 let dp: *u8 = sys_mmap(512); var dpo: i64=em_cat(dp, 0, EM_STORE)
191 var kk: i64=ls; while kk<mi { dp[dpo]=man[kk]; dpo=dpo+1; kk=kk+1 }
192 dpo=em_cat(dp, dpo, ".docs" as *u8); dp[dpo]=0 as u8
193 let dszp: *i64 = sys_mmap(16) as *i64
194 let b: *u8 = sys_map_file(dp, dszp)
195 if (b as i64)!=0 {
196 let sz: i64 = dszp[0]
197 var j: i64=0
198 while j+9 <= sz {
199 let kind: i64=b[j]; let kl: i64=em_r32(b,j+1); let koff: i64=j+5
200 if kl<0 { j=sz } else { if koff+kl+4>sz { j=sz } else {
201 let vl: i64=em_r32(b,koff+kl); let voff: i64=koff+kl+4
202 if vl<0 { j=sz } else { if voff+vl>sz { j=sz } else {
203 if kind==1 { if em_key_prefix((b as i64+koff) as *u8, kl, sp)==1 { if nm<EM_MAXMATCH {
204 let txt: *u8 = sys_mmap(EM_MAGIC_4096)
205 let tl: i64 = em_field((b as i64+voff) as *u8, vl, "text" as *u8, txt, EM_MAGIC_4096)
206 if tl>0 {
207 let s: i64 = em_score(txt, tl, query)
208 if s>0 {
209 let tsb: *u8 = sys_mmap(32)
210 em_field((b as i64+voff) as *u8, vl, "ts" as *u8, tsb, 32)
211 var ts: i64=0; var z: i64=0; while tsb[z]!=(0 as u8){ let cc: i64=tsb[z] as i64; if cc>=48 { if cc<=57 { ts=ts*10+(cc-48) } } z=z+1 }
212 let salb2: *u8 = sys_mmap(32)
213 em_field((b as i64+voff) as *u8, vl, "sal" as *u8, salb2, 32)
214 var sv: i64=0; var zz: i64=0; while salb2[zz]!=(0 as u8){ let dc: i64=salb2[zz] as i64; if dc>=48 { if dc<=57 { sv=sv*10+(dc-48) } } zz=zz+1 }
215 sc[nm]=s; tsA[nm]=ts; txA[nm]=txt as i64; salA[nm]=sv; rkA[nm]=s*100+sv; nm=nm+1
216 }
217 }
218 } } }
219 j=voff+vl
220 }}
221 }}
222 }
223 sys_munmap(b, sz)
224 }
225 } ls=mi+1 }
226 mi=mi+1
227 }
228 }
229 // emit top `max` by (score desc, ts desc) via selection
230 let out: *u8 = sys_mmap(EM_MAGIC_65536)
231 var o: i64 = em_cat(out, 0, "{\"session\":\"" as *u8)
232 var si: i64=0; while session[si]!=(0 as u8){ out[o]=session[si]; o=o+1; si=si+1 }
233 o = em_cat(out, o, "\",\"query\":\"" as *u8)
234 var qi: i64=0; while query[qi]!=(0 as u8){ if query[qi]==(34 as u8){out[o]=92 as u8;o=o+1} out[o]=query[qi]; o=o+1; qi=qi+1 }
235 o = em_cat(out, o, "\",\"hits\":" as *u8); o = em_udec(out, o, nm)
236 o = em_cat(out, o, ",\"recall\":[" as *u8)
237 let used: *i64 = sys_mmap(8*EM_MAXMATCH) as *i64
238 var u: i64=0; while u<nm { used[u]=0; u=u+1 }
239 var emitted: i64 = 0
240 while emitted < max {
241 var best: i64 = 0-1
242 var bi: i64 = 0
243 while bi < nm {
244 if used[bi]==0 {
245 if best<0 { best=bi } else {
246 if rkA[bi]>rkA[best] { best=bi } else { if rkA[bi]==rkA[best] { if tsA[bi]>tsA[best] { best=bi } } }
247 }
248 }
249 bi=bi+1
250 }
251 if best<0 { emitted=max } else {
252 used[best]=1
253 if emitted>0 { o=em_ch(out, o, 44) }
254 o=em_cat(out, o, "{\"score\":" as *u8); o=em_udec(out, o, sc[best])
255 o=em_cat(out, o, ",\"text\":\"" as *u8)
256 let tp: *u8 = txA[best] as *u8
257 var ci: i64=0; while tp[ci]!=(0 as u8){ if tp[ci]==(34 as u8){out[o]=92 as u8;o=o+1} if tp[ci]==(10 as u8){out[o]=32 as u8}else{out[o]=tp[ci]} o=o+1; ci=ci+1 }
258 o=em_cat(out, o, "\"}" as *u8)
259 emitted=emitted+1
260 }
261 }
262 o=em_cat(out, o, "]}" as *u8); o=em_ch(out, o, 10)
263 sys_write(1, out, o)
264 return 0
265}
266
267func em_selftest() -> i64 {
268 var pass: i64 = 0
269 // run-unique session so the additive store stays deterministic
270 let S: *u8 = sys_mmap(64); var so: i64=em_cat(S,0,"mt_" as *u8); so=em_udec(S,so,sys_now_realtime_ms()); S[so]=0 as u8
271 em_remember(S, "you love oat milk lattes from the corner cafe" as *u8, "preference" as *u8)
272 em_remember(S, "your sister is named Robin and lives in Denver" as *u8, "fact" as *u8)
273 em_remember(S, "we watched a horror movie together last friday" as *u8, "event" as *u8)
274 // T0 recall by a query that hits ONE memory strongly
275 // capture recall by re-scoring in-test would be complex; instead assert the scorer directly
276 var t0: i64 = 0
277 if em_score("you love oat milk lattes" as *u8, 24, "oat milk latte" as *u8) >= 2 { t0 = 1 }
278 if t0==1 { pass=pass+1; em_puts("T0 keyword scorer matches query terms: PASS\n" as *u8) } else { em_puts("T0 keyword scorer matches query terms: FAIL\n" as *u8) }
279 // T1 scorer is case-insensitive + ignores non-matching
280 var t1: i64 = 0
281 if em_score("Your Sister is named ROBIN" as *u8, 26, "robin sister" as *u8) >= 2 { if em_score("nothing relevant here" as *u8, 21, "robin sister" as *u8) == 0 { t1 = 1 } }
282 if t1==1 { pass=pass+1; em_puts("T1 scorer case-insensitive + zero on miss: PASS\n" as *u8) } else { em_puts("T1 scorer case-insensitive + zero on miss: FAIL\n" as *u8) }
283 // T2 field reader bounds-safe
284 let bad: *u8 = sys_mmap(16); bad[0]=78 as u8; bad[4]=0 as u8; bad[5]=0 as u8; bad[6]=0 as u8; bad[7]=50 as u8
285 let ob: *u8 = sys_mmap(32)
286 if em_field(bad, 8, "x" as *u8, ob, 32) == (0-1) { pass=pass+1; em_puts("T2 nxr1 field bounds-safe: PASS\n" as *u8) } else { em_puts("T2 nxr1 field bounds-safe: FAIL\n" as *u8) }
287 // T3 short words (<2 chars) do not spuriously match
288 var t3: i64 = 0
289 if em_score("a big cat" as *u8, 9, "a" as *u8) == 0 { t3=1 }
290 if t3==1 { pass=pass+1; em_puts("T3 sub-2-char query words ignored: PASS\n" as *u8) } else { em_puts("T3 sub-2-char query words ignored: FAIL\n" as *u8) }
291 // T4 WORD-BOUNDARY: 'do' must NOT match inside 'adopted' (the false-positive that mis-ranked recall live)
292 var t4: i64 = 0
293 if em_score("your sister adopted a greyhound" as *u8, 31, "do" as *u8) == 0 { if em_score("we do talk daily" as *u8, 16, "do" as *u8) == 1 { t4=1 } }
294 if t4==1 { pass=pass+1; em_puts("T4 word-boundary (do != adopted, do == do): PASS\n" as *u8) } else { em_puts("T4 word-boundary (do != adopted, do == do): FAIL\n" as *u8) }
295 em_puts("nx_elara_memory selftest T=5 PASS=" as *u8); let pb: *u8=sys_mmap(16); var po: i64=em_udec(pb,0,pass); pb[po]=0 as u8; em_puts(pb)
296 if pass==5 { em_puts(" verdict=GREEN\n" as *u8); return 0 }
297 em_puts(" verdict=RED\n" as *u8); return 1
298}
299
300func main(argc: i64, argv: *i64) -> i64 {
301 if argc >= 2 { if em_eq((argv[1]) as *u8, "selftest" as *u8) == 1 { return em_selftest() } }
302 if argc < 4 { em_puts("usage: nx_elara_memory remember <session> <text> [kind] | recall <session> <query> [max] | selftest\n" as *u8); return 2 }
303 let verb: *u8 = (argv[1]) as *u8
304 let session: *u8 = (argv[2]) as *u8
305 if em_eq(verb, "remember" as *u8) == 1 {
306 var kind: *u8 = "fact" as *u8
307 if argc >= 5 { kind = (argv[4]) as *u8 }
308 return em_remember(session, (argv[3]) as *u8, kind)
309 }
310 if em_eq(verb, "recall" as *u8) == 1 {
311 var mx: i64 = 5
312 if argc >= 5 { var v: i64=0; let a: *u8=(argv[4]) as *u8; var i: i64=0; while a[i]!=(0 as u8){ let c: i64=a[i] as i64; if c>=48 { if c<=57 { v=v*10+(c-48) } } i=i+1 } if v>0 { mx=v } }
313 return em_recall(session, (argv[3]) as *u8, mx)
314 }
315 em_puts("{\"error\":\"unknown verb\"}\n" as *u8)
316 return 2
317}