kiku-agent 1.2.0 → 1.3.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.
Files changed (100) hide show
  1. package/README.md +459 -407
  2. package/package.json +27 -3
  3. package/server/agent/agentCore.js +635 -267
  4. package/server/agent/appPaths.js +2 -0
  5. package/server/agent/catalogEntityResolver.js +629 -0
  6. package/server/agent/citations.js +0 -21
  7. package/server/agent/clarificationAnalytics.js +1 -1
  8. package/server/agent/clarifyingQuestions.js +286 -216
  9. package/server/agent/cli.js +528 -73
  10. package/server/agent/commandFeatures.js +123 -24
  11. package/server/agent/commandRegistry.js +128 -62
  12. package/server/agent/commands.js +1410 -95
  13. package/server/agent/config.js +253 -163
  14. package/server/agent/contextCompactor.js +239 -25
  15. package/server/agent/dynamicCatalog.js +192 -0
  16. package/server/agent/envBootstrap.js +46 -0
  17. package/server/agent/envManager.js +317 -0
  18. package/server/agent/eventBus.js +3 -3
  19. package/server/agent/feedbackLearning.js +0 -8
  20. package/server/agent/gpuMetrics.js +188 -55
  21. package/server/agent/ingestionWorker.js +339 -0
  22. package/server/agent/lanDiscovery.js +369 -0
  23. package/server/agent/llmClient.js +111 -39
  24. package/server/agent/lmStudioWakeup.js +144 -0
  25. package/server/agent/mcpClient.js +2 -2
  26. package/server/agent/memory.js +635 -494
  27. package/server/agent/modelConfig.js +487 -0
  28. package/server/agent/newsIntelligence.js +219 -0
  29. package/server/agent/newsWatch.js +85 -0
  30. package/server/agent/prompts.js +146 -0
  31. package/server/agent/publicTools.js +7 -0
  32. package/server/agent/queryDetails.js +62 -1
  33. package/server/agent/research.js +314 -35
  34. package/server/agent/routingInterceptor.js +77 -0
  35. package/server/agent/rumoredProjects.js +15 -0
  36. package/server/agent/sanitizer.js +117 -17
  37. package/server/agent/subagent.js +52 -9
  38. package/server/agent/suggestionTui.js +33 -13
  39. package/server/agent/suggestions.js +29 -13
  40. package/server/agent/telemetry.js +0 -11
  41. package/server/agent/terminalImage.js +0 -1
  42. package/server/agent/textToolProtocol.js +190 -0
  43. package/server/agent/tokenTracker.js +220 -0
  44. package/server/agent/toolRouter.js +362 -216
  45. package/server/agent/tools.js +1155 -194
  46. package/server/agent/ui.js +144 -41
  47. package/server/agent/vramService.js +572 -0
  48. package/server/agent/webSearch.js +2279 -273
  49. package/server/agent/youtubeAuth.js +82 -73
  50. package/server/agent/youtubeTranscript.js +209 -0
  51. package/server/apiRouter.js +383 -179
  52. package/server/mcp/data/brands.js +5 -5
  53. package/server/mcp/data/discography.js +56 -45
  54. package/server/mcp/data/gallery.js +9 -0
  55. package/server/mcp/data/index.js +1 -0
  56. package/server/mcp/data/industryIndex.js +251 -0
  57. package/server/mcp/data/profile.js +5 -5
  58. package/server/mcp/data/timeline.js +2 -2
  59. package/server/mcp/data/works.js +2 -2
  60. package/server/mcp/handler.js +0 -2
  61. package/server/mcp/prompts.js +42 -2
  62. package/server/mcp/resources.js +5 -1
  63. package/server/mcp/security.js +25 -14
  64. package/server/mcp/server.js +92 -3
  65. package/server/mcp/tools.js +20 -11
  66. package/server/web-chat/core/connection.js +218 -194
  67. package/server/web-chat/core/conversationContext.js +1 -1
  68. package/server/web-chat/core/directCatalogEngine.js +190 -184
  69. package/server/web-chat/core/followUpEngine.js +88 -11
  70. package/server/web-chat/core/security.js +7 -4
  71. package/server/web-chat/router.js +54 -17
  72. package/server/web-chat/routes/bilibiliRoute.js +34 -0
  73. package/server/web-chat/routes/chatRoute.js +488 -397
  74. package/server/web-chat/routes/clarifyConfigRoute.js +1 -2
  75. package/server/web-chat/routes/exportRoute.js +2 -1
  76. package/server/web-chat/routes/lanDiscoveryRoute.js +90 -0
  77. package/server/web-chat/routes/newsRoute.js +36 -6
  78. package/server/web-chat/routes/providerRoute.js +87 -0
  79. package/server/web-chat/routes/staticRoute.js +61 -13
  80. package/server/web-chat/routes/statusRoute.js +23 -2
  81. package/server/web-chat/routes/vramRoute.js +22 -0
  82. package/server/web-chat/server.js +135 -106
  83. package/server/web-chat/ui/htmlBuilder.js +8 -8
  84. package/server/web-chat/ui/previews/modelSettings.preview.html +41 -0
  85. package/server/web-chat/ui/scripts/agentBubbleSystem.js +0 -5
  86. package/server/web-chat/ui/scripts/client.js +10526 -8397
  87. package/server/web-chat/ui/scripts/webchatI18n.js +190 -138
  88. package/server/web-chat/ui/styles/main.css +7471 -4246
  89. package/server/web-chat/ui/templates/body.html +305 -179
  90. package/src/data/brandsData.js +29 -6
  91. package/src/data/discographyData.js +918 -900
  92. package/src/data/galleryData.js +66 -0
  93. package/src/data/industryIndexData.js +292 -0
  94. package/src/data/musicVideoData.js +48 -5
  95. package/src/data/profileData.js +15 -1
  96. package/src/data/timelineData.js +4 -3
  97. package/src/data/worksData.js +536 -535
  98. package/src/lib/vram/estimator.js +464 -0
  99. package/src/lib/vram/modelRegistry.js +387 -0
  100. package/tokens.css +33 -2
@@ -1,40 +1,52 @@
1
- import { promises as fs } from 'node:fs';
2
- import path from 'node:path';
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
3
  import { EXPORT_DIR, KIKU_DATA_DIR, SESSION_DIR } from './appPaths.js';
4
4
  import { redactSensitiveValue } from './session.js';
