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:
admin
2026-05-05 14:39:33 +00:00
Unverified
parent d7107e162f
commit c63a1c03ae
3 changed files with 454 additions and 6 deletions

View File

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