ideaco 1.1.5

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 (159) hide show
  1. package/.dockerignore +33 -0
  2. package/.nvmrc +1 -0
  3. package/ARCHITECTURE.md +394 -0
  4. package/Dockerfile +50 -0
  5. package/LICENSE +29 -0
  6. package/README.md +206 -0
  7. package/bin/i18n.js +46 -0
  8. package/bin/ideaco.js +494 -0
  9. package/deploy.sh +15 -0
  10. package/docker-compose.yml +30 -0
  11. package/electron/main.cjs +986 -0
  12. package/electron/preload.cjs +14 -0
  13. package/electron/web-backends.cjs +854 -0
  14. package/jsconfig.json +8 -0
  15. package/next.config.mjs +34 -0
  16. package/package.json +134 -0
  17. package/postcss.config.mjs +6 -0
  18. package/public/demo/dashboard.png +0 -0
  19. package/public/demo/employee.png +0 -0
  20. package/public/demo/messages.png +0 -0
  21. package/public/demo/office.png +0 -0
  22. package/public/demo/requirement.png +0 -0
  23. package/public/logo.jpeg +0 -0
  24. package/public/logo.png +0 -0
  25. package/scripts/prepare-electron.js +67 -0
  26. package/scripts/release.js +76 -0
  27. package/src/app/api/agents/[agentId]/chat/route.js +70 -0
  28. package/src/app/api/agents/[agentId]/conversations/route.js +35 -0
  29. package/src/app/api/agents/[agentId]/route.js +106 -0
  30. package/src/app/api/avatar/route.js +104 -0
  31. package/src/app/api/browse-dir/route.js +44 -0
  32. package/src/app/api/chat/route.js +265 -0
  33. package/src/app/api/company/factory-reset/route.js +43 -0
  34. package/src/app/api/company/route.js +82 -0
  35. package/src/app/api/departments/[deptId]/agents/[agentId]/dismiss/route.js +19 -0
  36. package/src/app/api/departments/route.js +92 -0
  37. package/src/app/api/group-chat-loop/events/route.js +70 -0
  38. package/src/app/api/group-chat-loop/route.js +94 -0
  39. package/src/app/api/mailbox/route.js +100 -0
  40. package/src/app/api/messages/route.js +14 -0
  41. package/src/app/api/providers/[id]/configure/route.js +21 -0
  42. package/src/app/api/providers/[id]/refresh-cookie/route.js +38 -0
  43. package/src/app/api/providers/[id]/test-cookie/route.js +28 -0
  44. package/src/app/api/providers/route.js +11 -0
  45. package/src/app/api/requirements/route.js +242 -0
  46. package/src/app/api/secretary/route.js +65 -0
  47. package/src/app/api/system/cli-backends/route.js +91 -0
  48. package/src/app/api/system/cron/route.js +110 -0
  49. package/src/app/api/system/knowledge/route.js +104 -0
  50. package/src/app/api/system/plugins/route.js +40 -0
  51. package/src/app/api/system/skills/route.js +46 -0
  52. package/src/app/api/system/status/route.js +46 -0
  53. package/src/app/api/talent-market/[profileId]/recall/route.js +22 -0
  54. package/src/app/api/talent-market/[profileId]/route.js +17 -0
  55. package/src/app/api/talent-market/route.js +26 -0
  56. package/src/app/api/teams/route.js +773 -0
  57. package/src/app/api/ws-files/[departmentId]/file/route.js +27 -0
  58. package/src/app/api/ws-files/[departmentId]/files/route.js +22 -0
  59. package/src/app/globals.css +130 -0
  60. package/src/app/layout.jsx +40 -0
  61. package/src/app/page.jsx +97 -0
  62. package/src/components/AgentChatModal.jsx +164 -0
  63. package/src/components/AgentDetailModal.jsx +425 -0
  64. package/src/components/AgentSpyModal.jsx +481 -0
  65. package/src/components/AvatarGrid.jsx +29 -0
  66. package/src/components/BossProfileModal.jsx +162 -0
  67. package/src/components/CachedAvatar.jsx +77 -0
  68. package/src/components/ChatPanel.jsx +219 -0
  69. package/src/components/ChatShared.jsx +255 -0
  70. package/src/components/DepartmentDetail.jsx +842 -0
  71. package/src/components/DepartmentView.jsx +367 -0
  72. package/src/components/FileReference.jsx +260 -0
  73. package/src/components/FilesView.jsx +465 -0
  74. package/src/components/GroupChatView.jsx +799 -0
  75. package/src/components/Mailbox.jsx +926 -0
  76. package/src/components/MessagesView.jsx +112 -0
  77. package/src/components/OnboardingGuide.jsx +209 -0
  78. package/src/components/OrgTree.jsx +151 -0
  79. package/src/components/Overview.jsx +391 -0
  80. package/src/components/PixelOffice.jsx +2281 -0
  81. package/src/components/ProviderGrid.jsx +551 -0
  82. package/src/components/ProvidersBoard.jsx +16 -0
  83. package/src/components/RequirementDetail.jsx +1279 -0
  84. package/src/components/RequirementsBoard.jsx +187 -0
  85. package/src/components/SecretarySettings.jsx +295 -0
  86. package/src/components/SetupWizard.jsx +388 -0
  87. package/src/components/Sidebar.jsx +169 -0
  88. package/src/components/SystemMonitor.jsx +808 -0
  89. package/src/components/TalentMarket.jsx +183 -0
  90. package/src/components/TeamDetail.jsx +697 -0
  91. package/src/core/agent/base-agent.js +104 -0
  92. package/src/core/agent/chat-store.js +602 -0
  93. package/src/core/agent/cli-agent/backends/claude-code/README.md +52 -0
  94. package/src/core/agent/cli-agent/backends/claude-code/config.js +27 -0
  95. package/src/core/agent/cli-agent/backends/codebuddy/README.md +236 -0
  96. package/src/core/agent/cli-agent/backends/codebuddy/config.js +27 -0
  97. package/src/core/agent/cli-agent/backends/codex/README.md +51 -0
  98. package/src/core/agent/cli-agent/backends/codex/config.js +27 -0
  99. package/src/core/agent/cli-agent/backends/index.js +27 -0
  100. package/src/core/agent/cli-agent/backends/registry.js +580 -0
  101. package/src/core/agent/cli-agent/index.js +154 -0
  102. package/src/core/agent/index.js +60 -0
  103. package/src/core/agent/llm-agent/client.js +320 -0
  104. package/src/core/agent/llm-agent/index.js +97 -0
  105. package/src/core/agent/message-bus.js +211 -0
  106. package/src/core/agent/session.js +608 -0
  107. package/src/core/agent/tools.js +596 -0
  108. package/src/core/agent/web-agent/backends/base-backend.js +180 -0
  109. package/src/core/agent/web-agent/backends/chatgpt/client.js +146 -0
  110. package/src/core/agent/web-agent/backends/chatgpt/config.js +148 -0
  111. package/src/core/agent/web-agent/backends/chatgpt/dom-scripts.js +303 -0
  112. package/src/core/agent/web-agent/backends/index.js +91 -0
  113. package/src/core/agent/web-agent/index.js +278 -0
  114. package/src/core/agent/web-agent/web-client.js +407 -0
  115. package/src/core/employee/base-employee.js +1088 -0
  116. package/src/core/employee/index.js +35 -0
  117. package/src/core/employee/knowledge.js +327 -0
  118. package/src/core/employee/lifecycle.js +990 -0
  119. package/src/core/employee/memory/index.js +642 -0
  120. package/src/core/employee/memory/store.js +143 -0
  121. package/src/core/employee/performance.js +224 -0
  122. package/src/core/employee/secretary.js +625 -0
  123. package/src/core/employee/skills.js +398 -0
  124. package/src/core/index.js +38 -0
  125. package/src/core/organization/company.js +2600 -0
  126. package/src/core/organization/department.js +737 -0
  127. package/src/core/organization/group-chat-loop.js +264 -0
  128. package/src/core/organization/index.js +8 -0
  129. package/src/core/organization/persistence.js +111 -0
  130. package/src/core/organization/team.js +267 -0
  131. package/src/core/organization/workforce/hr.js +377 -0
  132. package/src/core/organization/workforce/providers.js +468 -0
  133. package/src/core/organization/workforce/role-archetypes.js +805 -0
  134. package/src/core/organization/workforce/talent-market.js +205 -0
  135. package/src/core/prompts.js +532 -0
  136. package/src/core/requirement.js +1789 -0
  137. package/src/core/system/audit.js +483 -0
  138. package/src/core/system/cron.js +449 -0
  139. package/src/core/system/index.js +7 -0
  140. package/src/core/system/plugin.js +2183 -0
  141. package/src/core/utils/json-parse.js +188 -0
  142. package/src/core/workspace.js +239 -0
  143. package/src/lib/api-i18n.js +211 -0
  144. package/src/lib/avatar.js +268 -0
  145. package/src/lib/client-store.js +1025 -0
  146. package/src/lib/config-validator.js +483 -0
  147. package/src/lib/format-time.js +22 -0
  148. package/src/lib/hooks.js +414 -0
  149. package/src/lib/i18n.js +134 -0
  150. package/src/lib/paths.js +23 -0
  151. package/src/lib/store.js +72 -0
  152. package/src/locales/de.js +393 -0
  153. package/src/locales/en.js +1054 -0
  154. package/src/locales/es.js +393 -0
  155. package/src/locales/fr.js +393 -0
  156. package/src/locales/ja.js +501 -0
  157. package/src/locales/ko.js +513 -0
  158. package/src/locales/zh.js +828 -0
  159. package/tailwind.config.mjs +11 -0
