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.
- package/.dockerignore +33 -0
- package/.nvmrc +1 -0
- package/ARCHITECTURE.md +394 -0
- package/Dockerfile +50 -0
- package/LICENSE +29 -0
- package/README.md +206 -0
- package/bin/i18n.js +46 -0
- package/bin/ideaco.js +494 -0
- package/deploy.sh +15 -0
- package/docker-compose.yml +30 -0
- package/electron/main.cjs +986 -0
- package/electron/preload.cjs +14 -0
- package/electron/web-backends.cjs +854 -0
- package/jsconfig.json +8 -0
- package/next.config.mjs +34 -0
- package/package.json +134 -0
- package/postcss.config.mjs +6 -0
- package/public/demo/dashboard.png +0 -0
- package/public/demo/employee.png +0 -0
- package/public/demo/messages.png +0 -0
- package/public/demo/office.png +0 -0
- package/public/demo/requirement.png +0 -0
- package/public/logo.jpeg +0 -0
- package/public/logo.png +0 -0
- package/scripts/prepare-electron.js +67 -0
- package/scripts/release.js +76 -0
- package/src/app/api/agents/[agentId]/chat/route.js +70 -0
- package/src/app/api/agents/[agentId]/conversations/route.js +35 -0
- package/src/app/api/agents/[agentId]/route.js +106 -0
- package/src/app/api/avatar/route.js +104 -0
- package/src/app/api/browse-dir/route.js +44 -0
- package/src/app/api/chat/route.js +265 -0
- package/src/app/api/company/factory-reset/route.js +43 -0
- package/src/app/api/company/route.js +82 -0
- package/src/app/api/departments/[deptId]/agents/[agentId]/dismiss/route.js +19 -0
- package/src/app/api/departments/route.js +92 -0
- package/src/app/api/group-chat-loop/events/route.js +70 -0
- package/src/app/api/group-chat-loop/route.js +94 -0
- package/src/app/api/mailbox/route.js +100 -0
- package/src/app/api/messages/route.js +14 -0
- package/src/app/api/providers/[id]/configure/route.js +21 -0
- package/src/app/api/providers/[id]/refresh-cookie/route.js +38 -0
- package/src/app/api/providers/[id]/test-cookie/route.js +28 -0
- package/src/app/api/providers/route.js +11 -0
- package/src/app/api/requirements/route.js +242 -0
- package/src/app/api/secretary/route.js +65 -0
- package/src/app/api/system/cli-backends/route.js +91 -0
- package/src/app/api/system/cron/route.js +110 -0
- package/src/app/api/system/knowledge/route.js +104 -0
- package/src/app/api/system/plugins/route.js +40 -0
- package/src/app/api/system/skills/route.js +46 -0
- package/src/app/api/system/status/route.js +46 -0
- package/src/app/api/talent-market/[profileId]/recall/route.js +22 -0
- package/src/app/api/talent-market/[profileId]/route.js +17 -0
- package/src/app/api/talent-market/route.js +26 -0
- package/src/app/api/teams/route.js +773 -0
- package/src/app/api/ws-files/[departmentId]/file/route.js +27 -0
- package/src/app/api/ws-files/[departmentId]/files/route.js +22 -0
- package/src/app/globals.css +130 -0
- package/src/app/layout.jsx +40 -0
- package/src/app/page.jsx +97 -0
- package/src/components/AgentChatModal.jsx +164 -0
- package/src/components/AgentDetailModal.jsx +425 -0
- package/src/components/AgentSpyModal.jsx +481 -0
- package/src/components/AvatarGrid.jsx +29 -0
- package/src/components/BossProfileModal.jsx +162 -0
- package/src/components/CachedAvatar.jsx +77 -0
- package/src/components/ChatPanel.jsx +219 -0
- package/src/components/ChatShared.jsx +255 -0
- package/src/components/DepartmentDetail.jsx +842 -0
- package/src/components/DepartmentView.jsx +367 -0
- package/src/components/FileReference.jsx +260 -0
- package/src/components/FilesView.jsx +465 -0
- package/src/components/GroupChatView.jsx +799 -0
- package/src/components/Mailbox.jsx +926 -0
- package/src/components/MessagesView.jsx +112 -0
- package/src/components/OnboardingGuide.jsx +209 -0
- package/src/components/OrgTree.jsx +151 -0
- package/src/components/Overview.jsx +391 -0
- package/src/components/PixelOffice.jsx +2281 -0
- package/src/components/ProviderGrid.jsx +551 -0
- package/src/components/ProvidersBoard.jsx +16 -0
- package/src/components/RequirementDetail.jsx +1279 -0
- package/src/components/RequirementsBoard.jsx +187 -0
- package/src/components/SecretarySettings.jsx +295 -0
- package/src/components/SetupWizard.jsx +388 -0
- package/src/components/Sidebar.jsx +169 -0
- package/src/components/SystemMonitor.jsx +808 -0
- package/src/components/TalentMarket.jsx +183 -0
- package/src/components/TeamDetail.jsx +697 -0
- package/src/core/agent/base-agent.js +104 -0
- package/src/core/agent/chat-store.js +602 -0
- package/src/core/agent/cli-agent/backends/claude-code/README.md +52 -0
- package/src/core/agent/cli-agent/backends/claude-code/config.js +27 -0
- package/src/core/agent/cli-agent/backends/codebuddy/README.md +236 -0
- package/src/core/agent/cli-agent/backends/codebuddy/config.js +27 -0
- package/src/core/agent/cli-agent/backends/codex/README.md +51 -0
- package/src/core/agent/cli-agent/backends/codex/config.js +27 -0
- package/src/core/agent/cli-agent/backends/index.js +27 -0
- package/src/core/agent/cli-agent/backends/registry.js +580 -0
- package/src/core/agent/cli-agent/index.js +154 -0
- package/src/core/agent/index.js +60 -0
- package/src/core/agent/llm-agent/client.js +320 -0
- package/src/core/agent/llm-agent/index.js +97 -0
- package/src/core/agent/message-bus.js +211 -0
- package/src/core/agent/session.js +608 -0
- package/src/core/agent/tools.js +596 -0
- package/src/core/agent/web-agent/backends/base-backend.js +180 -0
- package/src/core/agent/web-agent/backends/chatgpt/client.js +146 -0
- package/src/core/agent/web-agent/backends/chatgpt/config.js +148 -0
- package/src/core/agent/web-agent/backends/chatgpt/dom-scripts.js +303 -0
- package/src/core/agent/web-agent/backends/index.js +91 -0
- package/src/core/agent/web-agent/index.js +278 -0
- package/src/core/agent/web-agent/web-client.js +407 -0
- package/src/core/employee/base-employee.js +1088 -0
- package/src/core/employee/index.js +35 -0
- package/src/core/employee/knowledge.js +327 -0
- package/src/core/employee/lifecycle.js +990 -0
- package/src/core/employee/memory/index.js +642 -0
- package/src/core/employee/memory/store.js +143 -0
- package/src/core/employee/performance.js +224 -0
- package/src/core/employee/secretary.js +625 -0
- package/src/core/employee/skills.js +398 -0
- package/src/core/index.js +38 -0
- package/src/core/organization/company.js +2600 -0
- package/src/core/organization/department.js +737 -0
- package/src/core/organization/group-chat-loop.js +264 -0
- package/src/core/organization/index.js +8 -0
- package/src/core/organization/persistence.js +111 -0
- package/src/core/organization/team.js +267 -0
- package/src/core/organization/workforce/hr.js +377 -0
- package/src/core/organization/workforce/providers.js +468 -0
- package/src/core/organization/workforce/role-archetypes.js +805 -0
- package/src/core/organization/workforce/talent-market.js +205 -0
- package/src/core/prompts.js +532 -0
- package/src/core/requirement.js +1789 -0
- package/src/core/system/audit.js +483 -0
- package/src/core/system/cron.js +449 -0
- package/src/core/system/index.js +7 -0
- package/src/core/system/plugin.js +2183 -0
- package/src/core/utils/json-parse.js +188 -0
- package/src/core/workspace.js +239 -0
- package/src/lib/api-i18n.js +211 -0
- package/src/lib/avatar.js +268 -0
- package/src/lib/client-store.js +1025 -0
- package/src/lib/config-validator.js +483 -0
- package/src/lib/format-time.js +22 -0
- package/src/lib/hooks.js +414 -0
- package/src/lib/i18n.js +134 -0
- package/src/lib/paths.js +23 -0
- package/src/lib/store.js +72 -0
- package/src/locales/de.js +393 -0
- package/src/locales/en.js +1054 -0
- package/src/locales/es.js +393 -0
- package/src/locales/fr.js +393 -0
- package/src/locales/ja.js +501 -0
- package/src/locales/ko.js +513 -0
- package/src/locales/zh.js +828 -0
- 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();
|