Project Files
src / memory / MemoryService.ts
const STOPWORDS = new Set([
"about", "after", "again", "also", "another", "answer", "because", "before",
"between", "could", "detail", "details", "discuss", "explain", "from", "have",
"how", "into", "more", "other", "please", "question", "should", "show", "some",
"that", "their", "there", "these", "this", "those", "what", "when", "where",
"which", "with", "would", "your",
]);
export interface MemoryEntry {
topic: string;
summary: string;
timestamp: number;
}
const MAX_TOPIC_CACHE = 256;
const MAX_ENTRIES_PER_TOPIC = 20;
const MAX_RETRIEVE_ENTRIES = 5;
const TOPIC_CACHE_PRUNE_THRESHOLD = 1000;
export class MemoryService {
private static memory = new Map<string, MemoryEntry[]>();
private static topicCache = new Map<string, string>();
private static cacheOps = 0;
public static save(topic: string, summary: string): void {
const cleanedTopic = this.normalizeTopic(topic);
if (!cleanedTopic) return;
let entries = this.memory.get(cleanedTopic);
if (!entries) {
entries = [];
this.memory.set(cleanedTopic, entries);
}
entries.push({ topic: cleanedTopic, summary, timestamp: Date.now() });
if (entries.length > MAX_ENTRIES_PER_TOPIC) {
this.memory.set(cleanedTopic, entries.slice(-MAX_ENTRIES_PER_TOPIC));
}
}
public static retrieve(topic: string): string {
const cleanedTopic = this.normalizeTopic(topic);
const entries = this.memory.get(cleanedTopic);
if (!entries?.length) return "";
const last = entries.slice(-MAX_RETRIEVE_ENTRIES);
let result = "";
for (let i = 0; i < last.length; i++) {
if (i > 0) result += "\n";
result += `- ${last[i].summary}`;
}
return result;
}
public static extractTopic(prompt: string): string {
const cached = this.topicCache.get(prompt);
if (cached !== undefined) return cached;
let word = "";
let start = 0;
const words: string[] = [];
while (start < prompt.length) {
const match = /\W/.exec(prompt[start]);
if (match) {
word = prompt.substring(start, start + match.index).toLowerCase();
if (word.length > 3 && !STOPWORDS.has(word)) {
words.push(word);
}
start += match.index + 1;
} else {
word = prompt.substring(start).toLowerCase();
if (word.length > 3 && !STOPWORDS.has(word)) {
words.push(word);
}
break;
}
}
const topic = words.slice(0, 6).join(" ");
this.topicCache.set(prompt, topic);
this.cacheOps++;
if (this.cacheOps > TOPIC_CACHE_PRUNE_THRESHOLD && this.topicCache.size > MAX_TOPIC_CACHE) {
const toRemove = this.topicCache.size - MAX_TOPIC_CACHE;
let removed = 0;
for (const key of this.topicCache.keys()) {
if (removed++ >= toRemove) break;
this.topicCache.delete(key);
}
this.cacheOps = 0;
}
return topic;
}
private static normalizeTopic(topic: string): string {
return topic.toLowerCase().replace(/\s+/g, " ").trim().slice(0, 80);
}
}