nx_charmem.nx source
↩ module page · 490 lines · 19378 B
1// nx_charmem.nx -- SOVEREIGN CHARACTER MEMORY (the Nomi/Replika-class relationship state, in Nishi).
2// Operator 2026-08-05: "SQLite-backed character memory (affection/trust/drift/learned user
3// preferences), we want in nishi sota."
4//
5// DESIGN: EVENT-SOURCED, APPEND-ONLY (rule 13 -- history is sacred). The Python original mutates
6// SQLite rows; this organ never mutates anything: every interaction APPENDS one line to
7// knowledge/status/charmem_<char>.jrnl and current state is a FOLD over the journal at read time.
8// That buys what the mutable version cannot: full audit (why is affection 72? read the lines),
9// time-travel debugging, and no lost-update races between writers.
10//
11// event <char> <kind> <args...> append one event. kinds:
12// aff|trust|intim|excit <+/-N> relationship metric delta (fold clamps to 0..100)
13// mood <word> current mood (latest wins)
14// pref <key>=<value> learned user preference (latest value per key wins)
15// mem <imp 1-9> <text...> an important memory with importance weight
16// state <char> fold -> metrics, mood, counts (machine-parseable line)
17// ctx <char> fold -> the LLM-ready system-prompt block (top prefs+memories)
18// selftest <char> hermetic assertions on a throwaway fixture journal
19//
20// Char names sanitize to [a-z0-9_] max 32 (boundary defense: the name builds a path).
21// license_tier: ORIGINAL module: nishi-core.companion.charmem
22import "nx_syscalls.nx"
23import "nx_lane_conf.nx"
24
25const CM_BUF: i64 = 1048576
26const CM_LINE: i64 = 4096
27const CM_PREF_MAX: i64 = 64
28const CM_MEM_MAX: i64 = 256
29// absent-conf fallbacks only (rule 11) -- live values are knowledge/write_lane.conf rows.
30const CM_CTX_PREFS: i64 = 6
31const CM_CTX_MEMS: i64 = 6
32const CM_DEF_AFF: i64 = 50
33const CM_DEF_TRUST: i64 = 50
34const CM_DEF_INTIM: i64 = 30
35const CM_DEF_EXCIT: i64 = 50
36
37func cm_def(slot: i64) -> i64 {
38 if slot==0 { return lc_write_lane("charmem_default_affection" as *u8, CM_DEF_AFF) }
39 if slot==1 { return lc_write_lane("charmem_default_trust" as *u8, CM_DEF_TRUST) }
40 if slot==2 { return lc_write_lane("charmem_default_intimacy" as *u8, CM_DEF_INTIM) }
41 return lc_write_lane("charmem_default_excitement" as *u8, CM_DEF_EXCIT)
42}
43
44func cm_len(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
45func cm_w(s: *u8) -> i64 { sys_write(1, s, cm_len(s)); return 0 }
46func cm_cat(dst: *u8, off: i64, s: *u8) -> i64 { var i: i64=0; while s[i]!=(0 as u8){dst[off+i]=s[i];i=i+1} return off+i }
47func cm_u(dst: *u8, off: i64, v: i64) -> i64 {
48 var m: i64=v
49 if m<0 { dst[off]=45 as u8; return cm_u(dst, off+1, 0-m) }
50 let t: *u8=sys_mmap(28); var k: i64=0
51 if m==0 { t[0]=48 as u8; k=1 }
52 while m>0 { t[k]=(48+(m-(m/10)*10)) as u8; m=m/10; k=k+1 }
53 var o: i64=off; var i: i64=0
54 while i<k { dst[o]=t[k-1-i]; o=o+1; i=i+1 }
55 return o
56}
57func cm_eq(a: *u8, b: *u8) -> i64 {
58 var i: i64=0
59 while 1==1 {
60 if a[i]!=b[i] { return 0 }
61 if a[i]==(0 as u8) { return 1 }
62 i=i+1
63 }
64 return 0
65}
66// parse signed int from s at *pi, advance past digits; returns value (0 if none)
67func cm_int(s: *u8, pi: *i64) -> i64 {
68 var i: i64=pi[0]
69 var neg: i64=0
70 if s[i]==(45 as u8) { neg=1; i=i+1 }
71 if s[i]==(43 as u8) { i=i+1 }
72 var v: i64=0
73 var run: i64=1
74 while run==1 {
75 if s[i]<(48 as u8) { run=0 } else {
76 if s[i]>(57 as u8) { run=0 } else { v=v*10+((s[i] as i64)-48); i=i+1 }
77 }
78 }
79 pi[0]=i
80 if neg==1 { return 0-v }
81 return v
82}
83func cm_clamp(v: i64) -> i64 { if v<0 { return 0 } if v>100 { return 100 } return v }
84
85// advance idx past the current line's newline; STOPS (does not loop) on NUL. returns new idx.
86// u2605the third-time-banked law: a loop that signals termination by overwriting its cursor destroys
87// the position it exists to report -- this helper owns line-skip so no caller re-spells it wrong.
88func cm_eol(buf: *u8, idx: i64) -> i64 {
89 var i: i64=idx
90 var run: i64=1
91 while run==1 {
92 if buf[i]==(10 as u8) { run=0; i=i+1 } else {
93 if buf[i]==(0 as u8) { run=0 } else { i=i+1 }
94 }
95 }
96 return i
97}
98// copy payload from buf[idx] until newline/NUL into dst (cap chars), NUL-terminate. returns len.
99func cm_line_copy(buf: *u8, idx: i64, dst: *u8, cap: i64) -> i64 {
100 var i: i64=idx
101 var o: i64=0
102 var run: i64=1
103 while run==1 {
104 if buf[i]==(10 as u8) { run=0 } else {
105 if buf[i]==(0 as u8) { run=0 } else {
106 if o<cap { dst[o]=buf[i]; o=o+1 }
107 i=i+1
108 }
109 }
110 }
111 dst[o]=0 as u8
112 return o
113}
114
115// sanitize char name -> out ([a-z0-9_] max 32). returns len, 0 = invalid.
116func cm_sane(s: *u8, out: *u8) -> i64 {
117 var i: i64=0
118 while s[i]!=(0 as u8) {
119 if i>=32 { return 0 }
120 let ch: u8=s[i]
121 var ok: i64=0
122 if ch>=(97 as u8) { if ch<=(122 as u8) { ok=1 } }
123 if ch>=(48 as u8) { if ch<=(57 as u8) { ok=1 } }
124 if ch==(95 as u8) { ok=1 }
125 if ok==0 { return 0 }
126 out[i]=ch
127 i=i+1
128 }
129 out[i]=0 as u8
130 return i
131}
132
133func cm_path(char: *u8, out: *u8) -> i64 {
134 var o: i64=cm_cat(out, 0, "knowledge/status/charmem_" as *u8)
135 o=cm_cat(out, o, char)
136 o=cm_cat(out, o, ".jrnl" as *u8)
137 out[o]=0 as u8
138 return o
139}
140
141func cm_now() -> i64 { let ts: *i64=sys_mmap(16) as *i64; ts[0]=0; sys_clock_gettime_real(ts); return ts[0] }
142
143// known kind? 1=metric (aff/trust/intim/excit) 2=mood 3=pref 4=mem, 0=unknown
144func cm_kind(k: *u8) -> i64 {
145 if cm_eq(k, "aff" as *u8)==1 { return 1 }
146 if cm_eq(k, "trust" as *u8)==1 { return 1 }
147 if cm_eq(k, "intim" as *u8)==1 { return 1 }
148 if cm_eq(k, "excit" as *u8)==1 { return 1 }
149 if cm_eq(k, "mood" as *u8)==1 { return 2 }
150 if cm_eq(k, "pref" as *u8)==1 { return 3 }
151 if cm_eq(k, "mem" as *u8)==1 { return 4 }
152 return 0
153}
154
155// append one event line: <epoch>|<kind>|<joined args>\n
156func cm_event(char: *u8, argc: i64, argv: *i64) -> i64 {
157 let kind: *u8=argv[3] as *u8
158 if cm_kind(kind)==0 { cm_w("CHARMEM REFUSED: unknown kind (aff|trust|intim|excit|mood|pref|mem)\n" as *u8); return 3 }
159 if argc<5 { cm_w("CHARMEM REFUSED: event needs an argument after the kind\n" as *u8); return 3 }
160 let path: *u8=sys_mmap(256)
161 cm_path(char, path)
162 let line: *u8=sys_mmap(CM_LINE)
163 var o: i64=cm_u(line, 0, cm_now())
164 line[o]=124 as u8; o=o+1
165 o=cm_cat(line, o, kind)
166 line[o]=124 as u8; o=o+1
167 var ai: i64=4
168 while ai<argc {
169 if ai>4 { line[o]=32 as u8; o=o+1 }
170 var si: i64=0
171 let s: *u8=argv[ai] as *u8
172 while s[si]!=(0 as u8) {
173 var ch: u8=s[si]
174 // one event = one line, fields = pipes: strip both from payloads
175 if ch==(10 as u8) { ch=32 as u8 }
176 if ch==(124 as u8) { ch=47 as u8 }
177 if o<CM_LINE-2 { line[o]=ch; o=o+1 }
178 si=si+1
179 }
180 ai=ai+1
181 }
182 line[o]=10 as u8; o=o+1
183 let fd: i64=sys_openat_append(path, 420)
184 if fd<0 { cm_w("CHARMEM ERROR: cannot open journal for append\n" as *u8); return 4 }
185 sys_write(fd, line, o)
186 sys_close(fd)
187 cm_w("CHARMEM-EVENT OK char=" as *u8); cm_w(char)
188 cm_w(" appended\n" as *u8)
189 return 0
190}
191
192// fold: metrics m[0..3] (aff trust intim excit), m[4]=last epoch, m[5]=events; latest mood; buf
193// receives the whole journal (NUL-terminated). Returns journal bytes.
194func cm_fold(char: *u8, m: *i64, moodbuf: *u8, buf: *u8) -> i64 {
195 m[0]=cm_def(0); m[1]=cm_def(1); m[2]=cm_def(2); m[3]=cm_def(3); m[4]=0; m[5]=0
196 moodbuf[0]=0 as u8
197 let path: *u8=sys_mmap(256)
198 cm_path(char, path)
199 let fd: i64=sys_openat_rd(path)
200 if fd<0 { return 0 }
201 var total: i64=0
202 var r: i64=1
203 while r>0 {
204 r=sys_read(fd, ((buf as i64)+total) as *u8, CM_BUF-1-total)
205 if r>0 { total=total+r }
206 if total>=CM_BUF-1 { r=0 }
207 }
208 sys_close(fd)
209 buf[total]=0 as u8
210 let ip: *i64=sys_mmap(16) as *i64
211 let kb: *u8=sys_mmap(16)
212 var i: i64=0
213 while i<total {
214 ip[0]=i
215 let ep: i64=cm_int(buf, ip)
216 i=ip[0]
217 if buf[i]==(124 as u8) {
218 i=i+1
219 var ko: i64=0
220 var krun: i64=1
221 var kok: i64=1
222 while krun==1 {
223 if buf[i]==(124 as u8) { krun=0; i=i+1 } else {
224 if buf[i]==(10 as u8) { krun=0; kok=0 } else {
225 if buf[i]==(0 as u8) { krun=0; kok=0 } else {
226 if ko<12 { kb[ko]=buf[i]; ko=ko+1; i=i+1 } else { krun=0; kok=0 }
227 }
228 }
229 }
230 }
231 if kok==1 {
232 kb[ko]=0 as u8
233 m[4]=ep
234 m[5]=m[5]+1
235 let kt: i64=cm_kind(kb)
236 if kt==1 {
237 ip[0]=i
238 let dv: i64=cm_int(buf, ip)
239 var slot: i64=0
240 if cm_eq(kb, "trust" as *u8)==1 { slot=1 }
241 if cm_eq(kb, "intim" as *u8)==1 { slot=2 }
242 if cm_eq(kb, "excit" as *u8)==1 { slot=3 }
243 m[slot]=cm_clamp(m[slot]+dv)
244 }
245 if kt==2 { cm_line_copy(buf, i, moodbuf, 30) }
246 }
247 }
248 i=cm_eol(buf, i)
249 }
250 return total
251}
252
253func cm_state(char: *u8) -> i64 {
254 let m: *i64=sys_mmap(64) as *i64
255 let mood: *u8=sys_mmap(40)
256 let buf: *u8=sys_mmap(CM_BUF)
257 let total: i64=cm_fold(char, m, mood, buf)
258 let out: *u8=sys_mmap(CM_LINE)
259 var o: i64=cm_cat(out, 0, "CHARMEM-STATE char=" as *u8)
260 o=cm_cat(out, o, char)
261 o=cm_cat(out, o, " affection=" as *u8); o=cm_u(out, o, m[0])
262 o=cm_cat(out, o, " trust=" as *u8); o=cm_u(out, o, m[1])
263 o=cm_cat(out, o, " intimacy=" as *u8); o=cm_u(out, o, m[2])
264 o=cm_cat(out, o, " excitement=" as *u8); o=cm_u(out, o, m[3])
265 o=cm_cat(out, o, " mood=" as *u8)
266 if mood[0]==(0 as u8) { o=cm_cat(out, o, "neutral" as *u8) } else { o=cm_cat(out, o, mood) }
267 o=cm_cat(out, o, " events=" as *u8); o=cm_u(out, o, m[5])
268 o=cm_cat(out, o, " last_epoch=" as *u8); o=cm_u(out, o, m[4])
269 o=cm_cat(out, o, " journal_bytes=" as *u8); o=cm_u(out, o, total)
270 out[o]=10 as u8; o=o+1
271 sys_write(1, out, o)
272 return 0
273}
274
275// ctx: the LLM-ready block. prefs latest-wins by key; memories ranked importance desc.
276func cm_ctx(char: *u8) -> i64 {
277 let m: *i64=sys_mmap(64) as *i64
278 let mood: *u8=sys_mmap(40)
279 let buf: *u8=sys_mmap(CM_BUF)
280 cm_fold(char, m, mood, buf)
281 let pk: *i64=sys_mmap(CM_PREF_MAX*8) as *i64 // pref payload offsets (latest wins per key)
282 let pkey: *u8=sys_mmap(CM_PREF_MAX*64) // keys, 64 bytes each
283 var np: i64=0
284 let mm: *i64=sys_mmap(CM_MEM_MAX*8) as *i64 // mem text offsets
285 let mimp: *i64=sys_mmap(CM_MEM_MAX*8) as *i64 // importances
286 var nm: i64=0
287 let ip: *i64=sys_mmap(16) as *i64
288 let kb: *u8=sys_mmap(16)
289 var i: i64=0
290 while buf[i]!=(0 as u8) {
291 ip[0]=i
292 cm_int(buf, ip)
293 i=ip[0]
294 if buf[i]==(124 as u8) {
295 i=i+1
296 var ko: i64=0
297 var krun: i64=1
298 var kok: i64=1
299 while krun==1 {
300 if buf[i]==(124 as u8) { krun=0; i=i+1 } else {
301 if buf[i]==(10 as u8) { krun=0; kok=0 } else {
302 if buf[i]==(0 as u8) { krun=0; kok=0 } else {
303 if ko<12 { kb[ko]=buf[i]; ko=ko+1; i=i+1 } else { krun=0; kok=0 }
304 }
305 }
306 }
307 }
308 if kok==1 {
309 kb[ko]=0 as u8
310 if cm_eq(kb, "pref" as *u8)==1 {
311 // key = chars up to '='; a pref line without '=' is skipped
312 let keyb: *u8=sys_mmap(64)
313 var kyo: i64=0
314 var j: i64=i
315 var prun: i64=1
316 var pok: i64=0
317 while prun==1 {
318 if buf[j]==(61 as u8) { prun=0; pok=1 } else {
319 if buf[j]==(10 as u8) { prun=0 } else {
320 if buf[j]==(0 as u8) { prun=0 } else {
321 if kyo<48 { keyb[kyo]=buf[j]; kyo=kyo+1 }
322 j=j+1
323 }
324 }
325 }
326 }
327 keyb[kyo]=0 as u8
328 if pok==1 { if kyo>0 {
329 var found: i64=0-1
330 var q: i64=0
331 while q<np { if cm_eq(((pkey as i64)+q*64) as *u8, keyb)==1 { found=q; q=np } else { q=q+1 } }
332 if found>=0 { pk[found]=i } else {
333 if np<CM_PREF_MAX { cm_cat(((pkey as i64)+np*64) as *u8, 0, keyb); pk[np]=i; np=np+1 }
334 }
335 } }
336 }
337 if cm_eq(kb, "mem" as *u8)==1 {
338 ip[0]=i
339 let imp: i64=cm_int(buf, ip)
340 var j2: i64=ip[0]
341 if buf[j2]==(32 as u8) { j2=j2+1 }
342 if nm<CM_MEM_MAX { mm[nm]=j2; mimp[nm]=imp; nm=nm+1 }
343 }
344 }
345 }
346 i=cm_eol(buf, i)
347 }
348 let out: *u8=sys_mmap(CM_BUF)
349 let lb: *u8=sys_mmap(CM_LINE)
350 var o: i64=cm_cat(out, 0, "[RELATIONSHIP STATE] affection=" as *u8)
351 o=cm_u(out, o, m[0])
352 o=cm_cat(out, o, "/100 trust=" as *u8); o=cm_u(out, o, m[1])
353 o=cm_cat(out, o, "/100 intimacy=" as *u8); o=cm_u(out, o, m[2])
354 o=cm_cat(out, o, "/100 excitement=" as *u8); o=cm_u(out, o, m[3])
355 o=cm_cat(out, o, "/100 mood=" as *u8)
356 if mood[0]==(0 as u8) { o=cm_cat(out, o, "neutral" as *u8) } else { o=cm_cat(out, o, mood) }
357 o=cm_cat(out, o, "\n" as *u8)
358 if np>0 {
359 o=cm_cat(out, o, "[FACTS ABOUT THEM -- weave in naturally; when one is relevant use the EXACT detail below, never an invented variant]\n" as *u8)
360 var p2: i64=np-lc_write_lane("charmem_ctx_prefs" as *u8, CM_CTX_PREFS)
361 if p2<0 { p2=0 }
362 while p2<np {
363 o=cm_cat(out, o, "- " as *u8)
364 cm_line_copy(buf, pk[p2], lb, CM_LINE-2)
365 o=cm_cat(out, o, lb)
366 o=cm_cat(out, o, "\n" as *u8)
367 p2=p2+1
368 }
369 }
370 if nm>0 {
371 o=cm_cat(out, o, "[SHARED MEMORIES -- things you both lived; reference them with their real details]\n" as *u8)
372 let used: *i64=sys_mmap(CM_MEM_MAX*8) as *i64
373 let ctxmems: i64=lc_write_lane("charmem_ctx_mems" as *u8, CM_CTX_MEMS)
374 var emitted: i64=0
375 var erun: i64=1
376 while erun==1 {
377 if emitted>=ctxmems { erun=0 } else {
378 var best: i64=0-1
379 var bi: i64=0-1
380 var q2: i64=0
381 while q2<nm {
382 if used[q2]==0 { if mimp[q2]>=best { best=mimp[q2]; bi=q2 } }
383 q2=q2+1
384 }
385 if bi<0 { erun=0 } else {
386 used[bi]=1
387 o=cm_cat(out, o, "- " as *u8)
388 cm_line_copy(buf, mm[bi], lb, CM_LINE-2)
389 o=cm_cat(out, o, lb)
390 o=cm_cat(out, o, "\n" as *u8)
391 emitted=emitted+1
392 }
393 }
394 }
395 }
396 sys_write(1, out, o)
397 return 0
398}
399
400func cm_file_size(path: *u8) -> i64 {
401 let fd: i64=sys_openat_rd(path)
402 if fd<0 { return 0 }
403 let sz: i64=sys_lseek(fd, 0, 2)
404 sys_close(fd)
405 return sz
406}
407
408func cm_selftest(char: *u8) -> i64 {
409 // fresh fixture: truncate the journal (test fixture only -- live journals are never truncated)
410 let path: *u8=sys_mmap(256)
411 cm_path(char, path)
412 let tf: i64=sys_openat_wr(path, 420)
413 if tf>=0 { sys_close(tf) }
414 var pass: i64=0
415 var fail: i64=0
416 let m: *i64=sys_mmap(64) as *i64
417 let mood: *u8=sys_mmap(40)
418 let buf: *u8=sys_mmap(CM_BUF)
419 // u2605expectations DERIVED from the same conf the fold reads -- a gate pinned to literal 50/30
420 // would go RED the day the operator retunes a default, accusing a correct organ (a RED from an
421 // impossible expectation is a defect in the ruler).
422 // T1 defaults on empty journal
423 cm_fold(char, m, mood, buf)
424 if m[0]==cm_def(0) { if m[2]==cm_def(2) { pass=pass+1 } else { fail=fail+1 } } else { fail=fail+1 }
425 // T2 delta applies: aff +10
426 let av: *i64=sys_mmap(64) as *i64
427 av[3]="aff" as i64
428 av[4]="+10" as i64
429 cm_event(char, 5, av)
430 cm_fold(char, m, mood, buf)
431 let exp2: i64=cm_clamp(cm_def(0)+10)
432 if m[0]==exp2 { pass=pass+1 } else { fail=fail+1 }
433 // T3 clamp high: a +90 on top must clamp at 100, never exceed it
434 av[4]="+90" as i64
435 cm_event(char, 5, av)
436 cm_fold(char, m, mood, buf)
437 if m[0]==cm_clamp(exp2+90) { pass=pass+1 } else { fail=fail+1 }
438 // T4 clamp low: trust deep-negative must clamp at 0
439 av[3]="trust" as i64
440 av[4]="-999" as i64
441 cm_event(char, 5, av)
442 cm_fold(char, m, mood, buf)
443 if m[1]==0 { pass=pass+1 } else { fail=fail+1 }
444 // T5 NEGATIVE CONTROL: unknown kind refuses AND the journal does not grow
445 let szb: i64=cm_file_size(path)
446 av[3]="bogus" as i64
447 av[4]="1" as i64
448 let rc: i64=cm_event(char, 5, av)
449 let sza: i64=cm_file_size(path)
450 if rc==3 { if sza==szb { pass=pass+1 } else { fail=fail+1 } } else { fail=fail+1 }
451 // T6 append-only: accepted events strictly grow the journal
452 av[3]="mood" as i64
453 av[4]="playful" as i64
454 cm_event(char, 5, av)
455 if cm_file_size(path)>sza { pass=pass+1 } else { fail=fail+1 }
456 // T7 mood latest-wins through the fold
457 cm_fold(char, m, mood, buf)
458 if cm_eq(mood, "playful" as *u8)==1 { pass=pass+1 } else { fail=fail+1 }
459 // T8 NEGATIVE CONTROL: path-injection char name rejected by the sanitizer
460 let sb: *u8=sys_mmap(40)
461 if cm_sane("../evil" as *u8, sb)==0 { pass=pass+1 } else { fail=fail+1 }
462 cm_w("CHARMEM-SELFTEST pass=" as *u8)
463 let nb: *u8=sys_mmap(32); var nn: i64=cm_u(nb, 0, pass); sys_write(1, nb, nn)
464 cm_w(" fail=" as *u8)
465 nn=cm_u(nb, 0, fail); sys_write(1, nb, nn)
466 if fail==0 { cm_w(" verdict=GREEN\n" as *u8); return 0 }
467 cm_w(" verdict=RED\n" as *u8)
468 return 1
469}
470
471func main(argc: i64, argv: *i64) -> i64 {
472 if argc<3 {
473 cm_w("usage: nx_charmem {event <char> <kind> <args...> | state <char> | ctx <char> | selftest <char>}\n" as *u8)
474 cm_w(" kinds: aff|trust|intim|excit <+/-N> . mood <word> . pref <k>=<v> . mem <imp1-9> <text>\n" as *u8)
475 return 2
476 }
477 let verb: *u8=argv[1] as *u8
478 let raw: *u8=argv[2] as *u8
479 let char: *u8=sys_mmap(40)
480 if cm_sane(raw, char)==0 { cm_w("CHARMEM REFUSED: char name must be [a-z0-9_] max 32\n" as *u8); return 3 }
481 if cm_eq(verb, "event" as *u8)==1 {
482 if argc<4 { cm_w("CHARMEM REFUSED: event needs a kind\n" as *u8); return 3 }
483 return cm_event(char, argc, argv)
484 }
485 if cm_eq(verb, "state" as *u8)==1 { return cm_state(char) }
486 if cm_eq(verb, "ctx" as *u8)==1 { return cm_ctx(char) }
487 if cm_eq(verb, "selftest" as *u8)==1 { return cm_selftest(char) }
488 cm_w("CHARMEM REFUSED: unknown verb\n" as *u8)
489 return 2
490}