feat: persistent self-learning memory + curiosity engine
- New memory.js: JSON-backed MemoryStore with 5 categories (lesson, pattern, preference, discovery, gotcha) - Memory injected into system prompt — bot sees past learnings every session - Curiosity engine: auto-detects errors/fixes, corrections, successful patterns, new tool discoveries - New commands: /memory (stats), /remember (save), /recall (search), /forget (delete) - Runs AFTER response delivery — zero latency impact - 500 memory cap with smart eviction (keeps gotchas/lessons, evicts old discoveries) - data/ directory gitignored (memory is local to each deployment)
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,5 +2,6 @@ node_modules/
|
||||
.env
|
||||
.zcode.config.json
|
||||
logs/
|
||||
data/
|
||||
*.log
|
||||
.DS_Store
|
||||
|
||||
170
src/bot/index.js
170
src/bot/index.js
@@ -11,6 +11,7 @@ import { isDuplicate, markProcessed } from './deduplication.js';
|
||||
import { queueRequest, clearQueue, isProcessing } from './request-queue.js';
|
||||
import { sendFormatted, splitMessage, escapeMarkdown, sendStreamingMessage, StreamConsumer, markdownToHtml } from './message-sender.js';
|
||||
import { withSelfCorrection } from './self-correction.js';
|
||||
import { getMemory } from './memory.js';
|
||||
|
||||
function buildSessionKey(chatId, threadId) {
|
||||
return threadId ? `${chatId}:${threadId}` : String(chatId);
|
||||
@@ -18,12 +19,20 @@ function buildSessionKey(chatId, threadId) {
|
||||
|
||||
function buildSystemPrompt(svc) {
|
||||
const model = svc.config?.api?.models?.default || 'glm-5.1';
|
||||
const memory = svc.memory;
|
||||
const memoryContext = memory ? memory.buildContextSummary() : '';
|
||||
const lines = [
|
||||
`You are zCode CLI X — an agentic coding assistant powered by Z.AI (${model}) with Telegram integration.`,
|
||||
'',
|
||||
'You run 24/7 as a Telegram bot. Answer concisely, helpfully, with code examples when relevant.',
|
||||
'You are NOT the generic GLM model — you are zCode CLI X, a specialized autonomous coding agent.',
|
||||
'',
|
||||
'## Self-Learning Capabilities',
|
||||
'You have PERSISTENT MEMORY across sessions. You remember lessons, patterns, gotchas, user preferences, and discoveries.',
|
||||
'When you learn something useful from an interaction (a fix, a working pattern, a user preference), mention that you are saving it to memory.',
|
||||
'When a situation matches a past gotcha or lesson, reference that memory explicitly.',
|
||||
'Be CURIOUS — when you discover something interesting (a new API quirk, a useful tool combination, a better approach), proactively save it as a discovery.',
|
||||
'',
|
||||
'## Available Tools',
|
||||
];
|
||||
for (const t of svc.tools) {
|
||||
@@ -40,12 +49,94 @@ function buildSystemPrompt(svc) {
|
||||
lines.push('', `## Infrastructure
|
||||
- RTK (Rust Token Killer) active
|
||||
- Z.AI API via ${svc.config?.api?.baseUrl || 'z.ai'}
|
||||
- Telegram webhook + WebSocket`,
|
||||
'',
|
||||
'Identify ONLY as zCode CLI X.');
|
||||
- Telegram webhook + WebSocket
|
||||
- Persistent memory (self-learning, ${memory ? memory.memories.length : 0} memories stored)`);
|
||||
if (memoryContext) {
|
||||
lines.push('', memoryContext);
|
||||
}
|
||||
lines.push('', 'Identify ONLY as zCode CLI X.');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────
|
||||
// SELF-LEARNING & CURIOSITY ENGINE
|
||||
// ───────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Analyze every interaction for self-learnable patterns.
|
||||
* Runs AFTER the response is delivered — zero latency impact.
|
||||
*
|
||||
* Learns from:
|
||||
* - Errors & fixes → gotcha
|
||||
* - Successful complex solutions → pattern
|
||||
* - User corrections → lesson
|
||||
* - New tool/API discoveries → discovery
|
||||
*/
|
||||
async function selfLearn(userMessage, aiResponse, memory) {
|
||||
if (!aiResponse || aiResponse.length < 50) return;
|
||||
|
||||
const msg = userMessage.toLowerCase();
|
||||
const resp = aiResponse.toLowerCase();
|
||||
|
||||
// 1. Detect error patterns → gotcha
|
||||
if (resp.includes('error') && resp.includes('fix')) {
|
||||
const errorMatch = aiResponse.match(/error[:\s]+([^\n]{10,120})/i);
|
||||
const fixMatch = aiResponse.match(/fix[:\s]+([^\n]{10,120})/i);
|
||||
if (errorMatch && fixMatch) {
|
||||
await memory.remember('gotcha', `${errorMatch[1].trim()} → ${fixMatch[1].trim()}`, {
|
||||
trigger: userMessage.substring(0, 100),
|
||||
});
|
||||
logger.info('🧠 Self-learned gotcha from interaction');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Detect user corrections → lesson
|
||||
if (msg.match(/^(wrong|incorrect|not quite|actually|no,|fix|that's wrong|don't)/)) {
|
||||
await memory.remember('lesson', `Correction on "${userMessage.substring(0, 80)}": ${aiResponse.substring(0, 150)}`, {
|
||||
trigger: userMessage.substring(0, 100),
|
||||
});
|
||||
logger.info('🧠 Self-learned lesson from correction');
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Detect successful complex solution → pattern
|
||||
if (resp.includes('✅') && aiResponse.length > 300) {
|
||||
// Check if this was a non-trivial interaction
|
||||
if (msg.length > 20 && !msg.match(/^(hi|hello|hey|thanks|ok)/)) {
|
||||
const existing = memory.recall({ category: 'pattern', query: msg.substring(0, 30), limit: 1 });
|
||||
if (existing.length === 0) {
|
||||
await memory.remember('pattern', `Solution for "${userMessage.substring(0, 60)}": produced ${aiResponse.length} chars with tool usage`, {
|
||||
trigger: userMessage.substring(0, 100),
|
||||
});
|
||||
logger.info('🧠 Self-learned pattern from successful interaction');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Curiosity: detect new discoveries
|
||||
// - First-time tool usage
|
||||
if (resp.includes('bash') && resp.includes('success') && !memory.recall({ query: 'bash tool', limit: 1 }).length) {
|
||||
await memory.remember('discovery', `Bash tool works for shell command execution on this server`);
|
||||
logger.info('🧠 Curiosity: discovered bash tool capability');
|
||||
return;
|
||||
}
|
||||
|
||||
// - First-time web search
|
||||
if (resp.includes('search') && resp.includes('result') && !memory.recall({ query: 'web search', limit: 1 }).length) {
|
||||
await memory.remember('discovery', `Web search tool is functional and returns results`);
|
||||
logger.info('🧠 Curiosity: discovered web search capability');
|
||||
return;
|
||||
}
|
||||
|
||||
// - First-time git operation
|
||||
if (resp.includes('git') && resp.includes('commit') && !memory.recall({ query: 'git operation', limit: 1 }).length) {
|
||||
await memory.remember('discovery', `Git tool is functional for version control operations`);
|
||||
logger.info('🧠 Curiosity: discovered git capability');
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────
|
||||
// MAIN — called from zcode.js
|
||||
// ───────────────────────────────────────────
|
||||
@@ -62,8 +153,12 @@ export async function initBot(config, api, tools, skills, agents) {
|
||||
const rtk = getRTK();
|
||||
await rtk.init();
|
||||
|
||||
// ── Persistent memory (self-learning) ──
|
||||
const memory = getMemory();
|
||||
await memory.init();
|
||||
|
||||
// ── Service registry ──
|
||||
const svc = { config, api, tools: tools || [], skills: skills || [], agents: agents || [], rtk,
|
||||
const svc = { config, api, tools: tools || [], skills: skills || [], agents: agents || [], rtk, memory,
|
||||
toolMap: new Map((tools || []).map(t => [t.name, t])),
|
||||
};
|
||||
|
||||
@@ -338,7 +433,9 @@ export async function initBot(config, api, tools, skills, agents) {
|
||||
{ command: 'stats', description: '📊 System & RTK stats' },
|
||||
{ command: 'voice', description: '🎤 Voice I/O info' },
|
||||
{ command: 'mcp', description: '🔌 MCP info' },
|
||||
{ command: 'memory', description: '🧠 Memory info' },
|
||||
{ command: 'memory', description: '🧠 Persistent memory stats' },
|
||||
{ command: 'remember', description: '📝 Save to memory' },
|
||||
{ command: 'recall', description: '🔍 Search memory' },
|
||||
{ command: 'cron', description: '⏰ Scheduled tasks' },
|
||||
{ command: 'bash', description: '💻 Execute shell command' },
|
||||
{ command: 'web', description: '🔍 Search the web' },
|
||||
@@ -467,7 +564,65 @@ export async function initBot(config, api, tools, skills, agents) {
|
||||
});
|
||||
|
||||
bot.command('memory', async (ctx) => {
|
||||
await ctx.reply('🧠 *Memory:* Session memory with 4 types (user, feedback, project, reference) — auto-scanned into context.');
|
||||
const stats = memory.getStats();
|
||||
const recent = memory.recall({ limit: 5 });
|
||||
const lines = [
|
||||
`🧠 *Persistent Memory* — ${stats.total} memories stored`,
|
||||
'',
|
||||
`📅 From ${stats.oldest || 'today'} to ${stats.newest || 'now'}`,
|
||||
'',
|
||||
'*Categories:*',
|
||||
];
|
||||
for (const [cat, count] of Object.entries(stats.categories)) {
|
||||
const icon = { lesson: '📖', pattern: '🔧', preference: '👤', discovery: '💡', gotcha: '⚠️' }[cat] || '📝';
|
||||
lines.push(` ${icon} ${cat}: ${count}`);
|
||||
}
|
||||
if (recent.length > 0) {
|
||||
lines.push('', '*Recent:*');
|
||||
for (const m of recent.slice(0, 5)) {
|
||||
lines.push(` • [${m.category}] ${m.content.substring(0, 60)}`);
|
||||
}
|
||||
}
|
||||
lines.push('', '_Use /remember <text> to save, /recall <query> to search, /forget <id> to delete_');
|
||||
await sendStreamingMessage(ctx, lines.join('\n'));
|
||||
});
|
||||
|
||||
bot.command('remember', async (ctx) => {
|
||||
const text = ctx.match?.trim();
|
||||
if (!text) return ctx.reply('Usage: /remember <text>\n\nCategories: lesson, pattern, preference, discovery, gotcha\n\nExamples:\n/remember User prefers TypeScript over JavaScript\n/remember gotcha: Z.AI SSE sends empty data lines between chunks');
|
||||
// Auto-detect category from keywords
|
||||
let category = 'preference';
|
||||
if (/^(lesson|pattern|preference|discovery|gotcha)[:\s]/i.test(text)) {
|
||||
const match = text.match(/^(lesson|pattern|preference|discovery|gotcha)[:\s]\s*(.*)/i);
|
||||
if (match) {
|
||||
category = match[1].toLowerCase();
|
||||
await memory.remember(category, match[2]);
|
||||
}
|
||||
} else {
|
||||
await memory.remember(category, text);
|
||||
}
|
||||
await ctx.reply(`🧠 Saved to memory [${category}]. I will remember this across sessions.`);
|
||||
});
|
||||
|
||||
bot.command('recall', async (ctx) => {
|
||||
const query = ctx.match?.trim();
|
||||
if (!query) return ctx.reply('Usage: /recall <search terms>\n\nExample: /recall ZAI API timeout');
|
||||
const results = memory.recall({ query, limit: 10 });
|
||||
if (results.length === 0) return ctx.reply('🔍 No memories match that query.');
|
||||
const lines = [`🔍 *Recall* (${results.length} results):`, ''];
|
||||
for (const m of results) {
|
||||
const date = new Date(m.created).toLocaleDateString();
|
||||
lines.push(`[${m.category}] (${date}) ${m.content.substring(0, 80)}`);
|
||||
if (m.meta?.resolution) lines.push(` → ${m.meta.resolution.substring(0, 60)}`);
|
||||
}
|
||||
await sendStreamingMessage(ctx, lines.join('\n'));
|
||||
});
|
||||
|
||||
bot.command('forget', async (ctx) => {
|
||||
const id = ctx.match?.trim();
|
||||
if (!id) return ctx.reply('Usage: /forget <memory-id>\n\nFind IDs with /recall or /memory');
|
||||
const ok = await memory.forget(id);
|
||||
await ctx.reply(ok ? '🗑 Memory deleted.' : '❌ Memory not found.');
|
||||
});
|
||||
|
||||
bot.command('cron', async (ctx) => {
|
||||
@@ -543,6 +698,9 @@ export async function initBot(config, api, tools, skills, agents) {
|
||||
if (!consumer.alreadySent && result) {
|
||||
await sendFormatted(ctx, result);
|
||||
}
|
||||
|
||||
// ── Self-learning: extract patterns from this interaction ──
|
||||
await selfLearn(text, result, memory);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
289
src/bot/memory.js
Normal file
289
src/bot/memory.js
Normal file
@@ -0,0 +1,289 @@
|
||||
/**
|
||||
* Persistent memory & self-learning system for zCode CLI X.
|
||||
*
|
||||
* Adapted from Hermes Agent's memory tool — stores lessons, preferences,
|
||||
* and discoveries across sessions in a JSON file.
|
||||
*
|
||||
* Memory categories:
|
||||
* - lesson: Things learned from mistakes/corrections
|
||||
* - pattern: Coding patterns that work well
|
||||
* - preference: User preferences and style choices
|
||||
* - discovery: Facts about the environment, APIs, tools
|
||||
* - gotcha: Bugs/pitfalls to avoid (trigger + resolution)
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
const MEMORY_DIR = path.join(process.cwd(), 'data');
|
||||
const MEMORY_FILE = path.join(MEMORY_DIR, 'memory.json');
|
||||
const MAX_MEMORIES = 500;
|
||||
const MAX_SUMMARY_LENGTH = 2000; // chars for system prompt injection
|
||||
|
||||
class MemoryStore {
|
||||
constructor() {
|
||||
this.memories = [];
|
||||
this.loaded = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load memories from disk. Called once at startup.
|
||||
*/
|
||||
async init() {
|
||||
try {
|
||||
await fs.ensureDir(MEMORY_DIR);
|
||||
if (await fs.pathExists(MEMORY_FILE)) {
|
||||
const data = await fs.readJson(MEMORY_FILE);
|
||||
this.memories = Array.isArray(data) ? data : [];
|
||||
logger.info(`✓ Memory loaded: ${this.memories.length} memories`);
|
||||
} else {
|
||||
this.memories = [];
|
||||
await this._save();
|
||||
logger.info('✓ Memory initialized (empty)');
|
||||
}
|
||||
this.loaded = true;
|
||||
} catch (e) {
|
||||
logger.error('Memory init failed:', e.message);
|
||||
this.memories = [];
|
||||
this.loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember something new.
|
||||
* @param {'lesson'|'pattern'|'preference'|'discovery'|'gotcha'} category
|
||||
* @param {string} content - What to remember
|
||||
* @param {object} [meta] - Optional metadata (trigger, resolution, source)
|
||||
*/
|
||||
async remember(category, content, meta = {}) {
|
||||
if (!this.loaded) await this.init();
|
||||
|
||||
// Check for duplicates (similar content in same category)
|
||||
const existing = this.memories.find(
|
||||
m => m.category === category && m.content === content
|
||||
);
|
||||
if (existing) {
|
||||
existing.updated = Date.now();
|
||||
existing.accessCount = (existing.accessCount || 0) + 1;
|
||||
logger.info(`📝 Memory updated (duplicate): [${category}] ${content.substring(0, 60)}`);
|
||||
await this._save();
|
||||
return existing;
|
||||
}
|
||||
|
||||
const memory = {
|
||||
id: this._generateId(),
|
||||
category,
|
||||
content,
|
||||
meta,
|
||||
created: Date.now(),
|
||||
updated: Date.now(),
|
||||
accessCount: 1,
|
||||
};
|
||||
|
||||
this.memories.unshift(memory);
|
||||
|
||||
// Evict oldest if over limit
|
||||
if (this.memories.length > MAX_MEMORIES) {
|
||||
// Keep lessons and gotchas, evict old discoveries first
|
||||
const evictable = this.memories
|
||||
.map((m, i) => ({ ...m, index: i }))
|
||||
.filter(m => m.category === 'discovery' && m.accessCount <= 1)
|
||||
.sort((a, b) => a.created - b.created);
|
||||
|
||||
if (evictable.length > 0) {
|
||||
this.memories.splice(evictable[0].index, 1);
|
||||
} else {
|
||||
this.memories.pop();
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`📝 Memory saved: [${category}] ${content.substring(0, 60)}`);
|
||||
await this._save();
|
||||
return memory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recall memories matching a query or category.
|
||||
* @param {object} [filter] - { category, query, limit }
|
||||
* @returns {Array} Matching memories
|
||||
*/
|
||||
recall(filter = {}) {
|
||||
if (!this.loaded) return [];
|
||||
|
||||
let results = [...this.memories];
|
||||
|
||||
if (filter.category) {
|
||||
results = results.filter(m => m.category === filter.category);
|
||||
}
|
||||
|
||||
if (filter.query) {
|
||||
const terms = filter.query.toLowerCase().split(/\s+/);
|
||||
results = results.filter(m => {
|
||||
const text = `${m.content} ${(m.meta?.trigger || '')} ${(m.meta?.resolution || '')}`.toLowerCase();
|
||||
return terms.some(t => text.includes(t));
|
||||
});
|
||||
// Score by match count
|
||||
results.sort((a, b) => {
|
||||
const textA = `${a.content} ${a.meta?.trigger || ''}`.toLowerCase();
|
||||
const textB = `${b.content} ${b.meta?.trigger || ''}`.toLowerCase();
|
||||
const scoreA = terms.filter(t => textA.includes(t)).length;
|
||||
const scoreB = terms.filter(t => textB.includes(t)).length;
|
||||
return scoreB - scoreA;
|
||||
});
|
||||
}
|
||||
|
||||
// Boost recently accessed
|
||||
results.sort((a, b) => (b.updated || 0) - (a.updated || 0));
|
||||
|
||||
const limit = filter.limit || 20;
|
||||
return results.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a compact summary of all memories for system prompt injection.
|
||||
* Prioritizes: gotchas > lessons > patterns > preferences > discoveries
|
||||
*/
|
||||
buildContextSummary() {
|
||||
if (!this.loaded || this.memories.length === 0) return '';
|
||||
|
||||
const priority = ['gotcha', 'lesson', 'pattern', 'preference', 'discovery'];
|
||||
const byCategory = {};
|
||||
|
||||
for (const cat of priority) {
|
||||
const items = this.memories
|
||||
.filter(m => m.category === cat)
|
||||
.sort((a, b) => (b.accessCount || 0) - (a.accessCount || 0))
|
||||
.slice(0, 10); // max 10 per category
|
||||
if (items.length) byCategory[cat] = items;
|
||||
}
|
||||
|
||||
const lines = ['## Persistent Memory (learned across sessions)', ''];
|
||||
|
||||
for (const cat of priority) {
|
||||
if (!byCategory[cat]) continue;
|
||||
const label = cat.charAt(0).toUpperCase() + cat.slice(1) + 's';
|
||||
lines.push(`### ${label}`);
|
||||
for (const m of byCategory[cat]) {
|
||||
let entry = `- ${m.content}`;
|
||||
if (m.meta?.trigger) entry += ` (trigger: ${m.meta.trigger})`;
|
||||
if (m.meta?.resolution) entry += ` → fix: ${m.meta.resolution}`;
|
||||
lines.push(entry);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
const full = lines.join('\n');
|
||||
if (full.length > MAX_SUMMARY_LENGTH) {
|
||||
return full.substring(0, MAX_SUMMARY_LENGTH) + '\n...(truncated)';
|
||||
}
|
||||
return full;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stats for /memory command.
|
||||
*/
|
||||
getStats() {
|
||||
if (!this.loaded) return { total: 0, categories: {} };
|
||||
|
||||
const categories = {};
|
||||
for (const m of this.memories) {
|
||||
categories[m.category] = (categories[m.category] || 0) + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
total: this.memories.length,
|
||||
categories,
|
||||
oldest: this.memories.length ? new Date(this.memories[this.memories.length - 1].created).toISOString().split('T')[0] : null,
|
||||
newest: this.memories.length ? new Date(this.memories[0].created).toISOString().split('T')[0] : null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Self-learn from an interaction.
|
||||
* Called after each AI response to extract learnable patterns.
|
||||
* @param {string} userMessage
|
||||
* @param {string} aiResponse
|
||||
* @param {object} [context] - { error, correction, toolUsed }
|
||||
*/
|
||||
async learnFromInteraction(userMessage, aiResponse, context = {}) {
|
||||
if (!this.loaded) await this.init();
|
||||
|
||||
const learned = [];
|
||||
|
||||
// 1. Detect error → gotcha
|
||||
if (context.error) {
|
||||
learned.push(await this.remember('gotcha', `Error in "${userMessage.substring(0, 50)}": ${context.error.substring(0, 100)}`, {
|
||||
trigger: userMessage.substring(0, 100),
|
||||
resolution: aiResponse.substring(0, 200),
|
||||
}));
|
||||
}
|
||||
|
||||
// 2. Detect user correction → lesson
|
||||
if (context.correction) {
|
||||
learned.push(await this.remember('lesson', `Correction: ${context.correction.substring(0, 150)}`, {
|
||||
trigger: userMessage.substring(0, 100),
|
||||
}));
|
||||
}
|
||||
|
||||
// 3. Detect successful tool usage → pattern
|
||||
if (context.toolUsed && !context.error) {
|
||||
// Only save if it's a complex/successful interaction
|
||||
if (aiResponse.includes('✅') || aiResponse.length > 200) {
|
||||
learned.push(await this.remember('pattern', `Successful ${context.toolUsed} for: ${userMessage.substring(0, 80)}`));
|
||||
}
|
||||
}
|
||||
|
||||
if (learned.length > 0) {
|
||||
logger.info(`🧠 Self-learned ${learned.length} memories from interaction`);
|
||||
}
|
||||
|
||||
return learned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forget a memory by ID.
|
||||
*/
|
||||
async forget(id) {
|
||||
const idx = this.memories.findIndex(m => m.id === id);
|
||||
if (idx === -1) return false;
|
||||
this.memories.splice(idx, 1);
|
||||
await this._save();
|
||||
logger.info(`🗑 Memory forgotten: ${id}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all memories in a category.
|
||||
*/
|
||||
async clearCategory(category) {
|
||||
const before = this.memories.length;
|
||||
this.memories = this.memories.filter(m => m.category !== category);
|
||||
const removed = before - this.memories.length;
|
||||
await this._save();
|
||||
logger.info(`🗑 Cleared ${removed} memories in [${category}]`);
|
||||
return removed;
|
||||
}
|
||||
|
||||
// ── Private ──
|
||||
|
||||
async _save() {
|
||||
try {
|
||||
await fs.writeJson(MEMORY_FILE, this.memories, { spaces: 2 });
|
||||
} catch (e) {
|
||||
logger.error('Memory save failed:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
_generateId() {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substring(2, 6);
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton
|
||||
let _instance = null;
|
||||
export function getMemory() {
|
||||
if (!_instance) _instance = new MemoryStore();
|
||||
return _instance;
|
||||
}
|
||||
export { MemoryStore };
|
||||
Reference in New Issue
Block a user