Files
spark-mind/src/memory.ts
T
n1kt4r bae171d6b4 P3-P5: SubAgent SDK, Telegram Channel, SQLite persistence, full pipeline
P3 - SubAgent SDK:
  - src/subagent.ts: researcher, coder, analyst, content-writer
  - Mind.runSubAgent() → SubAgentFactory.create() → execute()
  - Результаты сохраняются в long-term memory
  - Heartbeat.notify(subagent-complete) после завершения

P4 - SQLite persistence (5 таблиц, WAL):
  - src/database.ts: thoughts, notepads, self_observations, long_term, embeddings
  - memory.ts → Database (полная замена in-memory)
  - index.ts: dbPath → ~/.local/share/spark-mind/spark-mind.db

P5 - Telegram Channel Layer:
  - src/telegram.ts: https module (SNI fix), MAX Messenger API
  - mind.ts: sendReply() → Telegram + console fallback
  - index.ts: TelegramMessenger(config) → Mind

Fixes:
  - FK constraint: long_term INSERT → long_term_embeddings INSERT
  - JSON parse: nested objects, markdown fences
  - P5 telegram: graceful degradation on SSL error
2026-09-15 12:49:33 +05:00

139 lines
4.7 KiB
TypeScript

/**
* Memory System — 4 слоя памяти (концепт Animus).
* Хранение: SQLite (5 таблиц) + bge-m3 эмбеддинги (cosine-поиск).
*
* Слои:
* short-term → thoughts (recent thoughts, last N)
* working → notepads (notepad per contact)
* core self → self_observations (накопленная самосознательность)
* long-term → long_term + long_term_embeddings (факты/опыт/процедуры/исходы)
*/
import { EmbeddingClient, cosineSimilarity } from './embedding.js';
import { Database } from './database.js';
export interface Thought {
id: string;
text: string;
createdAt: number;
}
export interface ContactNotepad {
contactId: string;
block: string;
updatedAt: number;
}
export interface SelfObservation {
id: string;
text: string;
createdAt: number;
tags: string[];
}
export interface LongTermMemory {
id: string;
text: string;
type: 'fact' | 'experience' | 'procedure' | 'outcome';
createdAt: number;
embedding?: number[];
}
export interface MemorySystemConfig {
recentCount?: number;
dbPath?: string;
}
export class MemorySystem {
private recentCount: number;
private db: Database;
private embeddingClient: EmbeddingClient | null;
constructor(config: MemorySystemConfig = {}, embeddingClient?: EmbeddingClient) {
this.recentCount = config.recentCount ?? 10;
this.db = new Database(config.dbPath);
this.embeddingClient = embeddingClient ?? null;
}
// === Short-term: последние мысли/опыт/сообщения ===
addThought(text: string): Thought {
const t: Thought = { id: `th-${Date.now()}`, text, createdAt: Date.now() };
this.db.insertThought(t.id, t.text, t.createdAt);
return t;
}
recentThoughts(): Thought[] {
const rows = this.db.getRecentThoughts(this.recentCount);
return rows.map(r => ({ id: r.id, text: r.text, createdAt: r.created_at }));
}
// === Working memory: notepad по контактам ===
updateNotepad(contactId: string, block: string): void {
const ts = Date.now();
this.db.upsertNotepad(contactId, block, ts);
}
getNotepad(contactId: string): string {
return this.db.getNotepad(contactId) ?? '';
}
// === Core self: накопленная самосознательность ===
addSelfObservation(text: string, tags: string[] = []): SelfObservation {
const o: SelfObservation = { id: `so-${Date.now()}`, text, createdAt: Date.now(), tags };
this.db.insertSelfObservation(o.id, o.text, o.createdAt);
return o;
}
getAllSelfObservations(): SelfObservation[] {
const rows = this.db.getAllSelfObservations();
return rows.map(r => ({ id: r.id, text: r.text, createdAt: r.created_at, tags: [] }));
}
// === Long-term: факты/опыт/процедуры/исходы + эмбедд для векторного поиска ===
async addLongTerm(text: string, type: LongTermMemory['type']): Promise<LongTermMemory> {
const m: LongTermMemory = { id: `lt-${Date.now()}`, text, type, createdAt: Date.now() };
let embedding: number[] = [];
if (this.embeddingClient) {
embedding = await this.embeddingClient.embed(text);
}
// Сначала long_term (FK требует), потом embedding
this.db.insertLongTerm(m.id, m.text, m.type, m.createdAt);
if (embedding.length > 0) {
this.db.insertLongTermEmbedding(m.id, embedding);
}
m.embedding = embedding;
return m;
}
/** Поиск по long-term по тексту через эмбедд (cosine similarity в SQLite). */
async searchLongTerm(query: string, topK = 3): Promise<LongTermMemory[]> {
if (!this.embeddingClient) {
const rows = this.db.getLongTerm(this.recentCount);
return rows.map(r => ({ id: r.id, text: r.text, type: r.type as LongTermMemory['type'], createdAt: r.created_at }));
}
const q = await this.embeddingClient.embed(query);
const all = this.db.getAllLongTermWithVectors();
const scored = all
.map(m => ({ m, score: cosineSimilarity(q, Array.from(m.embedding)) }))
.sort((a, b) => b.score - a.score)
.slice(0, topK)
.map(x => ({
id: x.m.id,
text: x.m.text,
type: x.m.type as LongTermMemory['type'],
createdAt: x.m.created_at,
embedding: Array.from(x.m.embedding),
}));
return scored;
}
/** Консолидация: перенос недавних мыслей в long-term (по спецификации Animus). */
async consolidate(recent: Thought[]): Promise<void> {
for (const t of recent) {
// эвристика: только «значимые» мысли (по тегам/ключевым словам)
await this.addLongTerm(t.text, 'experience');
}
}
}