nx_companion_memory.nx source
↩ module page · 128 lines · 6142 B
1// nx_companion_memory.nx -- R1b of the ELDER AI companion spine: EPISODIC MEMORY. The Replika-differentiator
2// is that a real companion does not forget -- Elder's memory is APPEND-ONLY and total by construction (rule 13:
3// additive; history is sacred). Every interaction is one episode appended to the canonical nx_registry over
4// seg_store, keyed uniquely and enumerable per-persona in chronological order. Recall is (a) total (mem_count
5// == everything ever said) and (b) content-searchable (mem_search = plant a fact early, find it many turns
6// later -- LEXICAL here; the semantic layer is R1c via nx_distrib_embed + nx_vec_index). One memory stream per
7// persona, so Aria's history and Vera's history never mix. license_tier: ORIGINAL
8import "nx_registry.nx"
9import "nx_tabrec.nx"
10const EPI_MAGIC_8192: i64 = 8192
11
12const EPI_PREFIX: *u8 = "knowledge/episodic-" as *u8
13const EPI_KP: *u8 = "epi:" as *u8
14
15// record fields (rule 11: named). record = session \t ts \t speaker \t text
16const MF_SESSION: i64 = 0
17const MF_TS: i64 = 1
18const MF_SPEAKER: i64 = 2
19const MF_TEXT: i64 = 3
20
21// ---- id / index-key construction ---------------------------------------------------------------------
22func mem_strlen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
23func mem_catz(dst: *u8, o: i64, s: *u8) -> i64 { var i: i64=0; while s[i]!=(0 as u8){ dst[o+i]=s[i]; i=i+1 } return o+i }
24// zero-padded 8-digit seq (lexical order == chronological order)
25func mem_pad8(dst: *u8, o: i64, v: i64) -> i64 {
26 var m: i64 = v
27 var i: i64 = 7
28 while i >= 0 { dst[o+i] = (48 + (m % 10)) as u8; m = m / 10; i = i - 1 }
29 return o + 8
30}
31// "<persona>:<seq8>" into buf, NUL-terminated
32func mem_epi_id(buf: *u8, persona: *u8, seq: i64) -> i64 {
33 var o: i64 = mem_catz(buf, 0, persona)
34 buf[o] = 0x3A as u8; o = o + 1 // ':'
35 o = mem_pad8(buf, o, seq)
36 buf[o] = 0 as u8
37 return o
38}
39// "epidx:<persona>" into buf, NUL-terminated (per-persona enumeration index)
40func mem_idxkey(buf: *u8, persona: *u8) -> i64 {
41 var o: i64 = mem_catz(buf, 0, "epidx:" as *u8)
42 o = mem_catz(buf, o, persona)
43 buf[o] = 0 as u8
44 return o
45}
46
47// case-insensitive substring: 1 if needle occurs in hay[0,hlen)
48func mem_lc(c: u8) -> u8 { if c >= (65 as u8) { if c <= (90 as u8) { return (c + (32 as u8)) as u8 } } return c }
49func mem_ci_find(hay: *u8, hlen: i64, needle: *u8) -> i64 {
50 var nl: i64 = 0; while needle[nl] != (0 as u8) { nl = nl + 1 }
51 if nl == 0 { return 1 }
52 if hlen < nl { return 0 }
53 var i: i64 = 0
54 while i <= hlen - nl {
55 var m: i64 = 1; var c: i64 = 0
56 while c < nl { if mem_lc(hay[i+c]) != mem_lc(needle[c]) { m = 0; c = nl } else { c = c + 1 } }
57 if m == 1 { return 1 }
58 i = i + 1
59 }
60 return 0
61}
62
63// ---- count / append / get ----------------------------------------------------------------------------
64// number of episodes for this persona (== next seq, since we never delete)
65func mem_count_pfx(prefix: *u8, persona: *u8) -> i64 {
66 let ik: *u8 = sys_mmap(256); mem_idxkey(ik, persona)
67 // SIZE-TO-NEED: buffer derived from the index itself. The pair this replaces
68 // returned -1 once the episode index passed 1 MiB, and -1 skips the walk
69 // below -- a companion would have reported NO MEMORIES for exactly the
70 // personas it had talked to most.
71 let ibox: *i64 = sys_mmap(16) as *i64
72 let n: i64 = reg_index_read(prefix, ik, ibox)
73 let idxbuf: *u8 = ibox[0] as *u8
74 var cnt: i64 = 0; var i: i64 = 0
75 while i < n { if idxbuf[i] == (10 as u8) { cnt = cnt + 1 } i = i + 1 }
76 return cnt
77}
78
79// append one episode; returns its seq. seq derives from the current count (append-ordered, no separate counter).
80func mem_append_pfx(prefix: *u8, persona: *u8, session: *u8, ts: *u8, speaker: *u8, text: *u8) -> i64 {
81 let seq: i64 = mem_count_pfx(prefix, persona)
82 let id: *u8 = sys_mmap(256); mem_epi_id(id, persona, seq)
83 let ik: *u8 = sys_mmap(256); mem_idxkey(ik, persona)
84 let rec: *u8 = sys_mmap(EPI_MAGIC_8192); var o: i64 = 0
85 o = tr_cat(rec, o, session); o = tr_tab(rec, o)
86 o = tr_cat(rec, o, ts); o = tr_tab(rec, o)
87 o = tr_cat(rec, o, speaker); o = tr_tab(rec, o)
88 o = tr_cat(rec, o, text)
89 reg_put(prefix, EPI_KP, ik, id, rec, o)
90 return seq
91}
92
93func mem_get_pfx(prefix: *u8, persona: *u8, seq: i64, ptrout: *i64, lenout: *i64) -> i64 {
94 let id: *u8 = sys_mmap(256); mem_epi_id(id, persona, seq)
95 return reg_get(prefix, EPI_KP, id, ptrout, lenout)
96}
97
98// mem_search: LEXICAL recall -- scan every episode's TEXT field for `needle`; write matching seqs into
99// out_seqs (bounded by cap), return the match count. This is the plant-and-probe recall path (plant a fact
100// at turn 3, find it at turn 300). The semantic-paraphrase path is R1c.
101func mem_search_pfx(prefix: *u8, persona: *u8, needle: *u8, out_seqs: *i64, cap: i64) -> i64 {
102 let n: i64 = mem_count_pfx(prefix, persona)
103 let p: *i64 = sys_mmap(16) as *i64
104 let l: *i64 = sys_mmap(16) as *i64
105 let f: *i64 = sys_mmap(16) as *i64
106 var found: i64 = 0
107 var s: i64 = 0
108 while s < n {
109 if mem_get_pfx(prefix, persona, s, p, l) == 1 {
110 let rec: *u8 = p[0] as *u8
111 let rl: i64 = l[0]
112 if tr_field(rec, 0, rl, MF_TEXT, f) == 1 {
113 if mem_ci_find((rec as i64 + f[0]) as *u8, f[1], needle) == 1 {
114 if found < cap { out_seqs[found] = s }
115 found = found + 1
116 }
117 }
118 }
119 s = s + 1
120 }
121 return found
122}
123
124// production-prefix convenience wrappers
125func mem_count(persona: *u8) -> i64 { return mem_count_pfx(EPI_PREFIX, persona) }
126func mem_append(persona: *u8, session: *u8, ts: *u8, speaker: *u8, text: *u8) -> i64 { return mem_append_pfx(EPI_PREFIX, persona, session, ts, speaker, text) }
127func mem_get(persona: *u8, seq: i64, ptrout: *i64, lenout: *i64) -> i64 { return mem_get_pfx(EPI_PREFIX, persona, seq, ptrout, lenout) }
128func mem_search(persona: *u8, needle: *u8, out_seqs: *i64, cap: i64) -> i64 { return mem_search_pfx(EPI_PREFIX, persona, needle, out_seqs, cap) }