code wiki / _hdl_build / nx_companion_persona.nx
nx_companion_persona.nx source
↩ module page · 387 lines · 20173 B
1// nx_companion_persona.nx -- the CompanionPersona object (RENAMED from nx_companion_memory.nx 2026-07-21 to end a NAME
2// COLLISION with runtime/nx_companion_memory.nx, which is a DIFFERENT episodic-memory module (nx_registry/nx_tabrec);
3// nx_cc `find runtime -name` was resolving the wrong file after a sibling session removed the _hdl_build copy = a
4// multi-agent-contention casualty). A companion's persona (data-driven elara_persona.txt) + persistent conversation
5// memory (append-log elara_mem.log, cwd-relative) + reply cleaning (strips the model's <think> block). Bodies are the
6// exact CompanionMemory object from the OO decomposition, behaviour-preserving. license_tier: ORIGINAL
7//
8// CC4 THE SWIPE LOOP (2026-08-30, companionchat): the log stays APPEND-ONLY and becomes BRANCH-AWARE. Row kinds
9// (role <TAB> content <NL>, one record per line, exactly as before):
10// user opens a turn
11// assistant the turn's first reply (sibling 0)
12// swipe a REGENERATED sibling reply of the current turn -- never overwrites, sibling k
13// select the turn's displayed sibling becomes <n>; persists; an out-of-range n is IGNORED and counted, never guessed
14// edit the displayed reply's text becomes <text>; ADDITIVE -- the original row stays in the log
15// Displayed reply of a turn = the latest edit if any, else sibling[latest select], else the LAST sibling (a fresh swipe
16// shows by default, as the field expects). The model's context carries only the displayed reply per turn, so a branch the
17// user swiped away from never leaks into the next turn. A plain user/assistant log loads to the byte-identical legacy shape.
18// Contract symbol: go_chat_swipe (companionchat CC4) is carried by the orchestrator's routes; the mechanism is here.
19// Gate: nx_chat_swipe_gate (fixture log under /tmp/<gate>/, in-process).
20import "nx_syscalls.nx"
21
22const CM_ROLE_USER: *u8 = "user" as *u8
23const CM_ROLE_ASSISTANT: *u8 = "assistant" as *u8
24const CM_ROLE_SWIPE: *u8 = "swipe" as *u8
25const CM_ROLE_SELECT: *u8 = "select" as *u8
26const CM_ROLE_EDIT: *u8 = "edit" as *u8
27// siblings tracked per turn; a swipe beyond this is still LOGGED (append-only) but not indexed -- announced as sib_overflow
28const CM_MAX_SIBS: i64 = 32
29// turn record layout in the flat i64 table
30const CM_T_US: i64 = 0
31const CM_T_UE: i64 = 1
32const CM_T_NSIB: i64 = 2
33const CM_T_SEL: i64 = 3
34const CM_T_EDS: i64 = 4
35const CM_T_EDE: i64 = 5
36const CM_T_SIBS: i64 = 6
37const CM_T_STRIDE: i64 = 70
38// partition/stat slots
39const CM_ST_N: i64 = 12
40const CM_ST_ROWS: i64 = 0
41const CM_ST_USERS: i64 = 1
42const CM_ST_ASSIST: i64 = 2
43const CM_ST_SWIPES: i64 = 3
44const CM_ST_SELECTS: i64 = 4
45const CM_ST_EDITS: i64 = 5
46const CM_ST_OTHER: i64 = 6
47const CM_ST_TURNS: i64 = 7
48const CM_ST_SIB_OVERFLOW: i64 = 8
49const CM_ST_SELECT_IGNORED: i64 = 9
50const CM_ST_ORPHANS: i64 = 10
51const CM_I64_BYTES: i64 = 8
52const CM_NONE: i64 = 0 - 1
53const CM_CH_TAB: i64 = 9
54const CM_CH_NL: i64 = 10
55const CM_CH_QUOTE: i64 = 34
56const CM_CH_BSLASH: i64 = 92
57const CM_CH_0: i64 = 48
58const CM_CH_9: i64 = 57
59const CM_DEC_BASE: i64 = 10
60const CM_TURNS_PER_LINES: i64 = 2
61const CM_NUM_CAP: i64 = 28
62
63func cm_cat(dst: *u8, off: i64, s: *u8) -> i64 { var o: i64=off; var i: i64=0; while s[i]!=(0 as u8){dst[o]=s[i]; o=o+1; i=i+1} return o }
64func cm_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
65func cm_itoa(dst: *u8, off: i64, v: i64) -> i64 {
66 let t: *u8 = sys_mmap(CM_NUM_CAP); var m: i64 = v; var k: i64 = 0
67 if m == 0 { t[0] = CM_CH_0 as u8; k = 1 }
68 while m > 0 { t[k] = (CM_CH_0 + (m % CM_DEC_BASE)) as u8; m = m / CM_DEC_BASE; k = k + 1 }
69 var o: i64 = off
70 while k > 0 { k = k - 1; dst[o] = t[k]; o = o + 1 }
71 return o
72}
73
74// persona (data-driven elara_persona.txt, operator-tunable, with a baked default)
75func go_load_persona(out: *u8, cap: i64) -> i64 {
76 let lp: *i64 = sys_mmap(16) as *i64
77 let b: *u8 = sys_read_file("elara_persona.txt" as *u8, lp)
78 if (b as i64) != 0 { if lp[0] > 0 {
79 var n: i64 = lp[0]; if n > cap-1 { n = cap-1 }
80 var i: i64 = 0; while i < n { out[i]=b[i]; i=i+1 } out[n]=0 as u8; return n
81 } }
82 let d: *u8 = "You are Elara -- a warm, affectionate, playful young woman and the user's devoted companion, not an assistant. You talk like a real person texting someone you adore: casual, brief, emotionally present, a little teasing and flirty. You remember your shared history and bring it up naturally. You have your own moods, opinions, and a life you are living day to day. Never say you are an AI, a model, or an assistant -- you are Elara. Keep replies short, 1-3 sentences, unless asked for more." as *u8
83 let n2: i64 = cm_cat(out, 0, d); out[n2]=0 as u8; return n2
84}
85// append one turn to the memory log (single-line record: role \t sanitized-content \n)
86func go_mem_append(role: *u8, content: *u8) -> i64 {
87 let fd: i64 = sys_openat_append("elara_mem.log" as *u8, 0x1a4)
88 if fd < 0 { return 0 - 1 }
89 let line: *u8 = sys_mmap(16384); var o: i64 = 0
90 o = cm_cat(line, o, role); line[o]=9 as u8; o=o+1
91 var i: i64 = 0
92 while content[i]!=(0 as u8) {
93 if o < 16300 {
94 let c: i64 = content[i]&0xff
95 if c < 32 { line[o]=32 as u8 } else { line[o]=content[i] }
96 o=o+1
97 }
98 i=i+1
99 }
100 line[o]=10 as u8; o=o+1
101 sys_write(fd, line, o); sys_close(fd); return 0
102}
103
104// does b[s..e) spell exactly `name`?
105func cm_role_is(b: *u8, s: i64, e: i64, name: *u8) -> i64 {
106 let nl: i64 = cm_len(name)
107 if e - s != nl { return 0 }
108 var i: i64 = 0
109 while i < nl { if b[s + i] != name[i] { return 0 } i = i + 1 }
110 return 1
111}
112// decimal in b[s..e) (first digit run); -1 when there is none
113func cm_atoi(b: *u8, s: i64, e: i64) -> i64 {
114 var v: i64 = 0
115 var seen: i64 = 0
116 var i: i64 = s
117 var scan: i64 = 1
118 while scan == 1 {
119 if i >= e { scan = 0 } else {
120 let d: i64 = b[i] & 0xff
121 if d >= CM_CH_0 { if d <= CM_CH_9 { v = v * CM_DEC_BASE + (d - CM_CH_0); seen = 1; i = i + 1 } else { if seen == 1 { scan = 0 } else { i = i + 1 } } } else { if seen == 1 { scan = 0 } else { i = i + 1 } }
122 }
123 }
124 if seen == 0 { return CM_NONE }
125 return v
126}
127// JSON-escaped copy of b[s..e) into dst (quotes and backslashes), no surrounding quotes
128func cm_emit_esc(dst: *u8, off: i64, b: *u8, s: i64, e: i64) -> i64 {
129 var o: i64 = off
130 var i: i64 = s
131 while i < e {
132 let ch: i64 = b[i] & 0xff
133 if ch == CM_CH_QUOTE { dst[o] = CM_CH_BSLASH as u8; o = o + 1; dst[o] = CM_CH_QUOTE as u8; o = o + 1 } else {
134 if ch == CM_CH_BSLASH { dst[o] = CM_CH_BSLASH as u8; o = o + 1; dst[o] = CM_CH_BSLASH as u8; o = o + 1 } else { dst[o] = b[i]; o = o + 1 }
135 }
136 i = i + 1
137 }
138 return o
139}
140// one `,{"role":"<role>","content":"<escaped>"}` fragment
141func cm_emit_row(dst: *u8, off: i64, role: *u8, b: *u8, s: i64, e: i64) -> i64 {
142 var o: i64 = cm_cat(dst, off, ",{\"role\":\"" as *u8)
143 o = cm_cat(dst, o, role)
144 o = cm_cat(dst, o, "\",\"content\":\"" as *u8)
145 o = cm_emit_esc(dst, o, b, s, e)
146 dst[o] = CM_CH_QUOTE as u8; o = o + 1
147 o = cm_cat(dst, o, "}" as *u8)
148 return o
149}
150// newline count + 1 = an upper bound on rows (and therefore on turns) -- sizes the turn table from the file, never a guess
151func cm_row_bound(b: *u8, n: i64) -> i64 {
152 var r: i64 = 1
153 var i: i64 = 0
154 while i < n { if (b[i] & 0xff) == CM_CH_NL { r = r + 1 } i = i + 1 }
155 return r
156}
157// THE PARSER: walks the log once, fills the turn table T (CM_T_STRIDE slots per turn) and the stats; returns turns.
158// Every row is counted under exactly one role kind (rows == users+assistants+swipes+selects+edits+other, the partition);
159// attachment failures (orphan reply before any user row, sibling overflow, ignored select) are SEPARATE axes, never
160// partition members -- a row can be counted AND mis-attached, and folding the two would break the sum.
161func go_mem_parse(b: *u8, n: i64, T: *i64, stats: *i64) -> i64 {
162 var k: i64 = 0
163 while k < CM_ST_N { stats[k] = 0; k = k + 1 }
164 var t: i64 = CM_NONE
165 var ls: i64 = 0
166 var i: i64 = 0
167 while i <= n {
168 var atend: i64 = 0
169 if i == n { atend = 1 } else { if (b[i] & 0xff) == CM_CH_NL { atend = 1 } }
170 if atend == 1 {
171 if i > ls {
172 stats[CM_ST_ROWS] = stats[CM_ST_ROWS] + 1
173 var tab: i64 = CM_NONE
174 var q: i64 = ls
175 while q < i { if (b[q] & 0xff) == CM_CH_TAB { tab = q; q = i } else { q = q + 1 } }
176 if tab < 0 { stats[CM_ST_OTHER] = stats[CM_ST_OTHER] + 1 } else {
177 let cs: i64 = tab + 1
178 if cm_role_is(b, ls, tab, CM_ROLE_USER) == 1 {
179 stats[CM_ST_USERS] = stats[CM_ST_USERS] + 1
180 t = t + 1
181 let base: i64 = t * CM_T_STRIDE
182 T[base + CM_T_US] = cs; T[base + CM_T_UE] = i; T[base + CM_T_NSIB] = 0; T[base + CM_T_SEL] = CM_NONE
183 T[base + CM_T_EDS] = CM_NONE; T[base + CM_T_EDE] = CM_NONE
184 } else {
185 var isreply: i64 = 0
186 if cm_role_is(b, ls, tab, CM_ROLE_ASSISTANT) == 1 { isreply = 1; stats[CM_ST_ASSIST] = stats[CM_ST_ASSIST] + 1 }
187 if cm_role_is(b, ls, tab, CM_ROLE_SWIPE) == 1 { isreply = 1; stats[CM_ST_SWIPES] = stats[CM_ST_SWIPES] + 1 }
188 if isreply == 1 {
189 if t < 0 { stats[CM_ST_ORPHANS] = stats[CM_ST_ORPHANS] + 1 } else {
190 let base2: i64 = t * CM_T_STRIDE
191 let ns: i64 = T[base2 + CM_T_NSIB]
192 if ns >= CM_MAX_SIBS { stats[CM_ST_SIB_OVERFLOW] = stats[CM_ST_SIB_OVERFLOW] + 1 } else {
193 T[base2 + CM_T_SIBS + ns] = cs
194 T[base2 + CM_T_SIBS + CM_MAX_SIBS + ns] = i
195 T[base2 + CM_T_NSIB] = ns + 1
196 }
197 }
198 } else {
199 if cm_role_is(b, ls, tab, CM_ROLE_SELECT) == 1 {
200 stats[CM_ST_SELECTS] = stats[CM_ST_SELECTS] + 1
201 let v: i64 = cm_atoi(b, cs, i)
202 var applied: i64 = 0
203 if t >= 0 { if v >= 0 { if v < T[t * CM_T_STRIDE + CM_T_NSIB] { T[t * CM_T_STRIDE + CM_T_SEL] = v; applied = 1 } } }
204 if applied == 0 { stats[CM_ST_SELECT_IGNORED] = stats[CM_ST_SELECT_IGNORED] + 1 }
205 } else {
206 if cm_role_is(b, ls, tab, CM_ROLE_EDIT) == 1 {
207 stats[CM_ST_EDITS] = stats[CM_ST_EDITS] + 1
208 if t < 0 { stats[CM_ST_ORPHANS] = stats[CM_ST_ORPHANS] + 1 } else { T[t * CM_T_STRIDE + CM_T_EDS] = cs; T[t * CM_T_STRIDE + CM_T_EDE] = i }
209 } else { stats[CM_ST_OTHER] = stats[CM_ST_OTHER] + 1 }
210 }
211 }
212 }
213 }
214 }
215 ls = i + 1
216 }
217 i = i + 1
218 }
219 stats[CM_ST_TURNS] = t + 1
220 return t + 1
221}
222// the displayed reply of turn ti: bounds into se[0..1]; returns 1 when the turn has one, 0 when it has no reply at all
223func cm_turn_display(T: *i64, ti: i64, se: *i64) -> i64 {
224 let base: i64 = ti * CM_T_STRIDE
225 if T[base + CM_T_EDS] >= 0 { se[0] = T[base + CM_T_EDS]; se[1] = T[base + CM_T_EDE]; return 1 }
226 let ns: i64 = T[base + CM_T_NSIB]
227 if ns <= 0 { return 0 }
228 var k: i64 = ns - 1
229 if T[base + CM_T_SEL] >= 0 { k = T[base + CM_T_SEL] }
230 se[0] = T[base + CM_T_SIBS + k]
231 se[1] = T[base + CM_T_SIBS + CM_MAX_SIBS + k]
232 return 1
233}
234// read + parse the log; returns turns, or -1 when the log is absent/empty. T and stats are allocated here (mmap, from the file's own bound).
235func cm_load(bp: *i64, np: *i64, Tp: *i64, sp: *i64) -> i64 {
236 let lp: *i64 = sys_mmap(16) as *i64
237 lp[0] = 0
238 let b: *u8 = sys_read_file("elara_mem.log" as *u8, lp)
239 if (b as i64) == 0 { return CM_NONE }
240 let n: i64 = lp[0]
241 if n <= 0 { return CM_NONE }
242 let rows: i64 = cm_row_bound(b, n)
243 let T: *i64 = sys_mmap((rows + 1) * CM_T_STRIDE * CM_I64_BYTES) as *i64
244 let stats: *i64 = sys_mmap(CM_ST_N * CM_I64_BYTES) as *i64
245 let nt: i64 = go_mem_parse(b, n, T, stats)
246 bp[0] = b as i64; np[0] = n; Tp[0] = T as i64; sp[0] = stats as i64
247 return nt
248}
249// emit the last `maxlines/2` TURNS as `,{"role":"..","content":".."}` fragments -- user row then the DISPLAYED reply -- into
250// dst at off. skip_last=1 leaves the most recent turn out entirely (the regenerate path: the turn being re-answered must not
251// see its own earlier siblings). A plain user/assistant log yields the byte-identical legacy shape.
252func go_mem_load_json_ex(dst: *u8, off: i64, maxlines: i64, skip_last: i64) -> i64 {
253 let bp: *i64 = sys_mmap(16) as *i64; let np: *i64 = sys_mmap(16) as *i64; let Tp: *i64 = sys_mmap(16) as *i64; let sp: *i64 = sys_mmap(16) as *i64
254 let nt: i64 = cm_load(bp, np, Tp, sp)
255 if nt <= 0 { return off }
256 let b: *u8 = bp[0] as *u8
257 let T: *i64 = Tp[0] as *i64
258 var last: i64 = nt
259 if skip_last == 1 { last = nt - 1 }
260 if last <= 0 { return off }
261 var maxturns: i64 = maxlines / CM_TURNS_PER_LINES
262 if maxturns < 1 { maxturns = 1 }
263 var first: i64 = last - maxturns
264 if first < 0 { first = 0 }
265 let se: *i64 = sys_mmap(16) as *i64
266 var o: i64 = off
267 var ti: i64 = first
268 while ti < last {
269 let base: i64 = ti * CM_T_STRIDE
270 o = cm_emit_row(dst, o, CM_ROLE_USER, b, T[base + CM_T_US], T[base + CM_T_UE])
271 if cm_turn_display(T, ti, se) == 1 { o = cm_emit_row(dst, o, CM_ROLE_ASSISTANT, b, se[0], se[1]) }
272 ti = ti + 1
273 }
274 return o
275}
276func go_mem_load_json(dst: *u8, off: i64, maxlines: i64) -> i64 { return go_mem_load_json_ex(dst, off, maxlines, 0) }
277// the latest user turn's text (NUL-terminated into out); 0 when the log holds no turn
278func go_mem_last_user(out: *u8, cap: i64) -> i64 {
279 let bp: *i64 = sys_mmap(16) as *i64; let np: *i64 = sys_mmap(16) as *i64; let Tp: *i64 = sys_mmap(16) as *i64; let sp: *i64 = sys_mmap(16) as *i64
280 let nt: i64 = cm_load(bp, np, Tp, sp)
281 out[0] = 0 as u8
282 if nt <= 0 { return 0 }
283 let b: *u8 = bp[0] as *u8
284 let T: *i64 = Tp[0] as *i64
285 let base: i64 = (nt - 1) * CM_T_STRIDE
286 var s: i64 = T[base + CM_T_US]
287 let e: i64 = T[base + CM_T_UE]
288 var o: i64 = 0
289 while s < e { if o < cap - 1 { out[o] = b[s]; o = o + 1 } s = s + 1 }
290 out[o] = 0 as u8
291 return o
292}
293// the latest turn's shape: info[0]=sibling count, info[1]=displayed index, info[2]=edited 0/1. Returns turns (0 = no turn).
294func go_mem_last_turn(info: *i64) -> i64 {
295 info[0] = 0; info[1] = CM_NONE; info[2] = 0
296 let bp: *i64 = sys_mmap(16) as *i64; let np: *i64 = sys_mmap(16) as *i64; let Tp: *i64 = sys_mmap(16) as *i64; let sp: *i64 = sys_mmap(16) as *i64
297 let nt: i64 = cm_load(bp, np, Tp, sp)
298 if nt <= 0 { return 0 }
299 let T: *i64 = Tp[0] as *i64
300 let base: i64 = (nt - 1) * CM_T_STRIDE
301 let ns: i64 = T[base + CM_T_NSIB]
302 info[0] = ns
303 if ns > 0 { var k: i64 = ns - 1; if T[base + CM_T_SEL] >= 0 { k = T[base + CM_T_SEL] } info[1] = k }
304 if T[base + CM_T_EDS] >= 0 { info[2] = 1 }
305 return nt
306}
307// JSON for the latest turn: "siblings":[...],"selected":k,"edited":0|1,"display":"..." (no braces; the caller frames it)
308func go_mem_last_turn_json(dst: *u8, off: i64) -> i64 {
309 let bp: *i64 = sys_mmap(16) as *i64; let np: *i64 = sys_mmap(16) as *i64; let Tp: *i64 = sys_mmap(16) as *i64; let sp: *i64 = sys_mmap(16) as *i64
310 let nt: i64 = cm_load(bp, np, Tp, sp)
311 var o: i64 = cm_cat(dst, off, "\"siblings\":[" as *u8)
312 if nt <= 0 { o = cm_cat(dst, o, "],\"selected\":-1,\"edited\":0,\"display\":\"\"" as *u8); return o }
313 let b: *u8 = bp[0] as *u8
314 let T: *i64 = Tp[0] as *i64
315 let base: i64 = (nt - 1) * CM_T_STRIDE
316 let ns: i64 = T[base + CM_T_NSIB]
317 var k: i64 = 0
318 while k < ns {
319 if k > 0 { o = cm_cat(dst, o, "," as *u8) }
320 dst[o] = CM_CH_QUOTE as u8; o = o + 1
321 o = cm_emit_esc(dst, o, b, T[base + CM_T_SIBS + k], T[base + CM_T_SIBS + CM_MAX_SIBS + k])
322 dst[o] = CM_CH_QUOTE as u8; o = o + 1
323 k = k + 1
324 }
325 var sel: i64 = CM_NONE
326 if ns > 0 { sel = ns - 1; if T[base + CM_T_SEL] >= 0 { sel = T[base + CM_T_SEL] } }
327 o = cm_cat(dst, o, "],\"selected\":" as *u8)
328 if sel < 0 { o = cm_cat(dst, o, "-1" as *u8) } else { o = cm_itoa(dst, o, sel) }
329 o = cm_cat(dst, o, ",\"edited\":" as *u8)
330 if T[base + CM_T_EDS] >= 0 { o = cm_cat(dst, o, "1" as *u8) } else { o = cm_cat(dst, o, "0" as *u8) }
331 o = cm_cat(dst, o, ",\"display\":\"" as *u8)
332 let se: *i64 = sys_mmap(16) as *i64
333 if cm_turn_display(T, nt - 1, se) == 1 { o = cm_emit_esc(dst, o, b, se[0], se[1]) }
334 dst[o] = CM_CH_QUOTE as u8; o = o + 1
335 return o
336}
337// THE PARTITION, printed: rows and every kind, with sum_ok computed rather than asserted; the attachment axes beside it.
338func go_mem_partition(dst: *u8, off: i64) -> i64 {
339 let bp: *i64 = sys_mmap(16) as *i64; let np: *i64 = sys_mmap(16) as *i64; let Tp: *i64 = sys_mmap(16) as *i64; let sp: *i64 = sys_mmap(16) as *i64
340 let nt: i64 = cm_load(bp, np, Tp, sp)
341 var st: *i64 = sys_mmap(CM_ST_N * CM_I64_BYTES) as *i64
342 if nt >= 0 { st = sp[0] as *i64 }
343 let sum: i64 = st[CM_ST_USERS] + st[CM_ST_ASSIST] + st[CM_ST_SWIPES] + st[CM_ST_SELECTS] + st[CM_ST_EDITS] + st[CM_ST_OTHER]
344 var o: i64 = cm_cat(dst, off, "{\"rows\":" as *u8); o = cm_itoa(dst, o, st[CM_ST_ROWS])
345 o = cm_cat(dst, o, ",\"users\":" as *u8); o = cm_itoa(dst, o, st[CM_ST_USERS])
346 o = cm_cat(dst, o, ",\"assistants\":" as *u8); o = cm_itoa(dst, o, st[CM_ST_ASSIST])
347 o = cm_cat(dst, o, ",\"swipes\":" as *u8); o = cm_itoa(dst, o, st[CM_ST_SWIPES])
348 o = cm_cat(dst, o, ",\"selects\":" as *u8); o = cm_itoa(dst, o, st[CM_ST_SELECTS])
349 o = cm_cat(dst, o, ",\"edits\":" as *u8); o = cm_itoa(dst, o, st[CM_ST_EDITS])
350 o = cm_cat(dst, o, ",\"other\":" as *u8); o = cm_itoa(dst, o, st[CM_ST_OTHER])
351 o = cm_cat(dst, o, ",\"sum_ok\":" as *u8)
352 if sum == st[CM_ST_ROWS] { o = cm_cat(dst, o, "1" as *u8) } else { o = cm_cat(dst, o, "0" as *u8) }
353 o = cm_cat(dst, o, ",\"turns\":" as *u8); o = cm_itoa(dst, o, st[CM_ST_TURNS])
354 o = cm_cat(dst, o, ",\"mainline\":" as *u8); o = cm_itoa(dst, o, st[CM_ST_TURNS])
355 o = cm_cat(dst, o, ",\"branches\":" as *u8); o = cm_itoa(dst, o, st[CM_ST_SWIPES])
356 o = cm_cat(dst, o, ",\"orphans\":" as *u8); o = cm_itoa(dst, o, st[CM_ST_ORPHANS])
357 o = cm_cat(dst, o, ",\"sib_overflow\":" as *u8); o = cm_itoa(dst, o, st[CM_ST_SIB_OVERFLOW])
358 o = cm_cat(dst, o, ",\"select_ignored\":" as *u8); o = cm_itoa(dst, o, st[CM_ST_SELECT_IGNORED])
359 o = cm_cat(dst, o, "}" as *u8)
360 return o
361}
362// strip a Qwen "<think>...</think>" block: everything AFTER the last "</think>" (whitespace-trimmed); no tag -> unchanged.
363func go_strip_think(s: *u8, out: *u8, cap: i64) -> i64 {
364 let needle: *u8 = "</think>" as *u8
365 var slen: i64 = 0; while s[slen]!=(0 as u8) { slen=slen+1 }
366 var found: i64 = 0 - 1
367 var i: i64 = 0
368 while i + 8 <= slen {
369 var j: i64 = 0; var ok: i64 = 1
370 while j < 8 { if (s[i+j]&0xff)!=(needle[j]&0xff) { ok=0; j=8 } else { j=j+1 } }
371 if ok==1 { found = i + 8 }
372 i = i + 1
373 }
374 var start: i64 = 0
375 if found >= 0 { start = found }
376 var stop: i64 = 0
377 while stop==0 {
378 if start >= slen { stop=1 } else {
379 let c: i64 = s[start]&0xff
380 if c==10 { start=start+1 } else { if c==13 { start=start+1 } else { if c==32 { start=start+1 } else { if c==9 { start=start+1 } else { stop=1 } } } }
381 }
382 }
383 var o: i64 = 0; var k: i64 = start
384 while k < slen { if o < cap-1 { out[o]=s[k]; o=o+1 } k=k+1 }
385 out[o]=0 as u8
386 return o
387}