File size: 3,323 Bytes
8494d00 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | // 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 };
|