@@ -0,0 +1,642 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+
3
+ /**
4
+ * Memory System - Each Employee has independent long-term and short-term memory
5
+ *
6
+ * Short-term memory: temporary info related to current task, may be forgotten or consolidated after task ends
7
+ * - Has expiration time (default 24 hours), expired entries are auto-cleaned
8
+ * - Unnecessary short-term memories are cleaned during consolidation
9
+ * Long-term memory: lessons learned, skill growth, self-reflection and other persistent info, stays with Employee permanently
10
+ *
11
+ * Rolling Context System:
12
+ * - historySummary: per-group rolling summary of old messages (compressed by AI)
13
+ * - AI manages its own memory via memoryOps in chat responses
14
+ *
15
+ * Relationship Impressions:
16
+ * - relationships: Map<employeeId, { name, impression, affinity, updatedAt }>
17
+ * - Each employee maintains a personal impression of every colleague they've interacted with
18
+ * - Impressions are โ‰ค200 chars, affinity is 1-100 (50=neutral), updated by AI via relationshipOps
19
+ */
20
+ export class Memory {
21
+ constructor() {
22
+ this.shortTerm = []; // Short-term memory list
23
+ this.longTerm = []; // Long-term memory list
24
+ this.maxShortTerm = 20; // Max short-term capacity, oldest auto-evicted when exceeded
25
+ this.maxLongTerm = 50; // Max long-term capacity for active use (pruned by importance)
26
+ this.defaultShortTermTTL = 24 * 60 * 60 * 1000; // Default short-term memory TTL: 24 hours
27
+
28
+ // Per-group rolling history summary (compressed old messages)
29
+ // key: groupId, value: string (compressed summary text)
30
+ this.historySummary = new Map();
31
+ this.maxSummaryLength = 2000; // Max chars for a single group's summary
32
+
33
+ // Relationship impressions: how this employee perceives each colleague
34
+ // key: employeeId, value: { name: string, impression: string (โ‰ค200 chars), affinity: number (1-100), updatedAt: Date }
35
+ this.relationships = new Map();
36
+ this.maxImpressionLength = 200; // Max chars per impression
37
+ }
38
+
39
+ // ======================== Short-term Memory ========================
40
+
41
+ /**
42
+ * Add short-term memory
43
+ * @param {string} content - Memory content
44
+ * @param {string} [category] - Category tag
45
+ * @param {object} [options] - Additional options
46
+ * @param {number} [options.ttl] - Custom TTL in milliseconds
47
+ * @param {number} [options.importance] - Importance score 1-10 (default 5)
48
+ */
49
+ addShortTerm(content, category = 'task', options = {}) {
50
+ const ttl = options.ttl || this.defaultShortTermTTL;
51
+ const memory = {
52
+ id: uuidv4(),
53
+ content,
54
+ category,
55
+ type: 'short-term',
56
+ importance: options.importance || 5,
57
+ createdAt: new Date(),
58
+ expiresAt: new Date(Date.now() + ttl),
59
+ };
60
+ this.shortTerm.push(memory);
61
+
62
+ // Evict oldest when capacity exceeded
63
+ if (this.shortTerm.length > this.maxShortTerm) {
64
+ const evicted = this.shortTerm.shift();
65
+ // Auto-archive evicted short-term memory as long-term summary
66
+ this.addLongTerm(
67
+ `[Auto-archived] ${evicted.content}`,
68
+ 'archived',
69
+ { importance: Math.max(1, (evicted.importance || 5) - 2) }
70
+ );
71
+ }
72
+
73
+ return memory;
74
+ }
75
+
76
+ // ======================== Long-term Memory ========================
77
+
78
+ /**
79
+ * Add long-term memory
80
+ * @param {string} content - Memory content
81
+ * @param {string} [category] - Category: experience | reflection | skill | feedback | archived | preference | fact | instruction
82
+ * @param {object} [options]
83
+ * @param {number} [options.importance] - Importance score 1-10 (default 5)
84
+ */
85
+ addLongTerm(content, category = 'experience', options = {}) {
86
+ const memory = {
87
+ id: uuidv4(),
88
+ content,
89
+ category,
90
+ type: 'long-term',
91
+ importance: options.importance || 5,
92
+ createdAt: new Date(),
93
+ };
94
+ this.longTerm.push(memory);
95
+
96
+ // Auto-prune if exceeds hard limit
97
+ if (this.longTerm.length > 200) {
98
+ this._pruneByImportance();
99
+ }
100
+
101
+ return memory;
102
+ }
103
+
104
+ // ======================== Memory Operations (AI-driven) ========================
105
+
106
+ /**
107
+ * Process memory operations returned by AI in chat response.
108
+ * This is the core of the AI-driven memory management system.
109
+ *
110
+ * @param {Array} memoryOps - Array of memory operations from AI
111
+ * Each op: { op: 'add'|'update'|'delete', type: 'long_term'|'short_term',
112
+ * content, importance, ttl, id, category }
113
+ * @returns {{ added: number, updated: number, deleted: number }}
114
+ */
115
+ processMemoryOps(memoryOps) {
116
+ if (!Array.isArray(memoryOps) || memoryOps.length === 0) {
117
+ return { added: 0, updated: 0, deleted: 0 };
118
+ }
119
+
120
+ let added = 0, updated = 0, deleted = 0;
121
+
122
+ for (const op of memoryOps) {
123
+ try {
124
+ switch (op.op) {
125
+ case 'add': {
126
+ if (!op.content) continue;
127
+ // Dedupe: skip if very similar content already exists
128
+ if (this._isDuplicate(op.content)) continue;
129
+
130
+ if (op.type === 'long_term') {
131
+ this.addLongTerm(op.content, op.category || 'experience', {
132
+ importance: op.importance || 5,
133
+ });
134
+ } else {
135
+ this.addShortTerm(op.content, op.category || 'context', {
136
+ importance: op.importance || 5,
137
+ ttl: op.ttl ? op.ttl * 1000 : undefined, // AI sends ttl in seconds
138
+ });
139
+ }
140
+ added++;
141
+ break;
142
+ }
143
+ case 'update': {
144
+ if (!op.id || !op.content) continue;
145
+ const mem = this._findById(op.id);
146
+ if (mem) {
147
+ mem.content = op.content;
148
+ if (op.importance !== undefined) mem.importance = op.importance;
149
+ updated++;
150
+ }
151
+ break;
152
+ }
153
+ case 'delete': {
154
+ if (!op.id) continue;
155
+ const stIdx = this.shortTerm.findIndex(m => m.id === op.id);
156
+ if (stIdx !== -1) {
157
+ this.shortTerm.splice(stIdx, 1);
158
+ deleted++;
159
+ continue;
160
+ }
161
+ const ltIdx = this.longTerm.findIndex(m => m.id === op.id);
162
+ if (ltIdx !== -1) {
163
+ this.longTerm.splice(ltIdx, 1);
164
+ deleted++;
165
+ }
166
+ break;
167
+ }
168
+ }
169
+ } catch (e) {
170
+ console.warn(` โš ๏ธ [Memory] Failed to process memoryOp:`, op, e.message);
171
+ }
172
+ }
173
+
174
+ if (added + updated + deleted > 0) {
175
+ console.log(` ๐Ÿง  [Memory] Processed ${added} adds, ${updated} updates, ${deleted} deletes`);
176
+ }
177
+
178
+ return { added, updated, deleted };
179
+ }
180
+
181
+ // ======================== Rolling History Summary ========================
182
+
183
+ /**
184
+ * Update the rolling history summary for a group.
185
+ * Called when AI returns a summary of old messages.
186
+ *
187
+ * @param {string} groupId
188
+ * @param {string} newSummary - AI-generated summary of old messages
189
+ */
190
+ updateHistorySummary(groupId, newSummary) {
191
+ if (!newSummary || !newSummary.trim()) return;
192
+
193
+ const existing = this.historySummary.get(groupId) || '';
194
+ let combined;
195
+
196
+ if (existing) {
197
+ combined = `${existing}\n---\n${newSummary.trim()}`;
198
+ } else {
199
+ combined = newSummary.trim();
200
+ }
201
+
202
+ // Truncate if too long โ€” keep the most recent part
203
+ if (combined.length > this.maxSummaryLength) {
204
+ // Find a good split point (after a "---" separator)
205
+ const parts = combined.split('\n---\n');
206
+ while (parts.join('\n---\n').length > this.maxSummaryLength && parts.length > 1) {
207
+ parts.shift(); // Drop oldest summary chunk
208
+ }
209
+ combined = parts.join('\n---\n');
210
+ // If still too long, hard truncate
211
+ if (combined.length > this.maxSummaryLength) {
212
+ combined = combined.slice(-this.maxSummaryLength);
213
+ }
214
+ }
215
+
216
+ this.historySummary.set(groupId, combined);
217
+ }
218
+
219
+ /**
220
+ * Get the rolling history summary for a group.
221
+ * @param {string} groupId
222
+ * @returns {string}
223
+ */
224
+ getHistorySummary(groupId) {
225
+ return this.historySummary.get(groupId) || '';
226
+ }
227
+
228
+ // ======================== Relationship Impressions ========================
229
+
230
+ /**
231
+ * Process relationship operations returned by AI in chat response.
232
+ * @param {Array} relationshipOps - Array of relationship operations from AI
233
+ * Each op: { employeeId, name, impression, affinity }
234
+ * @returns {{ updated: number }}
235
+ */
236
+ processRelationshipOps(relationshipOps) {
237
+ if (!Array.isArray(relationshipOps) || relationshipOps.length === 0) {
238
+ return { updated: 0 };
239
+ }
240
+
241
+ let updated = 0;
242
+ for (const op of relationshipOps) {
243
+ try {
244
+ if (!op.employeeId || !op.impression) continue;
245
+ const impression = op.impression.slice(0, this.maxImpressionLength);
246
+ const existing = this.relationships.get(op.employeeId);
247
+ // Clamp affinity to 1-100, default to existing or 50 (neutral)
248
+ let affinity = 50;
249
+ if (typeof op.affinity === 'number') {
250
+ affinity = Math.max(1, Math.min(100, Math.round(op.affinity)));
251
+ } else if (existing?.affinity) {
252
+ affinity = existing.affinity;
253
+ }
254
+ this.relationships.set(op.employeeId, {
255
+ name: op.name || existing?.name || op.employeeId,
256
+ impression,
257
+ affinity,
258
+ updatedAt: new Date(),
259
+ });
260
+ updated++;
261
+ } catch (e) {
262
+ console.warn(` โš ๏ธ [Memory] Failed to process relationshipOp:`, op, e.message);
263
+ }
264
+ }
265
+
266
+ if (updated > 0) {
267
+ console.log(` ๐Ÿ‘ฅ [Memory] Updated ${updated} relationship impressions`);
268
+ }
269
+
270
+ return { updated };
271
+ }
272
+
273
+ /**
274
+ * Build a relationship context string for prompt injection.
275
+ * Only includes impressions for employees present in the given participant list.
276
+ * @param {Array<string>} participantIds - IDs of employees in the current conversation
277
+ * @returns {string}
278
+ */
279
+ buildRelationshipContext(participantIds) {
280
+ if (!participantIds || participantIds.length === 0) return '';
281
+
282
+ const impressions = [];
283
+ for (const pid of participantIds) {
284
+ const rel = this.relationships.get(pid);
285
+ if (rel) {
286
+ const heart = rel.affinity >= 80 ? 'โค๏ธ' : rel.affinity >= 60 ? '๐Ÿ˜Š' : rel.affinity >= 40 ? '๐Ÿ˜' : rel.affinity >= 20 ? '๐Ÿ˜’' : '๐Ÿ’ข';
287
+ impressions.push(`- ${rel.name} (${pid}): ${rel.impression} ${heart} affinity:${rel.affinity}/100`);
288
+ }
289
+ }
290
+
291
+ if (impressions.length === 0) return '';
292
+ return `\n\n**๐Ÿ‘ฅ Your impressions of colleagues in this conversation:**\n${impressions.join('\n')}`;
293
+ }
294
+
295
+ /**
296
+ * Get impression of a specific employee.
297
+ * @param {string} employeeId
298
+ * @returns {{ name: string, impression: string, affinity: number, updatedAt: Date } | null}
299
+ */
300
+ getImpression(employeeId) {
301
+ return this.relationships.get(employeeId) || null;
302
+ }
303
+
304
+ /**
305
+ * Get all relationship impressions.
306
+ * @returns {Array<{ employeeId: string, name: string, impression: string, affinity: number, updatedAt: Date }>}
307
+ */
308
+ getAllImpressions() {
309
+ const result = [];
310
+ for (const [id, rel] of this.relationships) {
311
+ result.push({ employeeId: id, ...rel });
312
+ }
313
+ return result;
314
+ }
315
+
316
+ // ======================== Context Builder ========================
317
+
318
+ /**
319
+ * Build a compact memory context string for inclusion in prompts.
320
+ * This replaces the old agentMemory approach with structured memory.
321
+ *
322
+ * @param {string} groupId - Current group context
323
+ * @returns {string} Formatted memory context for prompt injection
324
+ */
325
+ buildMemoryContext(groupId) {
326
+ const parts = [];
327
+
328
+ // 1. Rolling history summary
329
+ const summary = this.getHistorySummary(groupId);
330
+ if (summary) {
331
+ parts.push(`**๐Ÿ“œ Conversation History Summary:**\n${summary}`);
332
+ }
333
+
334
+ // 2. Long-term memories (sorted by importance, top items)
335
+ const activeLongTerm = this.longTerm
336
+ .sort((a, b) => (b.importance || 5) - (a.importance || 5))
337
+ .slice(0, 15);
338
+ if (activeLongTerm.length > 0) {
339
+ parts.push(`**๐Ÿ’พ Your Long-term Memories:**\n${activeLongTerm.map(m =>
340
+ `- [${m.category}] ${m.content} (id:${m.id})`
341
+ ).join('\n')}`);
342
+ }
343
+
344
+ // 3. Short-term memories (filtered for active, not expired)
345
+ this.cleanExpiredShortTerm();
346
+ const activeShortTerm = this.shortTerm
347
+ .sort((a, b) => (b.importance || 5) - (a.importance || 5))
348
+ .slice(0, 10);
349
+ if (activeShortTerm.length > 0) {
350
+ parts.push(`**โšก Your Short-term Memories:**\n${activeShortTerm.map(m =>
351
+ `- [${m.category}] ${m.content} (id:${m.id})`
352
+ ).join('\n')}`);
353
+ }
354
+
355
+ return parts.length > 0 ? '\n\n' + parts.join('\n\n') : '';
356
+ }
357
+
358
+ /**
359
+ * Build full context including memory + relationships for a group conversation.
360
+ * @param {string} groupId - Current group context
361
+ * @param {Array<string>} [participantIds] - IDs of employees in the conversation
362
+ * @returns {string}
363
+ */
364
+ buildFullContext(groupId, participantIds = []) {
365
+ let ctx = this.buildMemoryContext(groupId);
366
+ const relCtx = this.buildRelationshipContext(participantIds);
367
+ if (relCtx) ctx += relCtx;
368
+ return ctx;
369
+ }
370
+
371
+ // ======================== Existing Methods (preserved) ========================
372
+
373
+ /**
374
+ * Consolidate short-term memory into long-term
375
+ * @param {string} shortTermId - Short-term memory ID
376
+ * @param {string} [refinedContent] - Refined content (uses original if not provided)
377
+ */
378
+ consolidate(shortTermId, refinedContent = null) {
379
+ const idx = this.shortTerm.findIndex(m => m.id === shortTermId);
380
+ if (idx === -1) return null;
381
+
382
+ const original = this.shortTerm.splice(idx, 1)[0];
383
+ const longMemory = this.addLongTerm(
384
+ refinedContent || original.content,
385
+ 'experience'
386
+ );
387
+
388
+ return longMemory;
389
+ }
390
+
391
+ /**
392
+ * Clean up expired short-term memories
393
+ * @returns {number} Number of cleaned memories
394
+ */
395
+ cleanExpiredShortTerm() {
396
+ const now = Date.now();
397
+ const before = this.shortTerm.length;
398
+ this.shortTerm = this.shortTerm.filter(m => {
399
+ if (!m.expiresAt) return true;
400
+ return new Date(m.expiresAt).getTime() > now;
401
+ });
402
+ const cleaned = before - this.shortTerm.length;
403
+ if (cleaned > 0) {
404
+ console.log(`๐Ÿงน Cleaned ${cleaned} expired short-term memories`);
405
+ }
406
+ return cleaned;
407
+ }
408
+
409
+ /**
410
+ * Consolidate memories: clean expired short-term, deduplicate, and limit total long-term count
411
+ * Should be called before waking up an Employee
412
+ * @returns {object} { expiredCleaned, duplicatesRemoved }
413
+ */
414
+ consolidateMemories() {
415
+ // 1. Clean expired short-term memories
416
+ const expiredCleaned = this.cleanExpiredShortTerm();
417
+
418
+ // 2. Deduplicate long-term memories
419
+ const seen = new Map();
420
+ const deduped = [];
421
+ let duplicatesRemoved = 0;
422
+
423
+ for (const mem of this.longTerm) {
424
+ const key = mem.content.toLowerCase().trim().slice(0, 100);
425
+ if (seen.has(key)) {
426
+ duplicatesRemoved++;
427
+ continue;
428
+ }
429
+ seen.set(key, true);
430
+ deduped.push(mem);
431
+ }
432
+ this.longTerm = deduped;
433
+
434
+ // 3. If long-term memory exceeds limit, prune by importance
435
+ if (this.longTerm.length > 200) {
436
+ this._pruneByImportance();
437
+ }
438
+
439
+ if (expiredCleaned > 0 || duplicatesRemoved > 0) {
440
+ console.log(`๐Ÿง  Memory consolidation complete: cleaned ${expiredCleaned} expired short-term, deduplicated ${duplicatesRemoved} long-term`);
441
+ }
442
+
443
+ return { expiredCleaned, duplicatesRemoved };
444
+ }
445
+
446
+ /**
447
+ * Clear short-term memory (e.g. when switching department/project)
448
+ */
449
+ clearShortTerm() {
450
+ this.shortTerm = [];
451
+ }
452
+
453
+ /**
454
+ * Search long-term memory by category
455
+ */
456
+ searchLongTerm(category = null) {
457
+ if (!category) return [...this.longTerm];
458
+ return this.longTerm.filter(m => m.category === category);
459
+ }
460
+
461
+ /**
462
+ * Search all memory by keyword
463
+ */
464
+ search(keyword) {
465
+ const kw = keyword.toLowerCase();
466
+ const results = [];
467
+ [...this.shortTerm, ...this.longTerm].forEach(m => {
468
+ if (m.content.toLowerCase().includes(kw)) {
469
+ results.push(m);
470
+ }
471
+ });
472
+ return results;
473
+ }
474
+
475
+ /**
476
+ * Get memory summary
477
+ */
478
+ getSummary() {
479
+ return {
480
+ shortTermCount: this.shortTerm.length,
481
+ longTermCount: this.longTerm.length,
482
+ historySummaryGroups: this.historySummary.size,
483
+ relationshipCount: this.relationships.size,
484
+ shortTerm: this.shortTerm.map(m => ({
485
+ id: m.id,
486
+ content: m.content,
487
+ category: m.category,
488
+ importance: m.importance || 5,
489
+ expiresAt: m.expiresAt,
490
+ })),
491
+ longTerm: this.longTerm.map(m => ({
492
+ id: m.id,
493
+ content: m.content,
494
+ category: m.category,
495
+ importance: m.importance || 5,
496
+ })),
497
+ relationships: this.getAllImpressions().map(r => ({
498
+ employeeId: r.employeeId,
499
+ name: r.name,
500
+ impression: r.impression,
501
+ affinity: r.affinity || 50,
502
+ updatedAt: r.updatedAt,
503
+ })),
504
+ };
505
+ }
506
+
507
+ // ======================== Serialization ========================
508
+
509
+ /**
510
+ * Serialize (for persistence / talent market storage)
511
+ */
512
+ serialize() {
513
+ // Serialize historySummary Map โ†’ plain object
514
+ const summaryObj = {};
515
+ for (const [k, v] of this.historySummary) {
516
+ summaryObj[k] = v;
517
+ }
518
+
519
+ // Serialize relationships Map โ†’ plain object
520
+ const relObj = {};
521
+ for (const [k, v] of this.relationships) {
522
+ relObj[k] = {
523
+ ...v,
524
+ updatedAt: v.updatedAt instanceof Date ? v.updatedAt.toISOString() : v.updatedAt,
525
+ };
526
+ }
527
+
528
+ return {
529
+ shortTerm: this.shortTerm.map(m => ({
530
+ ...m,
531
+ createdAt: m.createdAt instanceof Date ? m.createdAt.toISOString() : m.createdAt,
532
+ expiresAt: m.expiresAt ? (m.expiresAt instanceof Date ? m.expiresAt.toISOString() : m.expiresAt) : null,
533
+ })),
534
+ longTerm: this.longTerm.map(m => ({
535
+ ...m,
536
+ createdAt: m.createdAt instanceof Date ? m.createdAt.toISOString() : m.createdAt,
537
+ })),
538
+ historySummary: summaryObj,
539
+ relationships: relObj,
540
+ };
541
+ }
542
+
543
+ /**
544
+ * Restore from serialized data
545
+ */
546
+ static deserialize(data) {
547
+ const memory = new Memory();
548
+ if (data.shortTerm) {
549
+ memory.shortTerm = data.shortTerm.map(m => ({
550
+ ...m,
551
+ createdAt: m.createdAt ? new Date(m.createdAt) : new Date(),
552
+ expiresAt: m.expiresAt ? new Date(m.expiresAt) : new Date(Date.now() + memory.defaultShortTermTTL),
553
+ }));
554
+ }
555
+ if (data.longTerm) {
556
+ memory.longTerm = data.longTerm.map(m => ({
557
+ ...m,
558
+ createdAt: m.createdAt ? new Date(m.createdAt) : new Date(),
559
+ }));
560
+ }
561
+ // Restore historySummary
562
+ if (data.historySummary && typeof data.historySummary === 'object') {
563
+ for (const [k, v] of Object.entries(data.historySummary)) {
564
+ memory.historySummary.set(k, v);
565
+ }
566
+ }
567
+ // Restore relationships
568
+ if (data.relationships && typeof data.relationships === 'object') {
569
+ for (const [k, v] of Object.entries(data.relationships)) {
570
+ memory.relationships.set(k, {
571
+ ...v,
572
+ updatedAt: v.updatedAt ? new Date(v.updatedAt) : new Date(),
573
+ });
574
+ }
575
+ }
576
+ return memory;
577
+ }
578
+
579
+ // ======================== Private Helpers ========================
580
+
581
+ /**
582
+ * Find a memory by ID across both short-term and long-term.
583
+ */
584
+ _findById(id) {
585
+ return this.shortTerm.find(m => m.id === id) || this.longTerm.find(m => m.id === id) || null;
586
+ }
587
+
588
+ /**
589
+ * Check if content is a duplicate of existing memory.
590
+ * Uses first 80 chars as fingerprint.
591
+ */
592
+ _isDuplicate(content) {
593
+ const key = content.toLowerCase().trim().slice(0, 80);
594
+ return [...this.shortTerm, ...this.longTerm].some(m =>
595
+ m.content.toLowerCase().trim().slice(0, 80) === key
596
+ );
597
+ }
598
+
599
+ /**
600
+ * Prune long-term memory by importance when over capacity.
601
+ * Keeps the highest-importance and most-recent entries.
602
+ */
603
+ _pruneByImportance() {
604
+ const MAX = 200;
605
+ if (this.longTerm.length <= MAX) return;
606
+
607
+ // Sort by importance (desc), then by creation time (desc)
608
+ this.longTerm.sort((a, b) => {
609
+ const impDiff = (b.importance || 5) - (a.importance || 5);
610
+ if (impDiff !== 0) return impDiff;
611
+ return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
612
+ });
613
+
614
+ const trimmed = this.longTerm.length - MAX;
615
+ this.longTerm = this.longTerm.slice(0, MAX);
616
+ console.log(`๐Ÿงน Long-term memory pruned by importance: removed ${trimmed} lowest-importance entries`);
617
+ }
618
+
619
+ /**
620
+ * Print memory contents (debug)
621
+ */
622
+ print(agentName = '') {
623
+ const prefix = agentName ? `[${agentName}]` : '';
624
+ console.log(`\n๐Ÿง  ${prefix} Memory System:`);
625
+ console.log(` Short-term (${this.shortTerm.length} items):`);
626
+ this.shortTerm.forEach(m => {
627
+ const expires = m.expiresAt ? ` [expires: ${new Date(m.expiresAt).toLocaleString()}]` : '';
628
+ const imp = m.importance ? ` [imp:${m.importance}]` : '';
629
+ console.log(` ๐Ÿ“ [${m.category}] ${m.content}${expires}${imp}`);
630
+ });
631
+ console.log(` Long-term (${this.longTerm.length} items):`);
632
+ this.longTerm.forEach(m => {
633
+ const imp = m.importance ? ` [imp:${m.importance}]` : '';
634
+ console.log(` ๐Ÿ’พ [${m.category}] ${m.content}${imp}`);
635
+ });
636
+ console.log(` History Summaries: ${this.historySummary.size} groups`);
637
+ console.log(` Relationships (${this.relationships.size} colleagues):`);
638
+ for (const [id, rel] of this.relationships) {
639
+ console.log(` ๐Ÿ‘ค ${rel.name} (${id}): ${rel.impression}`);
640
+ }
641
+ }
642
+ }