// relmem.js — RELATIONSHIP MEMORY: let bounded minds remember each other. // R50 found the entities are strangers who only know the user — zero lived memory // of each other. This is the additive, reversible layer that grows acquaintance: // after a room, each entity's turns are saved as ITS memories of the others; // next room, those memories are injected back as recallable fragments. An entity // only ever stores/recalls ITS OWN composed turns (already bounded to its // corpus) — so the bound holds. NEVER touches the source corpus. Delete the // cache file to forget. A society that accumulates shared history. 'use strict'; const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); function relmemFile(cacheDir, selfDir, otherName) { const h = crypto.createHash('sha1').update(path.resolve(selfDir)).digest('hex').slice(0, 10); const o = otherName.toLowerCase().replace(/[^a-z0-9]/g, ''); return path.join(cacheDir, `relmem-${h}-${o}.jsonl`); } // append this entity's turns (its memories of talking with `otherName`) function saveTurns(cacheDir, selfDir, otherName, texts) { const f = relmemFile(cacheDir, selfDir, otherName); const lines = texts.filter(t => t && t.trim()).map(t => JSON.stringify({ t: t.trim() })); if (lines.length) fs.appendFileSync(f, lines.join('\n') + '\n'); return f; } function loadTurns(cacheDir, selfDir, otherName) { const f = relmemFile(cacheDir, selfDir, otherName); if (!fs.existsSync(f)) return []; return fs.readFileSync(f, 'utf8').trim().split('\n').filter(Boolean).map(l => { try { return JSON.parse(l).t; } catch (_) { return null; } }).filter(Boolean); } function splitSentences(t) { return (t.split(/(?<=[.!?…])\s+|\n+/).map(s => s.trim()).filter(s => s.split(/\s+/).length >= 4 && s.split(/\s+/).length <= 45)); } // inject prior-conversation memories into the live store+emb as recallable // fragments. Returns { emb, injectedIdx:Set } — the new fragment indices, to be // name-boosted (they ARE the memories of the roommate). Async: embeds new frags. async function injectInto(store, emb, texts, embedAsync, srcTag) { const seen = new Set(); const newFrags = []; for (const t of texts) for (const sent of splitSentences(t)) { const key = sent.toLowerCase().replace(/[^a-z0-9 ]/g, '').replace(/\s+/g, ' ').trim(); if (seen.has(key)) continue; seen.add(key); newFrags.push({ text: sent, posTag: 'body', sentenceInitial: true, tier: 0, src: srcTag || 'relmem', prompt: null, promptIdx: -1, nativePos: 0.5 }); } if (!newFrags.length) return { emb, injectedIdx: new Set() }; const d = emb.d, n0 = emb.n, k = newFrags.length; const vecs = []; for (const f of newFrags) vecs.push(await embedAsync(f.text)); const merged = new Float32Array((n0 + k) * d); merged.set(emb.vectors, 0); for (let i = 0; i < k; i++) { const v = vecs[i]; if (v) for (let j = 0; j < d; j++) merged[(n0 + i) * d + j] = v[j]; } const injectedIdx = new Set(); for (let i = 0; i < k; i++) { store.fragments.push(newFrags[i]); injectedIdx.add(n0 + i); for (const w of (newFrags[i].text.toLowerCase().match(/[a-z0-9']+/g) || [])) store.vocab.add(w); } return { emb: { vectors: merged, n: n0 + k, d }, injectedIdx }; } module.exports = { relmemFile, saveTurns, loadTurns, injectInto };