@kernel.chat/kbot 3.62.0 → 3.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,66 @@
1
+ export interface DreamInsight {
2
+ /** Unique ID */
3
+ id: string;
4
+ /** The consolidated insight */
5
+ content: string;
6
+ /** Category: pattern | preference | skill | project | relationship */
7
+ category: DreamCategory;
8
+ /** Keywords for retrieval */
9
+ keywords: string[];
10
+ /** Relevance score (0-1), decays over time */
11
+ relevance: number;
12
+ /** How many sessions contributed to this insight */
13
+ sessions: number;
14
+ /** Created timestamp */
15
+ created: string;
16
+ /** Last reinforced (refreshed relevance) */
17
+ lastReinforced: string;
18
+ /** Source: which sessions/topics generated this */
19
+ source: string;
20
+ }
21
+ export type DreamCategory = 'pattern' | 'preference' | 'skill' | 'project' | 'relationship';
22
+ export interface DreamState {
23
+ /** Total dream cycles completed */
24
+ cycles: number;
25
+ /** Last dream timestamp */
26
+ lastDream: string | null;
27
+ /** Total insights ever created */
28
+ totalInsights: number;
29
+ /** Total insights archived (aged out) */
30
+ totalArchived: number;
31
+ /** Insights currently active */
32
+ activeInsights: number;
33
+ /** Last session turn count that was dreamed about */
34
+ lastSessionTurns: number;
35
+ }
36
+ /** Apply exponential decay to all insights based on time elapsed */
37
+ export declare function ageMemories(insights: DreamInsight[]): {
38
+ aged: DreamInsight[];
39
+ archived: DreamInsight[];
40
+ };
41
+ /** Run a full dream cycle — consolidate, reinforce, age */
42
+ export declare function dream(sessionId?: string): Promise<DreamResult>;
43
+ export interface DreamResult {
44
+ success: boolean;
45
+ newInsights: number;
46
+ reinforced: number;
47
+ archived: number;
48
+ cycle: number;
49
+ duration: number;
50
+ error: string | null;
51
+ }
52
+ /** Get dream insights for inclusion in system prompt */
53
+ export declare function getDreamPrompt(maxInsights?: number): string;
54
+ /** Get full dream status */
55
+ export declare function getDreamStatus(): {
56
+ state: DreamState;
57
+ insights: DreamInsight[];
58
+ archiveCount: number;
59
+ };
60
+ /** Search dream insights by keyword */
61
+ export declare function searchDreams(query: string): DreamInsight[];
62
+ /** Manually reinforce a specific insight (user confirms it's still relevant) */
63
+ export declare function reinforceInsight(insightId: string): boolean;
64
+ /** Run dream after session ends (non-blocking) */
65
+ export declare function dreamAfterSession(sessionId?: string): void;
66
+ //# sourceMappingURL=dream.d.ts.map
package/dist/dream.js ADDED
@@ -0,0 +1,377 @@
1
+ // kbot Dream Engine — In-Process Memory Consolidation
2
+ //
3
+ // Inspired by Claude Code's autoDream system, but built kbot's way:
4
+ // - Uses local Ollama models ($0 cost) instead of cloud API
5
+ // - Runs post-session or on-demand via `kbot dream`
6
+ // - Ages memories with exponential decay scoring
7
+ // - Extracts cross-session insights ("dreams")
8
+ // - Produces a dream journal that feeds back into system prompts
9
+ //
10
+ // The metaphor: after a session, kbot "sleeps" — consolidating short-term
11
+ // session history into long-term durable insights, just like biological memory.
12
+ //
13
+ // Storage: ~/.kbot/memory/dreams/
14
+ // - journal.json — consolidated insights (the "dream journal")
15
+ // - state.json — last dream timestamp, cycle count, stats
16
+ // - archive/ — old dreams that aged out (kept for archaeology)
17
+ import { homedir } from 'node:os';
18
+ import { join } from 'node:path';
19
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs';
20
+ import { getHistory } from './memory.js';
21
+ import { loadMemory } from './memory.js';
22
+ // ── Constants ──
23
+ const DREAM_DIR = join(homedir(), '.kbot', 'memory', 'dreams');
24
+ const JOURNAL_FILE = join(DREAM_DIR, 'journal.json');
25
+ const STATE_FILE = join(DREAM_DIR, 'state.json');
26
+ const ARCHIVE_DIR = join(DREAM_DIR, 'archive');
27
+ const MAX_INSIGHTS = 100; // Keep top 100 insights
28
+ const DECAY_RATE = 0.03; // ~3% relevance loss per day
29
+ const MIN_RELEVANCE = 0.15; // Below this → archive
30
+ const CONSOLIDATION_MODEL = 'kernel:latest'; // Local Ollama model
31
+ const OLLAMA_URL = 'http://localhost:11434';
32
+ const OLLAMA_TIMEOUT = 60_000; // 60s per generation
33
+ // ── Helpers ──
34
+ function ensureDirs() {
35
+ for (const dir of [DREAM_DIR, ARCHIVE_DIR]) {
36
+ if (!existsSync(dir))
37
+ mkdirSync(dir, { recursive: true });
38
+ }
39
+ }
40
+ function loadJournal() {
41
+ ensureDirs();
42
+ if (!existsSync(JOURNAL_FILE))
43
+ return [];
44
+ try {
45
+ return JSON.parse(readFileSync(JOURNAL_FILE, 'utf-8'));
46
+ }
47
+ catch {
48
+ return [];
49
+ }
50
+ }
51
+ function saveJournal(insights) {
52
+ ensureDirs();
53
+ writeFileSync(JOURNAL_FILE, JSON.stringify(insights, null, 2));
54
+ }
55
+ function loadState() {
56
+ ensureDirs();
57
+ const defaults = {
58
+ cycles: 0, lastDream: null, totalInsights: 0,
59
+ totalArchived: 0, activeInsights: 0, lastSessionTurns: 0,
60
+ };
61
+ if (!existsSync(STATE_FILE))
62
+ return defaults;
63
+ try {
64
+ return { ...defaults, ...JSON.parse(readFileSync(STATE_FILE, 'utf-8')) };
65
+ }
66
+ catch {
67
+ return defaults;
68
+ }
69
+ }
70
+ function saveState(state) {
71
+ ensureDirs();
72
+ writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
73
+ }
74
+ function generateId() {
75
+ return `dream_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
76
+ }
77
+ /** Calculate days between two ISO dates */
78
+ function daysBetween(a, b) {
79
+ return Math.abs(new Date(b).getTime() - new Date(a).getTime()) / (1000 * 60 * 60 * 24);
80
+ }
81
+ // ── Ollama Integration ──
82
+ async function ollamaGenerate(prompt, model = CONSOLIDATION_MODEL) {
83
+ try {
84
+ const controller = new AbortController();
85
+ const timer = setTimeout(() => controller.abort(), OLLAMA_TIMEOUT);
86
+ const res = await fetch(`${OLLAMA_URL}/api/generate`, {
87
+ method: 'POST',
88
+ headers: { 'Content-Type': 'application/json' },
89
+ body: JSON.stringify({
90
+ model,
91
+ prompt,
92
+ stream: false,
93
+ options: { temperature: 0.3, num_predict: 512 },
94
+ }),
95
+ signal: controller.signal,
96
+ });
97
+ clearTimeout(timer);
98
+ if (!res.ok)
99
+ return null;
100
+ const data = await res.json();
101
+ return data.response?.trim() || null;
102
+ }
103
+ catch {
104
+ return null;
105
+ }
106
+ }
107
+ async function isOllamaAvailable() {
108
+ try {
109
+ const res = await fetch(`${OLLAMA_URL}/api/tags`, { signal: AbortSignal.timeout(3000) });
110
+ return res.ok;
111
+ }
112
+ catch {
113
+ return false;
114
+ }
115
+ }
116
+ // ── Memory Aging ──
117
+ /** Apply exponential decay to all insights based on time elapsed */
118
+ export function ageMemories(insights) {
119
+ const now = new Date().toISOString();
120
+ const aged = [];
121
+ const archived = [];
122
+ for (const insight of insights) {
123
+ const days = daysBetween(insight.lastReinforced, now);
124
+ // Exponential decay: relevance * e^(-rate * days)
125
+ const decayed = insight.relevance * Math.exp(-DECAY_RATE * days);
126
+ if (decayed < MIN_RELEVANCE) {
127
+ archived.push({ ...insight, relevance: decayed });
128
+ }
129
+ else {
130
+ aged.push({ ...insight, relevance: Math.round(decayed * 1000) / 1000 });
131
+ }
132
+ }
133
+ return { aged, archived };
134
+ }
135
+ /** Archive old insights to disk */
136
+ function archiveInsights(insights) {
137
+ if (insights.length === 0)
138
+ return;
139
+ ensureDirs();
140
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
141
+ const archivePath = join(ARCHIVE_DIR, `archived_${timestamp}.json`);
142
+ writeFileSync(archivePath, JSON.stringify(insights, null, 2));
143
+ }
144
+ // ── Consolidation Prompts ──
145
+ function buildConsolidationPrompt(sessionHistory, existingInsights, existingMemory) {
146
+ const historyText = sessionHistory
147
+ .map(t => `[${t.role}]: ${t.content.slice(0, 500)}`)
148
+ .join('\n');
149
+ const existingText = existingInsights.length > 0
150
+ ? existingInsights.slice(0, 20).map(i => `- [${i.category}] ${i.content}`).join('\n')
151
+ : '(none yet)';
152
+ return `You are a memory consolidation system. Analyze this conversation session and extract durable insights.
153
+
154
+ EXISTING INSIGHTS:
155
+ ${existingText}
156
+
157
+ EXISTING PERSISTENT MEMORY:
158
+ ${existingMemory.slice(0, 2000) || '(empty)'}
159
+
160
+ SESSION TO CONSOLIDATE:
161
+ ${historyText}
162
+
163
+ INSTRUCTIONS:
164
+ Extract 1-5 insights from this session. Each insight should be:
165
+ - Durable (useful beyond this session)
166
+ - Non-obvious (not derivable from reading code)
167
+ - About the USER, their preferences, patterns, or project context
168
+
169
+ Format each insight as a JSON array of objects:
170
+ [
171
+ {
172
+ "content": "the insight text",
173
+ "category": "pattern|preference|skill|project|relationship",
174
+ "keywords": ["keyword1", "keyword2"]
175
+ }
176
+ ]
177
+
178
+ If the session is too short or trivial for insights, return: []
179
+
180
+ Respond ONLY with the JSON array, no other text.`;
181
+ }
182
+ function buildReinforcePrompt(sessionHistory, existingInsights) {
183
+ const historyText = sessionHistory
184
+ .map(t => `[${t.role}]: ${t.content.slice(0, 300)}`)
185
+ .join('\n');
186
+ const insightList = existingInsights
187
+ .map((i, idx) => `${idx}: [${i.category}] ${i.content}`)
188
+ .join('\n');
189
+ return `You are a memory reinforcement system. Given this conversation, which existing insights are confirmed/relevant?
190
+
191
+ EXISTING INSIGHTS (by index):
192
+ ${insightList}
193
+
194
+ SESSION:
195
+ ${historyText}
196
+
197
+ Return a JSON array of insight indices that this session reinforces. Example: [0, 3, 7]
198
+ If none are reinforced, return: []
199
+
200
+ Respond ONLY with the JSON array.`;
201
+ }
202
+ // ── Core Dream Functions ──
203
+ /** Run a full dream cycle — consolidate, reinforce, age */
204
+ export async function dream(sessionId = 'default') {
205
+ const result = {
206
+ success: false,
207
+ newInsights: 0,
208
+ reinforced: 0,
209
+ archived: 0,
210
+ cycle: 0,
211
+ duration: 0,
212
+ error: null,
213
+ };
214
+ const start = Date.now();
215
+ // Check Ollama availability
216
+ if (!(await isOllamaAvailable())) {
217
+ // Fallback: still do aging even without Ollama
218
+ const journal = loadJournal();
219
+ if (journal.length > 0) {
220
+ const { aged, archived } = ageMemories(journal);
221
+ archiveInsights(archived);
222
+ saveJournal(aged);
223
+ result.archived = archived.length;
224
+ }
225
+ result.error = 'Ollama not available — aging only (no new consolidation)';
226
+ result.duration = Date.now() - start;
227
+ return result;
228
+ }
229
+ const state = loadState();
230
+ let journal = loadJournal();
231
+ const history = getHistory(sessionId);
232
+ const memory = loadMemory();
233
+ // Skip if session too short
234
+ if (history.length < 4) {
235
+ result.error = 'Session too short for consolidation (< 4 turns)';
236
+ result.duration = Date.now() - start;
237
+ return result;
238
+ }
239
+ // Phase 1: Age existing memories
240
+ const { aged, archived } = ageMemories(journal);
241
+ archiveInsights(archived);
242
+ journal = aged;
243
+ result.archived = archived.length;
244
+ // Phase 2: Extract new insights from session
245
+ const consolidationPrompt = buildConsolidationPrompt(history, journal, memory);
246
+ const rawInsights = await ollamaGenerate(consolidationPrompt);
247
+ if (rawInsights) {
248
+ try {
249
+ const parsed = JSON.parse(rawInsights);
250
+ if (Array.isArray(parsed)) {
251
+ const now = new Date().toISOString();
252
+ for (const p of parsed.slice(0, 5)) {
253
+ // Dedup: skip if very similar content exists
254
+ const isDupe = journal.some(j => j.content.toLowerCase().includes(p.content.toLowerCase().slice(0, 50)) ||
255
+ p.content.toLowerCase().includes(j.content.toLowerCase().slice(0, 50)));
256
+ if (isDupe)
257
+ continue;
258
+ journal.push({
259
+ id: generateId(),
260
+ content: p.content,
261
+ category: p.category || 'pattern',
262
+ keywords: p.keywords || [],
263
+ relevance: 0.9,
264
+ sessions: 1,
265
+ created: now,
266
+ lastReinforced: now,
267
+ source: `session_${state.cycles + 1}`,
268
+ });
269
+ result.newInsights++;
270
+ }
271
+ }
272
+ }
273
+ catch {
274
+ // JSON parse failed — Ollama output wasn't clean
275
+ }
276
+ }
277
+ // Phase 3: Reinforce existing insights mentioned in session
278
+ if (journal.length > 0 && history.length >= 4) {
279
+ const reinforcePrompt = buildReinforcePrompt(history, journal);
280
+ const rawReinforce = await ollamaGenerate(reinforcePrompt);
281
+ if (rawReinforce) {
282
+ try {
283
+ const indices = JSON.parse(rawReinforce);
284
+ if (Array.isArray(indices)) {
285
+ const now = new Date().toISOString();
286
+ for (const idx of indices) {
287
+ if (idx >= 0 && idx < journal.length) {
288
+ journal[idx].relevance = Math.min(1, journal[idx].relevance + 0.15);
289
+ journal[idx].sessions++;
290
+ journal[idx].lastReinforced = now;
291
+ result.reinforced++;
292
+ }
293
+ }
294
+ }
295
+ }
296
+ catch {
297
+ // Reinforcement parse failed — non-critical
298
+ }
299
+ }
300
+ }
301
+ // Phase 4: Sort by relevance, trim to max
302
+ journal.sort((a, b) => b.relevance - a.relevance);
303
+ if (journal.length > MAX_INSIGHTS) {
304
+ const overflow = journal.slice(MAX_INSIGHTS);
305
+ archiveInsights(overflow);
306
+ journal = journal.slice(0, MAX_INSIGHTS);
307
+ result.archived += overflow.length;
308
+ }
309
+ // Save everything
310
+ saveJournal(journal);
311
+ state.cycles++;
312
+ state.lastDream = new Date().toISOString();
313
+ state.totalInsights += result.newInsights;
314
+ state.totalArchived += result.archived;
315
+ state.activeInsights = journal.length;
316
+ state.lastSessionTurns = history.length;
317
+ saveState(state);
318
+ result.success = true;
319
+ result.cycle = state.cycles;
320
+ result.duration = Date.now() - start;
321
+ return result;
322
+ }
323
+ // ── Query Functions ──
324
+ /** Get dream insights for inclusion in system prompt */
325
+ export function getDreamPrompt(maxInsights = 10) {
326
+ const journal = loadJournal();
327
+ if (journal.length === 0)
328
+ return '';
329
+ const top = journal
330
+ .filter(i => i.relevance > 0.3)
331
+ .slice(0, maxInsights);
332
+ if (top.length === 0)
333
+ return '';
334
+ const lines = top.map(i => `- [${i.category}] ${i.content} (relevance: ${Math.round(i.relevance * 100)}%)`);
335
+ return `\n[Dream Journal — Consolidated Insights]\n${lines.join('\n')}\n`;
336
+ }
337
+ /** Get full dream status */
338
+ export function getDreamStatus() {
339
+ const state = loadState();
340
+ const insights = loadJournal();
341
+ let archiveCount = 0;
342
+ if (existsSync(ARCHIVE_DIR)) {
343
+ archiveCount = readdirSync(ARCHIVE_DIR).filter(f => f.endsWith('.json')).length;
344
+ }
345
+ return { state, insights, archiveCount };
346
+ }
347
+ /** Search dream insights by keyword */
348
+ export function searchDreams(query) {
349
+ const journal = loadJournal();
350
+ const terms = query.toLowerCase().split(/\s+/);
351
+ return journal
352
+ .filter(i => {
353
+ const text = `${i.content} ${i.keywords.join(' ')} ${i.category}`.toLowerCase();
354
+ return terms.some(t => text.includes(t));
355
+ })
356
+ .sort((a, b) => b.relevance - a.relevance);
357
+ }
358
+ /** Manually reinforce a specific insight (user confirms it's still relevant) */
359
+ export function reinforceInsight(insightId) {
360
+ const journal = loadJournal();
361
+ const insight = journal.find(i => i.id === insightId);
362
+ if (!insight)
363
+ return false;
364
+ insight.relevance = Math.min(1, insight.relevance + 0.2);
365
+ insight.lastReinforced = new Date().toISOString();
366
+ insight.sessions++;
367
+ saveJournal(journal);
368
+ return true;
369
+ }
370
+ /** Run dream after session ends (non-blocking) */
371
+ export function dreamAfterSession(sessionId = 'default') {
372
+ // Fire and forget — don't block the user
373
+ dream(sessionId).catch(() => {
374
+ // Dream failed silently — non-critical
375
+ });
376
+ }
377
+ //# sourceMappingURL=dream.js.map
@@ -0,0 +1,60 @@
1
+ import { type ConversationTurn } from './memory.js';
2
+ export type MemorySignalKind = 'correction' | 'preference' | 'project_fact' | 'emotional';
3
+ export interface DetectedMemory {
4
+ /** What kind of signal was detected */
5
+ kind: MemorySignalKind;
6
+ /** The extracted memory content to save */
7
+ content: string;
8
+ /** The key to use for memory_save */
9
+ key: string;
10
+ /** Category for memory_save (fact | preference | pattern | solution) */
11
+ category: 'fact' | 'preference' | 'pattern' | 'solution';
12
+ /** Confidence score (0-1) */
13
+ confidence: number;
14
+ /** Which turn triggered the detection */
15
+ turnIndex: number;
16
+ /** Timestamp of detection */
17
+ detectedAt: string;
18
+ }
19
+ export interface ScannerStats {
20
+ /** Whether the scanner is currently active */
21
+ enabled: boolean;
22
+ /** Total turns observed this session */
23
+ turnsObserved: number;
24
+ /** Total scans performed */
25
+ scansPerformed: number;
26
+ /** Total moments detected */
27
+ momentsDetected: number;
28
+ /** Total memories saved */
29
+ memoriesSaved: number;
30
+ /** Breakdown by kind */
31
+ byKind: Record<MemorySignalKind, number>;
32
+ /** Recent detections (last 10) */
33
+ recentDetections: DetectedMemory[];
34
+ /** Session start time */
35
+ sessionStart: string;
36
+ }
37
+ interface ScannerState {
38
+ /** Cumulative stats across sessions */
39
+ totalScans: number;
40
+ totalDetections: number;
41
+ totalSaved: number;
42
+ /** Last scan timestamp */
43
+ lastScan: string | null;
44
+ /** Per-kind cumulative counts */
45
+ cumulativeByKind: Record<MemorySignalKind, number>;
46
+ }
47
+ /** Notify the scanner that a turn was added. Call after addTurn(). */
48
+ export declare function notifyTurn(turn: ConversationTurn, sessionId?: string): void;
49
+ /** Start the memory scanner for the current session */
50
+ export declare function startMemoryScanner(): void;
51
+ /** Stop the memory scanner and persist stats */
52
+ export declare function stopMemoryScanner(): void;
53
+ /** Get current scanner stats */
54
+ export declare function getMemoryScannerStats(): ScannerStats;
55
+ /** Check if the scanner is currently enabled */
56
+ export declare function isScannerEnabled(): boolean;
57
+ /** Get cumulative stats from disk (across all sessions) */
58
+ export declare function getCumulativeScannerStats(): ScannerState;
59
+ export {};
60
+ //# sourceMappingURL=memory-scanner.d.ts.map