5
-
6
- const MEMORY_DIR = KIKU_DATA_DIR;
7
- const MEMORY_FILE = path.join(MEMORY_DIR, 'long_term_memory.json');
8
- const SESSIONS_DIR = SESSION_DIR;
9
-
10
-
5
+ import { HierarchicalStore } from './memory/hierarchicalStore.js';
6
+ import { ContextAllocator } from './memory/contextAllocator.js';
7
+ import { ReflectionWorker } from './memory/reflectionWorker.js';
8
+
9
+ const MEMORY_DIR = KIKU_DATA_DIR;
10
+ const MEMORY_FILE = path.join(MEMORY_DIR, 'long_term_memory.json');
11
+ const SESSIONS_DIR = SESSION_DIR;
12
+ const MAX_FACTS = 500;
13
+ const SAVE_DEBOUNCE_MS = 400;
14
+
15
+
11
16
  export class AgentMemory {
12
17
  constructor(options = {}) {
13
18
  this.persistenceEnabled = options.persistence !== false;
14
19
  this.storageFile = options.storageFile ? path.resolve(String(options.storageFile)) : MEMORY_FILE;
15
- this.facts = [];
16
- this.preferences = {
17
- language: null,
18
- topicsOfInterest: [],
19
- responseStyle: 'default',
20
- userSkills: [],
21
- skillsEnabled: true,
22
- favorites: [],
23
- learnedStyles: [],
20
+ this.facts = [];
21
+ this.preferences = {
22
+ language: null,
23
+ topicsOfInterest: [],
24
+ responseStyle: 'default',
25
+ userSkills: [],
26
+ skillsEnabled: true,
27
+ favorites: [],
28
+ learnedStyles: [],
29
+ };
30
+ this.sessions = [];
31
+ this.summaries = [];
32
+ this.stats = {
33
+ totalFacts: 0,
34
+ totalSessions: 0,
35
+ totalConsolidations: 0,
36
+ lastConsolidationAt: null,
37
+ createdAt: new Date()
24
38
  };
25
- this.sessions = [];
26
- this.summaries = [];
27
- this.stats = {
28
- totalFacts: 0,
29
- totalSessions: 0,
30
- totalConsolidations: 0,
31
- lastConsolidationAt: null,
32
- createdAt: new Date()
33
- };
34
- this._loaded = false;
35
- this._storageWarningShown = false;
36
- this._storageError = null;
37
- }
39
+ this.hierarchicalStore = options.hierarchicalStore || new HierarchicalStore({ persistence: this.persistenceEnabled });
40
+ this.contextAllocator = new ContextAllocator({ store: this.hierarchicalStore });
41
+ this.reflectionWorker = new ReflectionWorker({ store: this.hierarchicalStore, memory: this });
42
+ this.activeRunId = options.activeRunId || null;
43
+ this._loaded = false;
44
+ this._storageWarningShown = false;
45
+ this._storageError = null;
46
+ this._saveTimer = null;
47
+ this._saveTimerResolve = null;
48
+ this._saveChain = Promise.resolve();
49
+ }
38
50
 
39
51
 
40
52
  async init() {
@@ -45,6 +57,7 @@ export class AgentMemory {
45
57
  try {
46
58
  await fs.mkdir(MEMORY_DIR, { recursive: true, mode: 0o700 });
47
59
  await fs.mkdir(SESSIONS_DIR, { recursive: true, mode: 0o700 });
60
+ await this.hierarchicalStore.init();
48
61
  this._storageError = null;
49
62
  return true;
50
63
  } catch (error) {
@@ -77,12 +90,12 @@ export class AgentMemory {
77
90
  lastAccessedAt: f.lastAccessedAt ? new Date(f.lastAccessedAt) : new Date()
78
91
  }));
79
92
 
80
- const loadedPrefs = parsed.preferences || {};
81
- this.preferences = {
82
- ...this.preferences,
83
- ...loadedPrefs,
84
- userSkills: Array.isArray(loadedPrefs.userSkills) ? loadedPrefs.userSkills : [],
85
- learnedStyles: Array.isArray(loadedPrefs.learnedStyles) ? loadedPrefs.learnedStyles : [],
93
+ const loadedPrefs = parsed.preferences || {};
94
+ this.preferences = {
95
+ ...this.preferences,
96
+ ...loadedPrefs,
97
+ userSkills: Array.isArray(loadedPrefs.userSkills) ? loadedPrefs.userSkills : [],
98
+ learnedStyles: Array.isArray(loadedPrefs.learnedStyles) ? loadedPrefs.learnedStyles : [],
86
99
  favorites: Array.isArray(loadedPrefs.favorites) ? loadedPrefs.favorites : [],
87
100
  topicsOfInterest: (loadedPrefs.topicsOfInterest || []).map(t => ({
88
101
  ...t,
@@ -101,11 +114,36 @@ export class AgentMemory {
101
114
  }
102
115
  }
103
116
 
104
- async save() {
105
- if (!this.persistenceEnabled) return false;
117
+ scheduleSave() {
118
+ if (!this.persistenceEnabled) return Promise.resolve(false);
119
+ if (this._saveTimer) {
120
+ clearTimeout(this._saveTimer);
121
+ this._saveTimer = null;
122
+ const superseded = this._saveTimerResolve;
123
+ this._saveTimerResolve = null;
124
+ if (superseded) superseded(false);
125
+ }
126
+ return new Promise(resolve => {
127
+ this._saveTimerResolve = resolve;
128
+ this._saveTimer = setTimeout(() => {
129
+ this._saveTimer = null;
130
+ const settle = this._saveTimerResolve;
131
+ this._saveTimerResolve = null;
132
+ this._flushSave().then(() => settle(true), () => settle(false));
133
+ }, SAVE_DEBOUNCE_MS);
134
+ });
135
+ }
136
+
137
+ _flushSave() {
138
+ const next = this._saveChain.then(() => this._writeMemoryFile());
139
+ this._saveChain = next.then(() => true, () => true);
140
+ return next;
141
+ }
142
+
143
+ async _writeMemoryFile() {
106
144
  try {
107
145
  if (!await this.init()) return false;
108
-
146
+
109
147
  const serializablePrefs = {
110
148
  ...this.preferences,
111
149
  topicsOfInterest: (this.preferences.topicsOfInterest || []).map(t => ({
@@ -125,7 +163,7 @@ export class AgentMemory {
125
163
  stats: this.stats,
126
164
  lastSavedAt: new Date().toISOString()
127
165
  };
128
- await fs.writeFile(this.storageFile, JSON.stringify(data, null, 2), { encoding: 'utf-8', mode: 0o600 });
166
+ await fs.writeFile(this.storageFile, JSON.stringify(data), { encoding: 'utf-8', mode: 0o600 });
129
167
  return true;
130
168
  } catch (error) {
131
169
  this._warnStorage(error);
@@ -133,6 +171,18 @@ export class AgentMemory {
133
171
  }
134
172
  }
135
173
 
174
+ async save() {
175
+ if (!this.persistenceEnabled) return false;
176
+ if (this._saveTimer) {
177
+ clearTimeout(this._saveTimer);
178
+ this._saveTimer = null;
179
+ const superseded = this._saveTimerResolve;
180
+ this._saveTimerResolve = null;
181
+ if (superseded) superseded(false);
182
+ }
183
+ return this._flushSave();
184
+ }
185
+
136
186
  addFact(fact, meta = {}) {
137
187
  const entry = {
138
188
  id: `fact_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
@@ -146,12 +196,37 @@ export class AgentMemory {
146
196
  tags: meta.tags || []
147
197
  };
148
198
  this.facts.push(entry);
199
+ if (this.facts.length > MAX_FACTS) {
200
+ let oldestIndex = 0;
201
+ let oldestTime = new Date(this.facts[0].lastAccessedAt || this.facts[0].createdAt || 0).getTime();
202
+ for (let i = 1; i < this.facts.length; i += 1) {
203
+ const time = new Date(this.facts[i].lastAccessedAt || this.facts[i].createdAt || 0).getTime();
204
+ if (time < oldestTime) {
205
+ oldestTime = time;
206
+ oldestIndex = i;
207
+ }
208
+ }
209
+ this.facts.splice(oldestIndex, 1);
210
+ }
149
211
  this.stats.totalFacts = this.facts.length;
150
- this.save().catch(() => {});
212
+ this.scheduleSave().catch(() => {});
151
213
  return entry;
152
214
  }
153
215
 
154
- scoreFactRelevance(fact, queryTerms = [], now = Date.now()) {
216
+ _compileTermMatchers(terms = []) {
217
+ const matchers = [];
218
+ for (const term of terms) {
219
+ if (!term || term.length < 2) continue;
220
+ matchers.push({
221
+ term,
222
+ lenBonus: Math.min(2.0, term.length / 4),
223
+ wordRegex: new RegExp(`\\b${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i')
224
+ });
225
+ }
226
+ return matchers;
227
+ }
228
+
229
+ scoreFactRelevance(fact, queryTerms = [], now = Date.now(), termMatchers = null) {
155
230
  if (!fact) return 0;
156
231
  const content = (fact.content || '').toLowerCase();
157
232
  const tags = (fact.tags || []).map(t => String(t).toLowerCase());
@@ -159,9 +234,10 @@ export class AgentMemory {
159
234
 
160
235
  let matchScore = 0;
161
236
  if (queryTerms.length > 0) {
162
- for (const term of queryTerms) {
163
- if (!term || term.length < 2) continue;
164
- const termLenBonus = Math.min(2.0, term.length / 4);
237
+ const matchers = termMatchers || this._compileTermMatchers(queryTerms);
238
+ for (const matcher of matchers) {
239
+ const term = matcher.term;
240
+ const termLenBonus = matcher.lenBonus;
165
241
 
166
242
  if (tags.includes(term)) {
167
243
  matchScore += 5.0 * termLenBonus;
@@ -173,8 +249,7 @@ export class AgentMemory {
173
249
  matchScore += 2.5;
174
250
  }
175
251
 
176
- const wordRegex = new RegExp(`\\b${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i');
177
- if (wordRegex.test(content)) {
252
+ if (matcher.wordRegex.test(content)) {
178
253
  matchScore += 3.5 * termLenBonus;
179
254
  } else if (content.includes(term)) {
180
255
  matchScore += 1.8 * termLenBonus;
@@ -199,10 +274,11 @@ export class AgentMemory {
199
274
  const q = String(query || '').toLowerCase().trim();
200
275
  const terms = q ? q.split(/\s+/).filter(t => t.length >= 2) : [];
201
276
  const now = Date.now();
277
+ const matchers = this._compileTermMatchers(terms);
202
278
 
203
279
  const scored = this.facts.map(fact => ({
204
280
  fact,
205
- score: this.scoreFactRelevance(fact, terms, now)
281
+ score: this.scoreFactRelevance(fact, terms, now, matchers)
206
282
  }));
207
283
 
208
284
  scored.sort((a, b) => b.score - a.score);
@@ -234,12 +310,14 @@ export class AgentMemory {
234
310
 
235
311
  const terms = q ? q.split(/\s+/).filter(t => t.length >= 2) : [];
236
312
  const now = Date.now();
313
+ const matchers = this._compileTermMatchers(terms);
237
314
 
238
- results.sort((a, b) => {
239
- const scoreA = this.scoreFactRelevance(a, terms, now);
240
- const scoreB = this.scoreFactRelevance(b, terms, now);
241
- return scoreB - scoreA;
242
- });
315
+ const decorated = results.map(fact => ({
316
+ fact,
317
+ score: this.scoreFactRelevance(fact, terms, now, matchers)
318
+ }));
319
+ decorated.sort((a, b) => b.score - a.score);
320
+ results = decorated.map(entry => entry.fact);
243
321
 
244
322
  for (const fact of results.slice(0, limit)) {
245
323
  fact.lastAccessedAt = new Date();
@@ -254,13 +332,13 @@ export class AgentMemory {
254
332
  if (idx === -1) return false;
255
333
  this.facts.splice(idx, 1);
256
334
  this.stats.totalFacts = this.facts.length;
257
- this.save().catch(() => {});
335
+ this.scheduleSave().catch(() => {});
258
336
  return true;
259
337
  }
260
338
 
261
339
  setPreference(key, value) {
262
340
  this.preferences[key] = value;
263
- this.save().catch(() => {});
341
+ this.scheduleSave().catch(() => {});
264
342
  }
265
343
 
266
344
  addFavorite(entity = {}) {
@@ -285,7 +363,7 @@ export class AgentMemory {
285
363
  };
286
364
  this.preferences.favorites.unshift(favorite);
287
365
  this.preferences.favorites = this.preferences.favorites.slice(0, 200);
288
- this.save().catch(() => {});
366
+ this.scheduleSave().catch(() => {});
289
367
  return { favorite, created: true };
290
368
  }
291
369
 
@@ -301,7 +379,7 @@ export class AgentMemory {
301
379
  const before = favorites.length;
302
380
  this.preferences.favorites = favorites.filter(item => item.id !== id);
303
381
  if (this.preferences.favorites.length === before) return false;
304
- this.save().catch(() => {});
382
+ this.scheduleSave().catch(() => {});
305
383
  return true;
306
384
  }
307
385
 
@@ -330,275 +408,338 @@ export class AgentMemory {
330
408
  await fs.writeFile(filePath, content, 'utf-8');
331
409
  return { filePath, filename, count: favorites.length, size: (await fs.stat(filePath)).size };
332
410
  }
333
-
334
- addUserSkill(instruction) {
335
- const value = String(instruction || '').replace(/\s+/g, ' ').trim();
336
- if (!value) return null;
337
- if (value.length > 500) throw new Error('Skill instruction must be 500 characters or fewer.');
338
- const duplicate = this.preferences.userSkills.find(skill => skill.instruction.toLowerCase() === value.toLowerCase());
339
- if (duplicate) return duplicate;
340
- const skill = { id: `skill_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`, instruction: value };
341
- this.preferences.userSkills.push(skill);
342
- this.preferences.userSkills = this.preferences.userSkills.slice(-20);
343
- this.save().catch(() => {});
344
- return skill;
345
- }
346
-
347
- removeUserSkill(id) {
348
- const before = this.preferences.userSkills.length;
349
- this.preferences.userSkills = this.preferences.userSkills.filter(skill => skill.id !== id);
350
- if (this.preferences.userSkills.length === before) return false;
351
- this.save().catch(() => {});
352
- return true;
353
- }
354
-
355
- clearUserSkills() {
356
- this.preferences.userSkills = [];
357
- this.save().catch(() => {});
358
- }
359
-
360
- buildUserSkillContext() {
361
- if (!this.preferences.skillsEnabled || this.preferences.userSkills.length === 0) return '';
362
- return this.preferences.userSkills.map((skill, index) => `${index + 1}. ${skill.instruction}`).join('\n');
363
- }
364
-
365
- applyFeedbackDirectives(directives = []) {
366
- const clean = (Array.isArray(directives) ? directives : [])
367
- .filter(d => d && typeof d.text === 'string' && d.text.trim())
368
- .slice(0, 6)
369
- .map(d => ({
370
- id: String(d.id || '').slice(0, 60),
371
- kind: d.kind === 'avoid' ? 'avoid' : 'prefer',
372
- text: String(d.text).replace(/\s+/g, ' ').trim().slice(0, 200),
373
- support: Number.isFinite(Number(d.support)) ? Number(d.support) : 0,
374
- updatedAt: d.updatedAt || new Date().toISOString(),
375
- }));
376
- this.preferences.learnedStyles = clean;
377
- this.save().catch(() => {});
378
- return clean;
379
- }
380
-
381
- buildFeedbackStyleContext() {
382
- const styles = Array.isArray(this.preferences.learnedStyles) ? this.preferences.learnedStyles : [];
383
- if (styles.length === 0) return '';
384
- return styles.map(s => `- ${s.text}`).join('\n');
385
- }
386
-
387
-
388
- async savePreferences() {
389
- await this.save();
390
- }
391
-
392
-
393
- trackTopicInterest(topic) {
394
- const existing = this.preferences.topicsOfInterest.find(
395
- t => t.topic.toLowerCase() === topic.toLowerCase()
396
- );
397
- if (existing) {
398
- existing.count++;
399
- existing.lastSeen = new Date();
400
- } else {
401
- this.preferences.topicsOfInterest.push({
402
- topic,
403
- count: 1,
404
- lastSeen: new Date()
405
- });
406
- }
407
-
408
- this.preferences.topicsOfInterest.sort((a, b) => b.count - a.count);
409
- this.preferences.topicsOfInterest = this.preferences.topicsOfInterest.slice(0, 20);
410
- this.save().catch(() => {});
411
- }
412
-
413
-
414
- getTopTopics(limit = 5) {
415
- return this.preferences.topicsOfInterest.slice(0, limit);
416
- }
417
-
418
-
411
+
412
+ addUserSkill(instruction) {
413
+ const value = String(instruction || '').replace(/\s+/g, ' ').trim();
414
+ if (!value) return null;
415
+ if (value.length > 500) throw new Error('Skill instruction must be 500 characters or fewer.');
416
+ const duplicate = this.preferences.userSkills.find(skill => skill.instruction.toLowerCase() === value.toLowerCase());
417
+ if (duplicate) return duplicate;
418
+ const skill = { id: `skill_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`, instruction: value };
419
+ this.preferences.userSkills.push(skill);
420
+ this.preferences.userSkills = this.preferences.userSkills.slice(-20);
421
+ this.scheduleSave().catch(() => {});
422
+ return skill;
423
+ }
424
+
425
+ removeUserSkill(id) {
426
+ const before = this.preferences.userSkills.length;
427
+ this.preferences.userSkills = this.preferences.userSkills.filter(skill => skill.id !== id);
428
+ if (this.preferences.userSkills.length === before) return false;
429
+ this.scheduleSave().catch(() => {});
430
+ return true;
431
+ }
432
+
433
+ clearUserSkills() {
434
+ this.preferences.userSkills = [];
435
+ this.scheduleSave().catch(() => {});
436
+ }
437
+
438
+ buildUserSkillContext() {
439
+ if (!this.preferences.skillsEnabled || this.preferences.userSkills.length === 0) return '';
440
+ return this.preferences.userSkills.map((skill, index) => `${index + 1}. ${skill.instruction}`).join('\n');
441
+ }
442
+
443
+ applyFeedbackDirectives(directives = []) {
444
+ const clean = (Array.isArray(directives) ? directives : [])
445
+ .filter(d => d && typeof d.text === 'string' && d.text.trim())
446
+ .slice(0, 6)
447
+ .map(d => ({
448
+ id: String(d.id || '').slice(0, 60),
449
+ kind: d.kind === 'avoid' ? 'avoid' : 'prefer',
450
+ text: String(d.text).replace(/\s+/g, ' ').trim().slice(0, 200),
451
+ support: Number.isFinite(Number(d.support)) ? Number(d.support) : 0,
452
+ updatedAt: d.updatedAt || new Date().toISOString(),
453
+ }));
454
+ this.preferences.learnedStyles = clean;
455
+ this.scheduleSave().catch(() => {});
456
+ return clean;
457
+ }
458
+
459
+ buildFeedbackStyleContext() {
460
+ const styles = Array.isArray(this.preferences.learnedStyles) ? this.preferences.learnedStyles : [];
461
+ if (styles.length === 0) return '';
462
+ return styles.map(s => `- ${s.text}`).join('\n');
463
+ }
464
+
465
+
466
+ async savePreferences() {
467
+ await this.save();
468
+ }
469
+
470
+
471
+ trackTopicInterest(topic) {
472
+ const existing = this.preferences.topicsOfInterest.find(
473
+ t => t.topic.toLowerCase() === topic.toLowerCase()
474
+ );
475
+ if (existing) {
476
+ existing.count++;
477
+ existing.lastSeen = new Date();
478
+ } else {
479
+ this.preferences.topicsOfInterest.push({
480
+ topic,
481
+ count: 1,
482
+ lastSeen: new Date()
483
+ });
484
+ }
485
+
486
+ this.preferences.topicsOfInterest.sort((a, b) => b.count - a.count);
487
+ this.preferences.topicsOfInterest = this.preferences.topicsOfInterest.slice(0, 20);
488
+ this.scheduleSave().catch(() => {});
489
+ }
490
+
491
+
492
+ getTopTopics(limit = 5) {
493
+ return this.preferences.topicsOfInterest.slice(0, limit);
494
+ }
495
+
496
+
419
497
  async archiveSession(sessionData) {
420
498
  if (!this.persistenceEnabled) return null;
421
- const archive = {
422
- id: sessionData.id,
423
- createdAt: sessionData.createdAt,
424
- endedAt: new Date(),
425
- messageCount: sessionData.messages?.length || 0,
426
- stats: { ...sessionData.stats },
427
- thinkingMode: sessionData.thinkingMode,
428
- summary: null
429
- };
430
-
431
-
432
- let archivedToDisk = false;
433
- try {
434
- if (!await this.init()) return null;
499
+ const archive = {
500
+ id: sessionData.id,
501
+ createdAt: sessionData.createdAt,
502
+ endedAt: new Date(),
503
+ messageCount: sessionData.messages?.length || 0,
504
+ stats: { ...sessionData.stats },
505
+ thinkingMode: sessionData.thinkingMode,
506
+ summary: null
507
+ };
508
+
509
+
510
+ let archivedToDisk = false;
511
+ try {
512
+ if (!await this.init()) return null;
435
513
  const safeId = String(sessionData.id || '').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 100);
436
514
  if (!safeId) return null;
437
515
  const sessionFile = path.join(SESSIONS_DIR, `${safeId}.json`);
438
516
  await fs.writeFile(sessionFile, JSON.stringify(redactSensitiveValue(sessionData), null, 2), { encoding: 'utf-8', mode: 0o600 });
439
- archivedToDisk = true;
440
- } catch (err) {
441
- this._warnStorage(err);
442
- }
443
-
444
- if (!archivedToDisk) return null;
445
-
446
- this.sessions.push(archive);
447
- this.stats.totalSessions = this.sessions.length;
448
- await this.save();
449
- return archive;
450
- }
451
-
452
- getArchivedSessions(limit = 20) {
453
- return this.sessions
454
- .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
455
- .slice(0, limit);
456
- }
457
-
517
+ archivedToDisk = true;
518
+ } catch (err) {
519
+ this._warnStorage(err);
520
+ }
521
+
522
+ if (!archivedToDisk) return null;
523
+
524
+ this.sessions.push(archive);
525
+ this.stats.totalSessions = this.sessions.length;
526
+ await this.save();
527
+ return archive;
528
+ }
529
+
530
+ getArchivedSessions(limit = 20) {
531
+ return [...this.sessions]
532
+ .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
533
+ .slice(0, limit);
534
+ }
535
+
458
536
  async loadArchivedSession(sessionId) {
459
537
  if (!this.persistenceEnabled) return null;
460
538
  try {
461
539
  const safeId = String(sessionId || '').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 100);
462
540
  if (!safeId) return null;
463
541
  const sessionFile = path.join(SESSIONS_DIR, `${safeId}.json`);
464
- const data = await fs.readFile(sessionFile, 'utf-8');
465
- return JSON.parse(data);
466
- } catch {
467
- return null;
468
- }
469
- }
470
-
471
-
472
- async consolidateSession(sessionData) {
473
- if (!sessionData?.messages?.length) return null;
474
-
475
- const userMessages = sessionData.messages
476
- .filter(m => m.role === 'user')
477
- .map(m => {
478
- const textPart = m.parts?.find(p => p.type === 'text');
479
- return textPart?.content || '';
480
- })
481
- .filter(Boolean);
482
-
483
- const assistantMessages = sessionData.messages
484
- .filter(m => m.role === 'assistant')
485
- .map(m => {
486
- const textPart = m.parts?.find(p => p.type === 'text');
487
- return textPart?.content || '';
488
- })
489
- .filter(Boolean);
490
-
491
- const topics = this._extractTopics(userMessages);
492
-
493
- for (const topic of topics) {
494
- this.trackTopicInterest(topic);
495
- }
496
-
497
- const summary = {
498
- id: `sum_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
499
- sessionId: sessionData.id,
500
- createdAt: new Date(),
501
- topics,
502
- questionCount: userMessages.length,
503
- keyExchanges: userMessages.slice(0, 5).map((q, i) => ({
504
- question: q.slice(0, 200),
505
- answerPreview: (assistantMessages[i] || '').slice(0, 200)
506
- })),
507
- durationMs: sessionData.stats?.totalDurationMs || 0,
508
- toolCalls: sessionData.stats?.totalToolCalls || 0
509
- };
510
-
511
- this.summaries.push(summary);
512
- this.stats.totalConsolidations = this.summaries.length;
513
- this.stats.lastConsolidationAt = new Date();
514
-
515
- const archived = this.sessions.find(s => s.id === sessionData.id);
516
- if (archived) {
517
- archived.summary = summary;
518
- }
519
-
520
- await this.save();
521
- return summary;
522
- }
523
-
524
- _extractTopics(messages) {
525
- const topicKeywords = {
526
- 'drama': ['drama', 'series', 'ละคร', '剧', '电视', 'tv series', 'film', 'movie', 'work'],
527
- 'music': ['song', 'music', 'mv', 'single', 'ost', 'เพลง', '歌', 'album', 'discography'],
528
- 'brand': ['brand', 'ambassador', 'endorsement', 'แบรนด์', '品牌', 'commercial'],
529
- 'awards': ['award', 'honor', 'ceremony', 'รางวัล', '奖', 'winner'],
530
- 'profile': ['profile', 'bio', 'birthday', 'age', 'ประวัติ', '资料', '个人信息'],
531
- 'timeline': ['career', 'timeline', 'history', 'era', 'เส้นทาง', '经历', 'ปี'],
532
- 'news': ['news', 'latest', 'recent', 'update', 'ข่าว', '最新', 'ล่าสุด'],
533
- 'video': ['video', 'stage', 'performance', 'live', 'คลิป', '视频', '舞台'],
534
- 'social': ['weibo', 'douyin', 'instagram', 'social media', 'โซเชียล', '微博'],
535
- 'gallery': ['photo', 'picture', 'gallery', 'fashion', 'photoshoot', 'ภาพ', '照片']
536
- };
537
-
538
- const topicCounts = {};
539
- const allText = messages.join(' ').toLowerCase();
540
-
541
- for (const [topic, keywords] of Object.entries(topicKeywords)) {
542
- const count = keywords.reduce((acc, kw) => {
543
- const regex = new RegExp(`\\b${kw}\\b`, 'gi');
544
- return acc + (allText.match(regex) || []).length;
545
- }, 0);
546
- if (count > 0) topicCounts[topic] = count;
547
- }
548
-
549
- return Object.entries(topicCounts)
550
- .sort(([, a], [, b]) => b - a)
551
- .slice(0, 5)
552
- .map(([topic]) => topic);
553
- }
554
-
555
-
542
+ const data = await fs.readFile(sessionFile, 'utf-8');
543
+ return JSON.parse(data);
544
+ } catch {
545
+ return null;
546
+ }
547
+ }
548
+
549
+
550
+ async consolidateSession(sessionData) {
551
+ if (!sessionData?.messages?.length) return null;
552
+
553
+ const userMessages = sessionData.messages
554
+ .filter(m => m.role === 'user')
555
+ .map(m => {
556
+ const textPart = m.parts?.find(p => p.type === 'text');
557
+ return textPart?.content || '';
558
+ })
559
+ .filter(Boolean);
560
+
561
+ const assistantMessages = sessionData.messages
562
+ .filter(m => m.role === 'assistant')
563
+ .map(m => {
564
+ const textPart = m.parts?.find(p => p.type === 'text');
565
+ return textPart?.content || '';
566
+ })
567
+ .filter(Boolean);
568
+
569
+ const topics = this._extractTopics(userMessages);
570
+
571
+ for (const topic of topics) {
572
+ this.trackTopicInterest(topic);
573
+ }
574
+
575
+ const summary = {
576
+ id: `sum_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
577
+ sessionId: sessionData.id,
578
+ createdAt: new Date(),
579
+ topics,
580
+ questionCount: userMessages.length,
581
+ keyExchanges: userMessages.slice(0, 5).map((q, i) => ({
582
+ question: q.slice(0, 200),
583
+ answerPreview: (assistantMessages[i] || '').slice(0, 200)
584
+ })),
585
+ durationMs: sessionData.stats?.totalDurationMs || 0,
586
+ toolCalls: sessionData.stats?.totalToolCalls || 0
587
+ };
588
+
589
+ this.summaries.push(summary);
590
+ this.stats.totalConsolidations = this.summaries.length;
591
+ this.stats.lastConsolidationAt = new Date();
592
+
593
+ const archived = this.sessions.find(s => s.id === sessionData.id);
594
+ if (archived) {
595
+ archived.summary = summary;
596
+ }
597
+
598
+ await this.save();
599
+ return summary;
600
+ }
601
+
602
+ _extractTopics(messages) {
603
+ const topicKeywords = {
604
+ 'drama': ['drama', 'series', 'ละคร', '剧', '电视', 'tv series', 'film', 'movie', 'work'],
605
+ 'music': ['song', 'music', 'mv', 'single', 'ost', 'เพลง', '歌', 'album', 'discography'],
606
+ 'brand': ['brand', 'ambassador', 'endorsement', 'แบรนด์', '品牌', 'commercial'],
607
+ 'awards': ['award', 'honor', 'ceremony', 'รางวัล', '奖', 'winner'],
608
+ 'profile': ['profile', 'bio', 'birthday', 'age', 'ประวัติ', '资料', '个人信息'],
609
+ 'timeline': ['career', 'timeline', 'history', 'era', 'เส้นทาง', '经历', 'ปี'],
610
+ 'news': ['news', 'latest', 'recent', 'update', 'ข่าว', '最新', 'ล่าสุด'],
611
+ 'video': ['video', 'stage', 'performance', 'live', 'คลิป', '视频', '舞台'],
612
+ 'social': ['weibo', 'douyin', 'instagram', 'social media', 'โซเชียล', '微博'],
613
+ 'gallery': ['photo', 'picture', 'gallery', 'fashion', 'photoshoot', 'ภาพ', '照片']
614
+ };
615
+
616
+ const topicCounts = {};
617
+ const allText = messages.join(' ').toLowerCase();
618
+
619
+ for (const [topic, keywords] of Object.entries(topicKeywords)) {
620
+ const count = keywords.reduce((acc, kw) => {
621
+ const regex = new RegExp(`\\b${kw}\\b`, 'gi');
622
+ return acc + (allText.match(regex) || []).length;
623
+ }, 0);
624
+ if (count > 0) topicCounts[topic] = count;
625
+ }
626
+
627
+ return Object.entries(topicCounts)
628
+ .sort(([, a], [, b]) => b - a)
629
+ .slice(0, 5)
630
+ .map(([topic]) => topic);
631
+ }
632
+
633
+
556
634
  buildMemoryContext(currentQuery = '') {
557
- const parts = [];
558
-
559
- if (this.preferences.language) {
560
- parts.push(`User's preferred language: ${this.preferences.language}`);
561
- }
562
-
563
- const userSkillContext = this.buildUserSkillContext();
564
- if (userSkillContext) {
565
- parts.push(`User-taught response skills (follow these preferences unless they conflict with safety or verified facts):\n${userSkillContext}`);
566
- }
567
-
568
- const feedbackStyleContext = this.buildFeedbackStyleContext();
569
- if (feedbackStyleContext) {
570
- parts.push(`Learned answer-style preferences (distilled from this user's like/dislike feedback — answer this way unless it conflicts with safety or verified facts):\n${feedbackStyleContext}`);
571
- }
572
-
573
- const topTopics = this.getTopTopics(5);
574
- if (topTopics.length > 0) {
575
- const topicList = topTopics.map(t => `${t.topic} (${t.count}x)`).join(', ');
576
- parts.push(`User frequently asks about: ${topicList}`);
577
- }
578
-
579
- const relevantFacts = currentQuery ? this.getRelevantFacts(currentQuery, 8) : this.facts.slice(-8);
580
- if (relevantFacts.length > 0) {
581
- const factList = relevantFacts.map(f => `- ${f.content}`).join('\n');
582
- parts.push(`Known facts from previous conversations:\n${factList}`);
583
- }
584
-
585
- const recentSummaries = this.summaries
586
- .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
587
- .slice(0, 3);
588
- if (recentSummaries.length > 0) {
589
- const sumList = recentSummaries.map(s => {
590
- const date = new Date(s.createdAt).toLocaleDateString();
591
- const topics = s.topics.join(', ');
592
- return `- [${date}] Topics: ${topics} (${s.questionCount} questions)`;
593
- }).join('\n');
594
- parts.push(`Recent conversation themes:\n${sumList}`);
595
- }
596
-
597
- if (parts.length === 0) return '';
598
-
635
+ const parts = [];
636
+
637
+ if (this.preferences.language) {
638
+ parts.push(`User's preferred language: ${this.preferences.language}`);
639
+ }
640
+
641
+ const userSkillContext = this.buildUserSkillContext();
642
+ if (userSkillContext) {
643
+ parts.push(`User-taught response skills (follow these preferences unless they conflict with safety or verified facts):\n${userSkillContext}`);
644
+ }
645
+
646
+ const feedbackStyleContext = this.buildFeedbackStyleContext();
647
+ if (feedbackStyleContext) {
648
+ parts.push(`Learned answer-style preferences (distilled from this user's like/dislike feedback — answer this way unless it conflicts with safety or verified facts):\n${feedbackStyleContext}`);
649
+ }
650
+
651
+ const topTopics = this.getTopTopics(5);
652
+ if (topTopics.length > 0) {
653
+ const topicList = topTopics.map(t => `${t.topic} (${t.count}x)`).join(', ');
654
+ parts.push(`User frequently asks about: ${topicList}`);
655
+ }
656
+
657
+ const relevantFacts = currentQuery ? this.getRelevantFacts(currentQuery, 8) : this.facts.slice(-8);
658
+ if (relevantFacts.length > 0) {
659
+ const factList = relevantFacts.map(f => `- ${f.content}`).join('\n');
660
+ parts.push(`Known facts from previous conversations:\n${factList}`);
661
+ }
662
+
663
+ const recentSummaries = [...this.summaries]
664
+ .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
665
+ .slice(0, 3);
666
+ if (recentSummaries.length > 0) {
667
+ const sumList = recentSummaries.map(s => {
668
+ const date = new Date(s.createdAt).toLocaleDateString();
669
+ const topics = s.topics.join(', ');
670
+ return `- [${date}] Topics: ${topics} (${s.questionCount} questions)`;
671
+ }).join('\n');
672
+ parts.push(`Recent conversation themes:\n${sumList}`);
673
+ }
674
+
675
+ if (parts.length === 0) return '';
676
+
599
677
  return `\n\n=== LONG-TERM MEMORY CONTEXT ===\n${parts.join('\n\n')}\n=== END MEMORY CONTEXT ===`;
600
678
  }
601
679
 
680
+ setActiveRunId(runId) {
681
+ this.activeRunId = runId;
682
+ }
683
+
684
+ getActiveRunId() {
685
+ return this.activeRunId;
686
+ }
687
+
688
+ async checkpointStep({ runId, stepName, status, summary, metadata } = {}) {
689
+ const targetRunId = runId || this.activeRunId;
690
+ return this.hierarchicalStore.checkpointStep({ runId: targetRunId, stepName, status, summary, metadata });
691
+ }
692
+
693
+ async recordFailure({ runId, errorTrace, recoveryAction, toolName } = {}) {
694
+ const targetRunId = runId || this.activeRunId;
695
+ return this.hierarchicalStore.recordFailure({ runId: targetRunId, errorTrace, recoveryAction, toolName });
696
+ }
697
+
698
+ async loadActiveRun(runId) {
699
+ const targetRunId = runId || this.activeRunId;
700
+ return this.hierarchicalStore.loadActiveRun(targetRunId);
701
+ }
702
+
703
+ async setGuideline(domain, text, type = 'user') {
704
+ return this.hierarchicalStore.setNode(`kiku/guidelines/${domain}`, text, { type });
705
+ }
706
+
707
+ async getGuideline(domain) {
708
+ const node = await this.hierarchicalStore.getNode(`kiku/guidelines/${domain}`);
709
+ return node?.content || null;
710
+ }
711
+
712
+ async addReflection(toolOrDomain, data) {
713
+ return this.hierarchicalStore.setNode(`kiku/reflections/${toolOrDomain}`, data, { type: 'context' });
714
+ }
715
+
716
+ async getReflections(toolOrDomain = '') {
717
+ if (toolOrDomain) {
718
+ const node = await this.hierarchicalStore.getNode(`kiku/reflections/${toolOrDomain}`);
719
+ return node?.content || null;
720
+ }
721
+ const nodes = await this.hierarchicalStore.listNodes('kiku/reflections/');
722
+ return nodes.map(n => n.content);
723
+ }
724
+
725
+ async triggerPostRunCompaction(runId, finalStatus = 'COMPLETED', metadata = {}) {
726
+ const targetRunId = runId || this.activeRunId;
727
+ return this.reflectionWorker.processPostRunCompaction(targetRunId, finalStatus, metadata);
728
+ }
729
+
730
+ async purgeExpiredArchives() {
731
+ return this.hierarchicalStore.purgeExpiredArchives();
732
+ }
733
+
734
+ async addMemoryContext(options = {}) {
735
+ return this.contextAllocator.allocateContext({
736
+ memory: this,
737
+ currentQuery: options.currentQuery || '',
738
+ activeRunId: options.activeRunId || this.activeRunId,
739
+ maxTokens: options.maxTokens
740
+ });
741
+ }
742
+
602
743
  getFactsByCategory() {
603
744
  return this.facts.reduce((counts, fact) => {
604
745
  const category = String(fact?.category || 'general');
@@ -608,160 +749,160 @@ export class AgentMemory {
608
749
  }
609
750
 
610
751
  getStatus() {
611
- return {
612
- factsCount: this.facts.length,
613
- factsByCategory: this.getFactsByCategory(),
614
- preferences: this.preferences,
615
- sessionsCount: this.sessions.length,
616
- summariesCount: this.summaries.length,
617
- topTopics: this.getTopTopics(10),
618
- stats: this.stats,
619
- lastConsolidationAt: this.stats.lastConsolidationAt,
620
- memoryFileSize: null
621
- };
622
- }
623
-
624
- async importFromJSON(filePath) {
625
- try {
626
- const raw = await fs.readFile(filePath, 'utf-8');
627
- const data = JSON.parse(raw);
628
-
629
- if (!data.facts && !data.preferences && !data.sessions) {
630
- return { success: false, message: 'Invalid memory file format — missing facts, preferences, or sessions.' };
631
- }
632
-
633
- let factsImported = 0;
634
- let factsSkipped = 0;
635
- let topicsImported = 0;
636
-
637
- if (Array.isArray(data.facts)) {
638
- for (const fact of data.facts) {
639
- const isDuplicate = this.facts.some(
640
- existing => existing.content === fact.content
641
- );
642
- if (!isDuplicate && fact.content) {
643
- this.facts.push({
644
- id: fact.id || `fact_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
645
- content: fact.content,
646
- category: fact.category || 'imported',
647
- source: fact.source || 'import',
648
- confidence: fact.confidence || 'high',
649
- createdAt: fact.createdAt ? new Date(fact.createdAt) : new Date(),
650
- lastAccessedAt: new Date(),
651
- accessCount: 0,
652
- tags: fact.tags || []
653
- });
654
- factsImported++;
655
- } else {
656
- factsSkipped++;
657
- }
658
- }
659
- }
660
-
661
- if (data.preferences) {
662
- if (data.preferences.language) {
663
- this.preferences.language = data.preferences.language;
664
- }
665
- if (data.preferences.responseStyle) {
666
- this.preferences.responseStyle = data.preferences.responseStyle;
667
- }
668
- if (Array.isArray(data.preferences.topicsOfInterest)) {
669
- for (const topic of data.preferences.topicsOfInterest) {
670
- const existing = this.preferences.topicsOfInterest.find(
671
- t => t.topic.toLowerCase() === topic.topic.toLowerCase()
672
- );
673
- if (existing) {
674
- existing.count += topic.count || 1;
675
- existing.lastSeen = new Date();
676
- } else {
677
- this.preferences.topicsOfInterest.push({
678
- topic: topic.topic,
679
- count: topic.count || 1,
680
- lastSeen: topic.lastSeen ? new Date(topic.lastSeen) : new Date()
681
- });
682
- topicsImported++;
683
- }
684
- }
685
- this.preferences.topicsOfInterest.sort((a, b) => b.count - a.count);
686
- this.preferences.topicsOfInterest = this.preferences.topicsOfInterest.slice(0, 20);
687
- }
688
- }
689
-
690
- if (Array.isArray(data.sessions)) {
691
- for (const session of data.sessions) {
692
- const isDuplicate = this.sessions.some(s => s.id === session.id);
693
- if (!isDuplicate) {
694
- this.sessions.push(session);
695
- }
696
- }
697
- }
698
-
699
- if (Array.isArray(data.summaries)) {
700
- for (const summary of data.summaries) {
701
- const isDuplicate = this.summaries.some(s => s.id === summary.id);
702
- if (!isDuplicate) {
703
- this.summaries.push(summary);
704
- }
705
- }
706
- }
707
-
708
- this.stats.totalFacts = this.facts.length;
709
- this.stats.totalSessions = this.sessions.length;
710
- this.stats.totalConsolidations = this.summaries.length;
711
-
712
- await this.save();
713
-
714
- return {
715
- success: true,
716
- factsImported,
717
- factsSkipped,
718
- topicsImported,
719
- totalFacts: this.facts.length,
720
- totalSessions: this.sessions.length,
721
- totalSummaries: this.summaries.length
722
- };
723
- } catch (error) {
724
- return { success: false, message: error.message };
725
- }
726
- }
727
-
728
- async exportToJSON() {
729
- const exportDir = EXPORT_DIR;
730
- try {
731
- await fs.mkdir(exportDir, { recursive: true });
732
- } catch {}
733
-
734
- const now = new Date();
735
- const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
736
- const filename = `kiku-memory-${timestamp}.json`;
737
- const filePath = path.join(exportDir, filename);
738
-
739
- const exportData = {
740
- exportedAt: now.toISOString(),
741
- stats: this.stats,
742
- preferences: {
743
- ...this.preferences,
744
- topicsOfInterest: (this.preferences.topicsOfInterest || []).map(t => ({
745
- ...t,
746
- lastSeen: t.lastSeen ? t.lastSeen.toISOString() : null
747
- }))
748
- },
749
- facts: this.facts.map(f => ({
750
- ...f,
751
- createdAt: f.createdAt ? f.createdAt.toISOString() : f.createdAt,
752
- lastAccessedAt: f.lastAccessedAt ? f.lastAccessedAt.toISOString() : f.lastAccessedAt
753
- })),
754
- factsByCategory: this.getFactsByCategory(),
755
- sessions: this.sessions,
756
- summaries: this.summaries
757
- };
758
-
759
- await fs.writeFile(filePath, JSON.stringify(exportData, null, 2), 'utf-8');
760
- return { filePath, filename, size: (await fs.stat(filePath)).size };
761
- }
762
-
763
- async clearAll() {
764
- this.facts = [];
752
+ return {
753
+ factsCount: this.facts.length,
754
+ factsByCategory: this.getFactsByCategory(),
755
+ preferences: this.preferences,
756
+ sessionsCount: this.sessions.length,
757
+ summariesCount: this.summaries.length,
758
+ topTopics: this.getTopTopics(10),
759
+ stats: this.stats,
760
+ lastConsolidationAt: this.stats.lastConsolidationAt,
761
+ memoryFileSize: null
762
+ };
763
+ }
764
+
765
+ async importFromJSON(filePath) {
766
+ try {
767
+ const raw = await fs.readFile(filePath, 'utf-8');
768
+ const data = JSON.parse(raw);
769
+
770
+ if (!data.facts && !data.preferences && !data.sessions) {
771
+ return { success: false, message: 'Invalid memory file format — missing facts, preferences, or sessions.' };
772
+ }
773
+
774
+ let factsImported = 0;
775
+ let factsSkipped = 0;
776
+ let topicsImported = 0;
777
+
778
+ if (Array.isArray(data.facts)) {
779
+ for (const fact of data.facts) {
780
+ const isDuplicate = this.facts.some(
781
+ existing => existing.content === fact.content
782
+ );
783
+ if (!isDuplicate && fact.content) {
784
+ this.facts.push({
785
+ id: fact.id || `fact_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
786
+ content: fact.content,
787
+ category: fact.category || 'imported',
788
+ source: fact.source || 'import',
789
+ confidence: fact.confidence || 'high',
790
+ createdAt: fact.createdAt ? new Date(fact.createdAt) : new Date(),
791
+ lastAccessedAt: new Date(),
792
+ accessCount: 0,
793
+ tags: fact.tags || []
794
+ });
795
+ factsImported++;
796
+ } else {
797
+ factsSkipped++;
798
+ }
799
+ }
800
+ }
801
+
802
+ if (data.preferences) {
803
+ if (data.preferences.language) {
804
+ this.preferences.language = data.preferences.language;
805
+ }
806
+ if (data.preferences.responseStyle) {
807
+ this.preferences.responseStyle = data.preferences.responseStyle;
808
+ }
809
+ if (Array.isArray(data.preferences.topicsOfInterest)) {
810
+ for (const topic of data.preferences.topicsOfInterest) {
811
+ const existing = this.preferences.topicsOfInterest.find(
812
+ t => t.topic.toLowerCase() === topic.topic.toLowerCase()
813
+ );
814
+ if (existing) {
815
+ existing.count += topic.count || 1;
816
+ existing.lastSeen = new Date();
817
+ } else {
818
+ this.preferences.topicsOfInterest.push({
819
+ topic: topic.topic,
820
+ count: topic.count || 1,
821
+ lastSeen: topic.lastSeen ? new Date(topic.lastSeen) : new Date()
822
+ });
823
+ topicsImported++;
824
+ }
825
+ }
826
+ this.preferences.topicsOfInterest.sort((a, b) => b.count - a.count);
827
+ this.preferences.topicsOfInterest = this.preferences.topicsOfInterest.slice(0, 20);
828
+ }
829
+ }
830
+
831
+ if (Array.isArray(data.sessions)) {
832
+ for (const session of data.sessions) {
833
+ const isDuplicate = this.sessions.some(s => s.id === session.id);
834
+ if (!isDuplicate) {
835
+ this.sessions.push(session);
836
+ }
837
+ }
838
+ }
839
+
840
+ if (Array.isArray(data.summaries)) {
841
+ for (const summary of data.summaries) {
842
+ const isDuplicate = this.summaries.some(s => s.id === summary.id);
843
+ if (!isDuplicate) {
844
+ this.summaries.push(summary);
845
+ }
846
+ }
847
+ }
848
+
849
+ this.stats.totalFacts = this.facts.length;
850
+ this.stats.totalSessions = this.sessions.length;
851
+ this.stats.totalConsolidations = this.summaries.length;
852
+
853
+ await this.save();
854
+
855
+ return {
856
+ success: true,
857
+ factsImported,
858
+ factsSkipped,
859
+ topicsImported,
860
+ totalFacts: this.facts.length,
861
+ totalSessions: this.sessions.length,
862
+ totalSummaries: this.summaries.length
863
+ };
864
+ } catch (error) {
865
+ return { success: false, message: error.message };
866
+ }
867
+ }
868
+
869
+ async exportToJSON() {
870
+ const exportDir = EXPORT_DIR;
871
+ try {
872
+ await fs.mkdir(exportDir, { recursive: true });
873
+ } catch {}
874
+
875
+ const now = new Date();
876
+ const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
877
+ const filename = `kiku-memory-${timestamp}.json`;
878
+ const filePath = path.join(exportDir, filename);
879
+
880
+ const exportData = {
881
+ exportedAt: now.toISOString(),
882
+ stats: this.stats,
883
+ preferences: {
884
+ ...this.preferences,
885
+ topicsOfInterest: (this.preferences.topicsOfInterest || []).map(t => ({
886
+ ...t,
887
+ lastSeen: t.lastSeen ? t.lastSeen.toISOString() : null
888
+ }))
889
+ },
890
+ facts: this.facts.map(f => ({
891
+ ...f,
892
+ createdAt: f.createdAt ? f.createdAt.toISOString() : f.createdAt,
893
+ lastAccessedAt: f.lastAccessedAt ? f.lastAccessedAt.toISOString() : f.lastAccessedAt
894
+ })),
895
+ factsByCategory: this.getFactsByCategory(),
896
+ sessions: this.sessions,
897
+ summaries: this.summaries
898
+ };
899
+
900
+ await fs.writeFile(filePath, JSON.stringify(exportData, null, 2), 'utf-8');
901
+ return { filePath, filename, size: (await fs.stat(filePath)).size };
902
+ }
903
+
904
+ async clearAll() {
905
+ this.facts = [];
765
906
  this.preferences = {
766
907
  language: null,
767
908
  topicsOfInterest: [],
@@ -769,34 +910,34 @@ export class AgentMemory {
769
910
  userSkills: [],
770
911
  skillsEnabled: true,
771
912
  favorites: [],
772
- };
773
- this.sessions = [];
774
- this.summaries = [];
775
- this.stats = {
776
- totalFacts: 0,
777
- totalSessions: 0,
778
- totalConsolidations: 0,
779
- lastConsolidationAt: null,
780
- createdAt: new Date()
781
- };
782
- await this.save();
783
- return true;
784
- }
785
-
786
- async getStorageSize() {
787
- try {
913
+ };
914
+ this.sessions = [];
915
+ this.summaries = [];
916
+ this.stats = {
917
+ totalFacts: 0,
918
+ totalSessions: 0,
919
+ totalConsolidations: 0,
920
+ lastConsolidationAt: null,
921
+ createdAt: new Date()
922
+ };
923
+ await this.save();
924
+ return true;
925
+ }
926
+
927
+ async getStorageSize() {
928
+ try {
788
929
  const stat = await fs.stat(this.storageFile);
789
- return stat.size;
790
- } catch {
791
- return 0;
792
- }
793
- }
794
-
795
- static formatBytes(bytes) {
796
- if (bytes === 0) return '0 B';
797
- const k = 1024;
798
- const sizes = ['B', 'KB', 'MB', 'GB'];
799
- const i = Math.floor(Math.log(bytes) / Math.log(k));
800
- return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
801
- }
802
- }
930
+ return stat.size;
931
+ } catch {
932
+ return 0;
933
+ }
934
+ }
935
+
936
+ static formatBytes(bytes) {
937
+ if (bytes === 0) return '0 B';
938
+ const k = 1024;
939
+ const sizes = ['B', 'KB', 'MB', 'GB'];
940
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
941
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
942
+ }
943
+ }