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 };