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,608 @@
1
+ /**
2
+ * Session Manager - Conversation context and state persistence
3
+ *
4
+ * Distilled from OpenClaw's session system (vendor/openclaw/src/sessions/
5
+ * and vendor/openclaw/src/config/sessions/)
6
+ * Re-implemented as an enterprise "engagement tracking / conversation memory" system
7
+ *
8
+ * Features:
9
+ * - Session creation and lifecycle management
10
+ * - Session key derivation (agent + channel + peer)
11
+ * - Message transcript storage with configurable depth
12
+ * - Session metadata and tagging
13
+ * - Auto-pruning of stale sessions
14
+ * - Send policy enforcement (allow/deny per session)
15
+ * - Session serialization for persistence
16
+ */
17
+ import { v4 as uuidv4 } from 'uuid';
18
+
19
+ /**
20
+ * Session states
21
+ */
22
+ export const SessionState = {
23
+ ACTIVE: 'active',
24
+ IDLE: 'idle',
25
+ ARCHIVED: 'archived',
26
+ EXPIRED: 'expired',
27
+ };
28
+
29
+ /**
30
+ * Send policy decisions (controls whether agent can send in a session)
31
+ */
32
+ export const SendPolicy = {
33
+ ALLOW: 'allow',
34
+ DENY: 'deny',
35
+ };
36
+
37
+ /**
38
+ * Build a deterministic session key from components
39
+ * Distilled from OpenClaw's session-key.ts pattern
40
+ *
41
+ * @param {object} params
42
+ * @param {string} params.agentId - Agent identifier
43
+ * @param {string} params.channel - Communication channel (e.g., 'web', 'chat', 'email')
44
+ * @param {string} params.peerId - Peer identifier (user, group, etc.)
45
+ * @param {string} params.peerKind - Peer type ('direct', 'group', 'channel')
46
+ * @returns {string} Normalized session key
47
+ */
48
+ export function buildSessionKey({ agentId, channel = 'default', peerId = '', peerKind = 'direct' }) {
49
+ const parts = [
50
+ agentId?.trim().toLowerCase() || 'default',
51
+ channel.trim().toLowerCase(),
52
+ peerKind.trim().toLowerCase(),
53
+ ];
54
+ if (peerId) {
55
+ parts.push(peerId.trim().toLowerCase());
56
+ }
57
+ return parts.join(':');
58
+ }
59
+
60
+ /**
61
+ * Session Entry - A single conversation session
62
+ */
63
+ class Session {
64
+ /**
65
+ * @param {object} config
66
+ * @param {string} config.sessionKey - Deterministic key
67
+ * @param {string} config.agentId - Agent involved
68
+ * @param {string} config.channel - Communication channel
69
+ * @param {string} config.peerId - The other party
70
+ * @param {string} config.peerKind - Type of peer
71
+ * @param {number} config.maxTranscriptLength - Max messages in transcript
72
+ */
73
+ constructor(config) {
74
+ this.id = uuidv4();
75
+ this.sessionKey = config.sessionKey;
76
+ this.agentId = config.agentId;
77
+ this.channel = config.channel || 'default';
78
+ this.peerId = config.peerId || '';
79
+ this.peerKind = config.peerKind || 'direct';
80
+
81
+ this.state = SessionState.ACTIVE;
82
+ this.sendPolicy = SendPolicy.ALLOW;
83
+
84
+ // Transcript: ordered list of messages
85
+ this.transcript = [];
86
+ this.maxTranscriptLength = config.maxTranscriptLength ?? 100;
87
+
88
+ // Metadata
89
+ this.metadata = {};
90
+ this.tags = new Set();
91
+ this.tokenUsage = { input: 0, output: 0 };
92
+ this.messageCount = 0;
93
+ this.toolCallCount = 0;
94
+
95
+ // Timestamps
96
+ this.createdAt = new Date();
97
+ this.lastActiveAt = new Date();
98
+ this.expiresAt = null;
99
+
100
+ // Optional label for display
101
+ this.label = '';
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Session Manager - Manages all active sessions
107
+ *
108
+ * Distilled from OpenClaw's session store (vendor/openclaw/src/config/sessions/store.ts)
109
+ * and session management patterns
110
+ */
111
+ export class SessionManager {
112
+ /**
113
+ * @param {object} options
114
+ * @param {number} options.maxSessions - Maximum concurrent sessions
115
+ * @param {number} options.sessionTTL - Session time-to-live in ms (0 = no expiry)
116
+ * @param {number} options.idleTimeout - Time before marking idle (ms)
117
+ * @param {number} options.pruneInterval - Auto-prune check interval (ms)
118
+ * @param {number} options.maxTranscriptLength - Default max transcript messages per session
119
+ */
120
+ constructor(options = {}) {
121
+ this.maxSessions = options.maxSessions ?? 500;
122
+ this.sessionTTL = options.sessionTTL ?? 0; // No expiry by default
123
+ this.idleTimeout = options.idleTimeout ?? 30 * 60 * 1000; // 30 min
124
+ this.pruneInterval = options.pruneInterval ?? 5 * 60 * 1000; // 5 min
125
+ this.maxTranscriptLength = options.maxTranscriptLength ?? 100;
126
+
127
+ /** @type {Map<string, Session>} sessionKey => Session */
128
+ this.sessions = new Map();
129
+
130
+ /** @type {Map<string, Set<string>>} agentId => Set<sessionKey> */
131
+ this.sessionsByAgent = new Map();
132
+
133
+ // Send policy rules
134
+ this.sendPolicyRules = [];
135
+
136
+ // Auto-prune timer
137
+ this._pruneTimer = null;
138
+ }
139
+
140
+ /**
141
+ * Get or create a session for the given parameters
142
+ *
143
+ * @param {object} params
144
+ * @param {string} params.agentId
145
+ * @param {string} params.channel
146
+ * @param {string} params.peerId
147
+ * @param {string} params.peerKind
148
+ * @returns {Session}
149
+ */
150
+ getOrCreate(params) {
151
+ const key = buildSessionKey(params);
152
+ let session = this.sessions.get(key);
153
+
154
+ if (session) {
155
+ // Reactivate if expired or archived
156
+ if (session.state === SessionState.EXPIRED || session.state === SessionState.ARCHIVED) {
157
+ session.state = SessionState.ACTIVE;
158
+ }
159
+ session.lastActiveAt = new Date();
160
+ return session;
161
+ }
162
+
163
+ // Create new session
164
+ session = new Session({
165
+ sessionKey: key,
166
+ agentId: params.agentId,
167
+ channel: params.channel || 'default',
168
+ peerId: params.peerId || '',
169
+ peerKind: params.peerKind || 'direct',
170
+ maxTranscriptLength: this.maxTranscriptLength,
171
+ });
172
+
173
+ // Apply send policy
174
+ session.sendPolicy = this._resolveSendPolicy(session);
175
+
176
+ // Apply TTL
177
+ if (this.sessionTTL > 0) {
178
+ session.expiresAt = new Date(Date.now() + this.sessionTTL);
179
+ }
180
+
181
+ // Enforce session limit (evict oldest idle session)
182
+ if (this.sessions.size >= this.maxSessions) {
183
+ this._evictOldest();
184
+ }
185
+
186
+ this.sessions.set(key, session);
187
+
188
+ // Track by agent
189
+ if (!this.sessionsByAgent.has(params.agentId)) {
190
+ this.sessionsByAgent.set(params.agentId, new Set());
191
+ }
192
+ this.sessionsByAgent.get(params.agentId).add(key);
193
+
194
+ return session;
195
+ }
196
+
197
+ /**
198
+ * Get session by key
199
+ * @param {string} sessionKey
200
+ * @returns {Session|null}
201
+ */
202
+ get(sessionKey) {
203
+ return this.sessions.get(sessionKey) || null;
204
+ }
205
+
206
+ /**
207
+ * Record a message in a session's transcript
208
+ *
209
+ * @param {string} sessionKey
210
+ * @param {object} message
211
+ * @param {string} message.role - 'user', 'assistant', 'system', 'tool'
212
+ * @param {string} message.content - Message content
213
+ * @param {object} message.metadata - Optional metadata
214
+ * @returns {boolean} Whether the message was recorded
215
+ */
216
+ addMessage(sessionKey, message) {
217
+ const session = this.sessions.get(sessionKey);
218
+ if (!session) return false;
219
+
220
+ const entry = {
221
+ id: uuidv4(),
222
+ role: message.role || 'user',
223
+ content: message.content || '',
224
+ metadata: message.metadata || {},
225
+ timestamp: new Date(),
226
+ };
227
+
228
+ session.transcript.push(entry);
229
+ session.messageCount++;
230
+ session.lastActiveAt = new Date();
231
+
232
+ // Enforce transcript limit
233
+ if (session.transcript.length > session.maxTranscriptLength) {
234
+ session.transcript.shift();
235
+ }
236
+
237
+ // Reactivate idle session
238
+ if (session.state === SessionState.IDLE) {
239
+ session.state = SessionState.ACTIVE;
240
+ }
241
+
242
+ return true;
243
+ }
244
+
245
+ /**
246
+ * Record token usage in a session
247
+ * @param {string} sessionKey
248
+ * @param {number} inputTokens
249
+ * @param {number} outputTokens
250
+ */
251
+ recordTokenUsage(sessionKey, inputTokens = 0, outputTokens = 0) {
252
+ const session = this.sessions.get(sessionKey);
253
+ if (!session) return;
254
+ session.tokenUsage.input += inputTokens;
255
+ session.tokenUsage.output += outputTokens;
256
+ }
257
+
258
+ /**
259
+ * Record a tool call in a session
260
+ * @param {string} sessionKey
261
+ */
262
+ recordToolCall(sessionKey) {
263
+ const session = this.sessions.get(sessionKey);
264
+ if (session) session.toolCallCount++;
265
+ }
266
+
267
+ /**
268
+ * Get transcript for a session
269
+ * @param {string} sessionKey
270
+ * @param {number} limit - Max messages to return (most recent)
271
+ * @returns {Array}
272
+ */
273
+ getTranscript(sessionKey, limit = 50) {
274
+ const session = this.sessions.get(sessionKey);
275
+ if (!session) return [];
276
+ const transcript = session.transcript;
277
+ return limit ? transcript.slice(-limit) : [...transcript];
278
+ }
279
+
280
+ /**
281
+ * Archive a session (keep data but mark inactive)
282
+ * @param {string} sessionKey
283
+ */
284
+ archive(sessionKey) {
285
+ const session = this.sessions.get(sessionKey);
286
+ if (session) {
287
+ session.state = SessionState.ARCHIVED;
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Delete a session permanently
293
+ * @param {string} sessionKey
294
+ * @returns {boolean}
295
+ */
296
+ delete(sessionKey) {
297
+ const session = this.sessions.get(sessionKey);
298
+ if (!session) return false;
299
+
300
+ // Remove from agent index
301
+ const agentSessions = this.sessionsByAgent.get(session.agentId);
302
+ if (agentSessions) {
303
+ agentSessions.delete(sessionKey);
304
+ if (agentSessions.size === 0) {
305
+ this.sessionsByAgent.delete(session.agentId);
306
+ }
307
+ }
308
+
309
+ this.sessions.delete(sessionKey);
310
+ return true;
311
+ }
312
+
313
+ /**
314
+ * Reset a session (clear transcript, keep metadata)
315
+ * @param {string} sessionKey
316
+ */
317
+ reset(sessionKey) {
318
+ const session = this.sessions.get(sessionKey);
319
+ if (!session) return;
320
+ session.transcript = [];
321
+ session.messageCount = 0;
322
+ session.toolCallCount = 0;
323
+ session.tokenUsage = { input: 0, output: 0 };
324
+ session.state = SessionState.ACTIVE;
325
+ session.lastActiveAt = new Date();
326
+ }
327
+
328
+ /**
329
+ * Get all sessions for an agent
330
+ * @param {string} agentId
331
+ * @returns {Session[]}
332
+ */
333
+ getAgentSessions(agentId) {
334
+ const keys = this.sessionsByAgent.get(agentId);
335
+ if (!keys) return [];
336
+ return [...keys].map(k => this.sessions.get(k)).filter(Boolean);
337
+ }
338
+
339
+ // ========================================================================
340
+ // Send Policy
341
+ // ========================================================================
342
+
343
+ /**
344
+ * Add a send policy rule
345
+ *
346
+ * @param {object} rule
347
+ * @param {string} rule.action - 'allow' or 'deny'
348
+ * @param {object} rule.match - Matching criteria
349
+ * @param {string} rule.match.channel - Match channel
350
+ * @param {string} rule.match.peerKind - Match peer kind
351
+ * @param {string} rule.match.agentId - Match agent
352
+ */
353
+ addSendPolicyRule(rule) {
354
+ this.sendPolicyRules.push({
355
+ action: rule.action === 'deny' ? SendPolicy.DENY : SendPolicy.ALLOW,
356
+ match: rule.match || {},
357
+ });
358
+ }
359
+
360
+ /**
361
+ * Resolve send policy for a session
362
+ * Distilled from OpenClaw's send-policy.ts pattern
363
+ * @param {Session} session
364
+ * @returns {string} 'allow' or 'deny'
365
+ */
366
+ _resolveSendPolicy(session) {
367
+ for (const rule of this.sendPolicyRules) {
368
+ const match = rule.match;
369
+ if (match.channel && match.channel !== session.channel) continue;
370
+ if (match.peerKind && match.peerKind !== session.peerKind) continue;
371
+ if (match.agentId && match.agentId !== session.agentId) continue;
372
+ return rule.action;
373
+ }
374
+ return SendPolicy.ALLOW;
375
+ }
376
+
377
+ // ========================================================================
378
+ // Pruning & Eviction
379
+ // ========================================================================
380
+
381
+ /**
382
+ * Start auto-pruning timer
383
+ */
384
+ startPruning() {
385
+ if (this._pruneTimer) return;
386
+ this._pruneTimer = setInterval(() => this.prune(), this.pruneInterval);
387
+ }
388
+
389
+ /**
390
+ * Stop auto-pruning timer
391
+ */
392
+ stopPruning() {
393
+ if (this._pruneTimer) {
394
+ clearInterval(this._pruneTimer);
395
+ this._pruneTimer = null;
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Prune expired and stale sessions
401
+ * @returns {number} Number of sessions pruned
402
+ */
403
+ prune() {
404
+ const now = Date.now();
405
+ let pruned = 0;
406
+
407
+ for (const [key, session] of this.sessions) {
408
+ // Check TTL expiry
409
+ if (session.expiresAt && session.expiresAt.getTime() <= now) {
410
+ this.delete(key);
411
+ pruned++;
412
+ continue;
413
+ }
414
+
415
+ // Check idle timeout
416
+ if (
417
+ session.state === SessionState.ACTIVE &&
418
+ this.idleTimeout > 0 &&
419
+ now - session.lastActiveAt.getTime() > this.idleTimeout
420
+ ) {
421
+ session.state = SessionState.IDLE;
422
+ }
423
+ }
424
+
425
+ return pruned;
426
+ }
427
+
428
+ /**
429
+ * Evict the oldest idle session to make room
430
+ */
431
+ _evictOldest() {
432
+ let oldestKey = null;
433
+ let oldestTime = Infinity;
434
+
435
+ // Prefer evicting idle/archived sessions
436
+ for (const [key, session] of this.sessions) {
437
+ if (
438
+ (session.state === SessionState.IDLE || session.state === SessionState.ARCHIVED) &&
439
+ session.lastActiveAt.getTime() < oldestTime
440
+ ) {
441
+ oldestKey = key;
442
+ oldestTime = session.lastActiveAt.getTime();
443
+ }
444
+ }
445
+
446
+ // If no idle sessions, evict oldest active
447
+ if (!oldestKey) {
448
+ for (const [key, session] of this.sessions) {
449
+ if (session.lastActiveAt.getTime() < oldestTime) {
450
+ oldestKey = key;
451
+ oldestTime = session.lastActiveAt.getTime();
452
+ }
453
+ }
454
+ }
455
+
456
+ if (oldestKey) {
457
+ this.delete(oldestKey);
458
+ }
459
+ }
460
+
461
+ // ========================================================================
462
+ // Query & Introspection
463
+ // ========================================================================
464
+
465
+ /**
466
+ * List all sessions with optional filters
467
+ * @param {object} filters
468
+ * @param {string} filters.agentId
469
+ * @param {string} filters.channel
470
+ * @param {string} filters.state
471
+ * @param {number} filters.limit
472
+ * @returns {Array}
473
+ */
474
+ list(filters = {}) {
475
+ let results = [...this.sessions.values()];
476
+
477
+ if (filters.agentId) results = results.filter(s => s.agentId === filters.agentId);
478
+ if (filters.channel) results = results.filter(s => s.channel === filters.channel);
479
+ if (filters.state) results = results.filter(s => s.state === filters.state);
480
+
481
+ // Sort by last active (most recent first)
482
+ results.sort((a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime());
483
+
484
+ if (filters.limit) results = results.slice(0, filters.limit);
485
+
486
+ return results.map(s => ({
487
+ id: s.id,
488
+ sessionKey: s.sessionKey,
489
+ agentId: s.agentId,
490
+ channel: s.channel,
491
+ peerId: s.peerId,
492
+ peerKind: s.peerKind,
493
+ state: s.state,
494
+ sendPolicy: s.sendPolicy,
495
+ messageCount: s.messageCount,
496
+ toolCallCount: s.toolCallCount,
497
+ tokenUsage: { ...s.tokenUsage },
498
+ label: s.label,
499
+ tags: [...s.tags],
500
+ createdAt: s.createdAt.toISOString(),
501
+ lastActiveAt: s.lastActiveAt.toISOString(),
502
+ expiresAt: s.expiresAt?.toISOString() || null,
503
+ }));
504
+ }
505
+
506
+ /**
507
+ * Get overall summary statistics
508
+ * @returns {object}
509
+ */
510
+ getSummary() {
511
+ const sessions = [...this.sessions.values()];
512
+ const byState = {};
513
+ const byChannel = {};
514
+ let totalMessages = 0;
515
+ let totalTokens = 0;
516
+
517
+ for (const s of sessions) {
518
+ byState[s.state] = (byState[s.state] || 0) + 1;
519
+ byChannel[s.channel] = (byChannel[s.channel] || 0) + 1;
520
+ totalMessages += s.messageCount;
521
+ totalTokens += s.tokenUsage.input + s.tokenUsage.output;
522
+ }
523
+
524
+ return {
525
+ totalSessions: sessions.length,
526
+ byState,
527
+ byChannel,
528
+ totalMessages,
529
+ totalTokens,
530
+ agentCount: this.sessionsByAgent.size,
531
+ };
532
+ }
533
+
534
+ /**
535
+ * Serialize all sessions for persistence
536
+ * @returns {object}
537
+ */
538
+ serialize() {
539
+ const data = [];
540
+ for (const [key, session] of this.sessions) {
541
+ data.push({
542
+ sessionKey: session.sessionKey,
543
+ agentId: session.agentId,
544
+ channel: session.channel,
545
+ peerId: session.peerId,
546
+ peerKind: session.peerKind,
547
+ state: session.state,
548
+ sendPolicy: session.sendPolicy,
549
+ transcript: session.transcript,
550
+ metadata: session.metadata,
551
+ tags: [...session.tags],
552
+ tokenUsage: session.tokenUsage,
553
+ messageCount: session.messageCount,
554
+ toolCallCount: session.toolCallCount,
555
+ label: session.label,
556
+ createdAt: session.createdAt.toISOString(),
557
+ lastActiveAt: session.lastActiveAt.toISOString(),
558
+ expiresAt: session.expiresAt?.toISOString() || null,
559
+ });
560
+ }
561
+ return { sessions: data, savedAt: new Date().toISOString() };
562
+ }
563
+
564
+ /**
565
+ * Restore sessions from serialized data
566
+ * @param {object} data
567
+ */
568
+ restore(data) {
569
+ if (!data || !data.sessions) return;
570
+ for (const entry of data.sessions) {
571
+ try {
572
+ const session = new Session({
573
+ sessionKey: entry.sessionKey,
574
+ agentId: entry.agentId,
575
+ channel: entry.channel,
576
+ peerId: entry.peerId,
577
+ peerKind: entry.peerKind,
578
+ maxTranscriptLength: this.maxTranscriptLength,
579
+ });
580
+ session.state = entry.state || SessionState.ACTIVE;
581
+ session.sendPolicy = entry.sendPolicy || SendPolicy.ALLOW;
582
+ session.transcript = entry.transcript || [];
583
+ session.metadata = entry.metadata || {};
584
+ session.tags = new Set(entry.tags || []);
585
+ session.tokenUsage = entry.tokenUsage || { input: 0, output: 0 };
586
+ session.messageCount = entry.messageCount || 0;
587
+ session.toolCallCount = entry.toolCallCount || 0;
588
+ session.label = entry.label || '';
589
+ session.createdAt = new Date(entry.createdAt);
590
+ session.lastActiveAt = new Date(entry.lastActiveAt);
591
+ session.expiresAt = entry.expiresAt ? new Date(entry.expiresAt) : null;
592
+
593
+ this.sessions.set(session.sessionKey, session);
594
+
595
+ // Rebuild agent index
596
+ if (!this.sessionsByAgent.has(session.agentId)) {
597
+ this.sessionsByAgent.set(session.agentId, new Set());
598
+ }
599
+ this.sessionsByAgent.get(session.agentId).add(session.sessionKey);
600
+ } catch (err) {
601
+ console.error(`[SessionManager] Failed to restore session "${entry.sessionKey}":`, err.message);
602
+ }
603
+ }
604
+ }
605
+ }
606
+
607
+ // Global singleton
608
+ export const sessionManager = new SessionManager();