ec-web 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,625 @@
1
+ /**
2
+ * 消息数据源:扫描 data/sessions 下所有渠道的 messages.jsonl。
3
+ *
4
+ * 左列按 agent 聚合;中列列出该 agent 下所有渠道的私聊/群聊会话。
5
+ * 兼容旧参数名 aid/peer:前端的 aid 实际上传 scope id。
6
+ */
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import { resolvePaths } from '../paths.js';
10
+ import { resolveParentDistModule, toFileUrl } from './parent-package.js';
11
+ import { isTransientProtocolMessage, readAllJsonlLines, readJsonFile, } from '../fs-utils.js';
12
+ let contextModulesPromise = null;
13
+ const sessionAdapterPromises = new Map();
14
+ function getContextModules() {
15
+ if (!contextModulesPromise) {
16
+ contextModulesPromise = Promise.all([
17
+ import(toFileUrl(resolveParentDistModule('config', 'peer-role-resolver.js'))),
18
+ import(toFileUrl(resolveParentDistModule('core', 'model', 'config-scope.js'))),
19
+ import(toFileUrl(resolveParentDistModule('core', 'message', 'peer-mode.js'))),
20
+ ]).then(([resolver, modelScope, peerMode]) => ({ resolver, modelScope, peerMode }));
21
+ }
22
+ return contextModulesPromise;
23
+ }
24
+ function getSessionAdapter(baseagent) {
25
+ if (!sessionAdapterPromises.has(baseagent)) {
26
+ const classNames = {
27
+ claude: 'ClaudeSessionFileAdapter',
28
+ codex: 'CodexSessionFileAdapter',
29
+ gemini: 'GeminiSessionFileAdapter',
30
+ };
31
+ const className = classNames[baseagent];
32
+ const promise = !className
33
+ ? Promise.resolve(null)
34
+ : Promise.resolve()
35
+ .then(() => import(toFileUrl(resolveParentDistModule('core', 'session', 'adapters', `${baseagent}-session-file-adapter.js`))))
36
+ .then(mod => mod[className] ? new mod[className]() : null)
37
+ .catch(() => null);
38
+ sessionAdapterPromises.set(baseagent, promise);
39
+ }
40
+ return sessionAdapterPromises.get(baseagent);
41
+ }
42
+ function safeId(value) {
43
+ return Buffer.from(value, 'utf8').toString('base64url');
44
+ }
45
+ function findMessageFiles(root) {
46
+ const out = [];
47
+ const visit = (dir) => {
48
+ let entries;
49
+ try {
50
+ entries = fs.readdirSync(dir, { withFileTypes: true });
51
+ }
52
+ catch {
53
+ return;
54
+ }
55
+ for (const entry of entries) {
56
+ const full = path.join(dir, entry.name);
57
+ if (entry.isDirectory())
58
+ visit(full);
59
+ else if (entry.isFile() && entry.name === 'messages.jsonl')
60
+ out.push(full);
61
+ }
62
+ };
63
+ visit(root);
64
+ return out;
65
+ }
66
+ function readLastJsonlObject(file) {
67
+ const rows = readAllJsonlLines(file);
68
+ return rows.length ? rows[rows.length - 1] : null;
69
+ }
70
+ function loadThreadSessions(dirPath) {
71
+ const threadsDir = path.join(dirPath, '_threads');
72
+ const index = readJsonFile(path.join(threadsDir, 'thread-index.json')) || {};
73
+ return Object.entries(index).map(([threadId, value]) => {
74
+ const sessionId = typeof value === 'string' ? value : value?.sessionId || null;
75
+ const metaFile = typeof value === 'object' && value?.metaFile
76
+ ? value.metaFile
77
+ : sessionId ? `${sessionId}.jsonl` : '';
78
+ const metaRows = metaFile ? readAllJsonlLines(path.join(threadsDir, metaFile)) : [];
79
+ const active = metaRows.length ? metaRows[metaRows.length - 1] : {};
80
+ const replyToMessageIds = Array.from(new Set(metaRows.flatMap(row => {
81
+ const replyContext = row?.metadata?.replyContext;
82
+ const ids = [
83
+ replyContext?.replyToMessageId,
84
+ replyContext?.metadata?.refMessageId,
85
+ ];
86
+ return ids.filter((id) => typeof id === 'string' && id.length > 0);
87
+ })));
88
+ return { threadId, sessionId, active, replyToMessageIds };
89
+ });
90
+ }
91
+ function loadMainSessionIds(dirPath, active) {
92
+ const ids = new Set();
93
+ if (typeof active.id === 'string' && active.id)
94
+ ids.add(active.id);
95
+ let entries;
96
+ try {
97
+ entries = fs.readdirSync(dirPath, { withFileTypes: true });
98
+ }
99
+ catch {
100
+ return ids;
101
+ }
102
+ for (const entry of entries) {
103
+ if (!entry.isFile() || !entry.name.startsWith('meta_') || !entry.name.endsWith('.jsonl'))
104
+ continue;
105
+ const latest = readLastJsonlObject(path.join(dirPath, entry.name));
106
+ if (typeof latest?.id === 'string' && !latest.threadId)
107
+ ids.add(latest.id);
108
+ }
109
+ return ids;
110
+ }
111
+ export function partitionMessages(messages, threadSessions, mainSessionIds) {
112
+ const threadBySessionId = new Map(threadSessions
113
+ .filter(item => item.sessionId)
114
+ .map(item => [item.sessionId, item]));
115
+ // A topic session records the inbound message it is replying to in its
116
+ // metadata. Use only unique references; collisions stay unclassified.
117
+ const threadByReplyMessageId = new Map();
118
+ for (const thread of threadSessions) {
119
+ for (const messageId of thread.replyToMessageIds || []) {
120
+ if (threadByReplyMessageId.has(messageId))
121
+ threadByReplyMessageId.set(messageId, null);
122
+ else
123
+ threadByReplyMessageId.set(messageId, thread);
124
+ }
125
+ }
126
+ const partitions = new Map();
127
+ const ensurePartition = (key, partition, threadId) => {
128
+ let item = partitions.get(key);
129
+ if (!item) {
130
+ item = { partition, threadId, messages: [] };
131
+ partitions.set(key, item);
132
+ }
133
+ return item;
134
+ };
135
+ for (const message of messages) {
136
+ const hasThreadField = Object.prototype.hasOwnProperty.call(message, 'threadId');
137
+ const explicitThreadId = typeof message.threadId === 'string' && message.threadId ? message.threadId : null;
138
+ const inferredThread = !hasThreadField && message.sessionId ? threadBySessionId.get(message.sessionId) : undefined;
139
+ const inferredReplyThread = message.msgId ? threadByReplyMessageId.get(message.msgId) : undefined;
140
+ if (explicitThreadId || inferredThread || inferredReplyThread) {
141
+ const threadId = explicitThreadId || inferredThread?.threadId || inferredReplyThread.threadId;
142
+ ensurePartition(`thread:${threadId}`, 'thread', threadId).messages.push({ ...message, threadId });
143
+ }
144
+ else if (hasThreadField || threadSessions.length === 0 || (message.sessionId && mainSessionIds.has(message.sessionId))) {
145
+ ensurePartition('main', 'main', null).messages.push({ ...message, threadId: null });
146
+ }
147
+ else {
148
+ ensurePartition('legacy', 'legacy', null).messages.push(message);
149
+ }
150
+ }
151
+ return Array.from(partitions.values());
152
+ }
153
+ function listConfiguredAgentAids(root) {
154
+ const agentsDir = path.join(root, 'agents');
155
+ const aids = new Set();
156
+ let entries;
157
+ try {
158
+ entries = fs.readdirSync(agentsDir, { withFileTypes: true });
159
+ }
160
+ catch {
161
+ return aids;
162
+ }
163
+ for (const entry of entries) {
164
+ if (!entry.isDirectory())
165
+ continue;
166
+ const config = readJsonFile(path.join(agentsDir, entry.name, 'config.json'));
167
+ const aid = typeof config?.aid === 'string' ? config.aid : entry.name;
168
+ if (looksLikeAID(aid))
169
+ aids.add(aid);
170
+ }
171
+ return aids;
172
+ }
173
+ function decodeDirSegment(seg) {
174
+ return seg.replace(/%([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
175
+ }
176
+ function parseChannelKey(value) {
177
+ if (typeof value !== 'string')
178
+ return null;
179
+ const parts = value.split('#');
180
+ if (parts.length !== 3)
181
+ return null;
182
+ const [type, selfAID, name] = parts;
183
+ if (!type || !selfAID || !name)
184
+ return null;
185
+ return { type, selfAID, name };
186
+ }
187
+ function looksLikeAID(value) {
188
+ return !!value && value !== 'self' && value !== '_unknown' && /^[^./#\s]+\.[^/#\s]+/.test(value);
189
+ }
190
+ function parseLegacySessionKey(value, channelType) {
191
+ if (typeof value !== 'string')
192
+ return null;
193
+ const parts = value.split('#');
194
+ if (parts.length < 3 || parts[0] !== channelType)
195
+ return null;
196
+ if (!looksLikeAID(parts[1]))
197
+ return null;
198
+ return { type: parts[0], selfAID: parts[1], name: parts[2] || 'main' };
199
+ }
200
+ function pathIdentity(sessionsDir, dirPath) {
201
+ const relParts = path.relative(sessionsDir, dirPath).split(path.sep).filter(Boolean);
202
+ if (!relParts.length)
203
+ return {};
204
+ const firstKey = parseChannelKey(decodeDirSegment(relParts[0]));
205
+ if (firstKey) {
206
+ return {
207
+ channelType: firstKey.type,
208
+ channel: `${firstKey.type}#${firstKey.selfAID}#${firstKey.name}`,
209
+ channelId: relParts[1] ? decodeDirSegment(relParts[1]) : undefined,
210
+ selfAID: firstKey.selfAID,
211
+ };
212
+ }
213
+ if (relParts[0] === 'aun' && relParts.length >= 3) {
214
+ const selfAID = decodeDirSegment(relParts[1]);
215
+ return {
216
+ channelType: 'aun',
217
+ channel: looksLikeAID(selfAID) ? `aun#${selfAID}#main` : undefined,
218
+ channelId: decodeDirSegment(relParts[2]),
219
+ selfAID: looksLikeAID(selfAID) ? selfAID : undefined,
220
+ };
221
+ }
222
+ return {
223
+ channelType: decodeDirSegment(relParts[0]),
224
+ channelId: relParts[1] ? decodeDirSegment(relParts[1]) : undefined,
225
+ };
226
+ }
227
+ function inferSelfAID(active, messages, hints) {
228
+ if (looksLikeAID(active?.selfAID))
229
+ return String(active.selfAID);
230
+ if (looksLikeAID(hints.selfAID))
231
+ return hints.selfAID;
232
+ const channelKey = parseChannelKey(active?.channel) || parseChannelKey(active?.channelType);
233
+ if (channelKey)
234
+ return channelKey.selfAID;
235
+ const sessionKey = parseLegacySessionKey(active?.sessionKey, hints.channelType || active?.channelType || '');
236
+ if (sessionKey)
237
+ return sessionKey.selfAID;
238
+ const out = messages.find(m => m.dir === 'out' && m.from && m.from !== 'self');
239
+ if (out?.from)
240
+ return String(out.from);
241
+ const inboundTo = messages.find(m => m.dir === 'in' && m.to && m.to !== 'self');
242
+ if (inboundTo?.to)
243
+ return String(inboundTo.to);
244
+ return 'unknown';
245
+ }
246
+ function normalizeChatIdentity(sessionsDir, dirPath, active, messages) {
247
+ const pathHints = pathIdentity(sessionsDir, dirPath);
248
+ const activeChannelKey = parseChannelKey(active?.channel);
249
+ const activeTypeKey = parseChannelKey(active?.channelType);
250
+ const msgChannelType = messages.find(m => m.channelType)?.channelType;
251
+ const rawType = String(activeChannelKey?.type
252
+ || activeTypeKey?.type
253
+ || active?.channelType
254
+ || msgChannelType
255
+ || pathHints.channelType
256
+ || 'unknown');
257
+ const channelType = parseChannelKey(rawType)?.type || rawType;
258
+ const selfAID = inferSelfAID(active, messages, { ...pathHints, channelType });
259
+ const rawChannel = typeof active?.channel === 'string' && active.channel
260
+ ? active.channel
261
+ : pathHints.channel || `${channelType}#${selfAID}#main`;
262
+ const channelKey = parseChannelKey(rawChannel);
263
+ const channel = channelKey
264
+ ? `${channelKey.type}#${channelKey.selfAID}#${channelKey.name}`
265
+ : (selfAID !== 'unknown' ? `${channelType}#${selfAID}#main` : rawChannel);
266
+ const channelId = String(active.channelId || pathHints.channelId || path.basename(dirPath));
267
+ return { channelType, channel, channelId, selfAID };
268
+ }
269
+ function chatDisplayId(active, messages) {
270
+ const metadata = active?.metadata ?? {};
271
+ const chatType = active?.chatType || messages.find(m => m.chatType)?.chatType || 'private';
272
+ const groupId = chatType === 'group'
273
+ ? String(metadata.groupId || active?.channelId || messages.find(m => m.groupId)?.groupId || '')
274
+ : null;
275
+ const groupName = chatType === 'group' && metadata.groupName ? String(metadata.groupName) : null;
276
+ if (chatType === 'group') {
277
+ return {
278
+ peerId: groupId || String(active?.channelId || 'group'),
279
+ peerName: groupName,
280
+ groupId,
281
+ groupName,
282
+ };
283
+ }
284
+ const peerId = String(metadata.peerId || active?.channelId || messages.find(m => m.dir === 'in')?.from || messages.find(m => m.dir === 'out')?.to || 'unknown');
285
+ return {
286
+ peerId,
287
+ peerName: metadata.peerName ? String(metadata.peerName) : null,
288
+ groupId: null,
289
+ groupName: null,
290
+ };
291
+ }
292
+ function loadChats() {
293
+ const paths = resolvePaths();
294
+ const sessionsDir = paths.sessionsDir;
295
+ const configuredAids = listConfiguredAgentAids(paths.root);
296
+ const files = findMessageFiles(sessionsDir);
297
+ const chats = [];
298
+ for (const file of files) {
299
+ const dirPath = path.dirname(file);
300
+ const active = readJsonFile(path.join(dirPath, 'active.json')) ?? {};
301
+ const messages = readAllJsonlLines(file)
302
+ .filter(m => !isTransientProtocolMessage(m));
303
+ if (!messages.length)
304
+ continue;
305
+ const identity = normalizeChatIdentity(sessionsDir, dirPath, active, messages);
306
+ const { channelType, channel, channelId, selfAID } = identity;
307
+ if (configuredAids.size && !configuredAids.has(selfAID))
308
+ continue;
309
+ const chatType = String(active.chatType || messages.find(m => m.chatType)?.chatType || 'private');
310
+ const display = chatDisplayId({ ...active, chatType, channelId }, messages);
311
+ const threadSessions = loadThreadSessions(dirPath);
312
+ const threadById = new Map(threadSessions.map(item => [item.threadId, item]));
313
+ const mainSessionIds = loadMainSessionIds(dirPath, active);
314
+ for (const item of partitionMessages(messages, threadSessions, mainSessionIds)) {
315
+ if (!item.messages.length)
316
+ continue;
317
+ const thread = item.threadId ? threadById.get(item.threadId) : undefined;
318
+ const partitionActive = item.partition === 'thread' ? thread?.active || {} : item.partition === 'main' ? active : {};
319
+ const lastMsgAt = item.messages.reduce((max, message) => Math.max(max, message.ts || 0), 0);
320
+ const updatedAt = Math.max(Number(partitionActive.updatedAt || 0), lastMsgAt);
321
+ chats.push({
322
+ id: safeId(`${selfAID}\n${channelType}\n${channelId}\n${chatType}\n${item.partition}\n${item.threadId || ''}`),
323
+ dirPath,
324
+ channelType,
325
+ channel,
326
+ channelId,
327
+ selfAID,
328
+ chatType,
329
+ peerId: display.peerId,
330
+ peerName: display.peerName,
331
+ groupId: display.groupId,
332
+ groupName: display.groupName,
333
+ partition: item.partition,
334
+ threadId: item.threadId,
335
+ threadName: item.partition === 'thread'
336
+ ? String(partitionActive.name || partitionActive.metadata?.topicName || item.threadId)
337
+ : null,
338
+ updatedAt,
339
+ messages: item.messages,
340
+ active: partitionActive,
341
+ });
342
+ }
343
+ }
344
+ return chats;
345
+ }
346
+ function scopeId(chat) {
347
+ return safeId(chat.selfAID);
348
+ }
349
+ function legacyChannelScopeId(chat) {
350
+ return safeId(`${chat.selfAID}\n${chat.channelType}\n${chat.channel}`);
351
+ }
352
+ function buildScopes(chats) {
353
+ const map = new Map();
354
+ const seenChats = new Map();
355
+ const seenChannels = new Map();
356
+ const seenChannelTypes = new Map();
357
+ for (const chat of chats) {
358
+ const id = scopeId(chat);
359
+ let scope = map.get(id);
360
+ if (!scope) {
361
+ scope = {
362
+ id,
363
+ aid: id,
364
+ selfAID: chat.selfAID,
365
+ totalIn: 0,
366
+ totalOut: 0,
367
+ peerCount: 0,
368
+ groupCount: 0,
369
+ channelCount: 0,
370
+ channelTypes: [],
371
+ channels: [],
372
+ lastAt: 0,
373
+ };
374
+ map.set(id, scope);
375
+ seenChats.set(id, new Set());
376
+ seenChannels.set(id, new Set());
377
+ seenChannelTypes.set(id, new Set());
378
+ }
379
+ const channelSeen = seenChannels.get(id);
380
+ if (!channelSeen.has(chat.channel)) {
381
+ channelSeen.add(chat.channel);
382
+ scope.channels.push(chat.channel);
383
+ scope.channelCount = channelSeen.size;
384
+ }
385
+ const typeSeen = seenChannelTypes.get(id);
386
+ if (!typeSeen.has(chat.channelType)) {
387
+ typeSeen.add(chat.channelType);
388
+ scope.channelTypes.push(chat.channelType);
389
+ scope.channelTypes.sort();
390
+ }
391
+ let hasIn = false;
392
+ let hasOut = false;
393
+ for (const msg of chat.messages) {
394
+ if (msg.dir === 'in') {
395
+ scope.totalIn++;
396
+ hasIn = true;
397
+ }
398
+ else if (msg.dir === 'out') {
399
+ scope.totalOut++;
400
+ hasOut = true;
401
+ }
402
+ if (msg.ts > scope.lastAt)
403
+ scope.lastAt = msg.ts;
404
+ }
405
+ if (hasIn || hasOut) {
406
+ const chatKey = `${chat.channel}\n${chat.channelId}\n${chat.chatType}\n${chat.partition}\n${chat.threadId || ''}`;
407
+ const seen = seenChats.get(id);
408
+ if (!seen.has(chatKey)) {
409
+ seen.add(chatKey);
410
+ scope.peerCount++;
411
+ if (chat.chatType === 'group')
412
+ scope.groupCount++;
413
+ }
414
+ }
415
+ if (chat.updatedAt > scope.lastAt)
416
+ scope.lastAt = chat.updatedAt;
417
+ }
418
+ return Array.from(map.values())
419
+ .sort((a, b) => b.lastAt - a.lastAt);
420
+ }
421
+ function channelName(channel) {
422
+ return parseChannelKey(channel)?.name ?? null;
423
+ }
424
+ function chatKey(chat) {
425
+ return safeId(`${chat.channelType}\n${chat.channel}\n${chat.channelId}\n${chat.chatType}\n${chat.partition}\n${chat.threadId || ''}`);
426
+ }
427
+ function mergePeerInfo(chats) {
428
+ const map = new Map();
429
+ for (const chat of chats) {
430
+ const key = chatKey(chat);
431
+ let peer = map.get(key);
432
+ if (!peer) {
433
+ peer = {
434
+ id: key,
435
+ peerId: key,
436
+ peerName: chat.chatType === 'group' ? (chat.groupName || chat.groupId || chat.channelId) : (chat.peerName || chat.peerId),
437
+ channel: chat.channel,
438
+ channelType: chat.channelType,
439
+ channelName: channelName(chat.channel),
440
+ chatType: chat.chatType,
441
+ groupId: chat.groupId,
442
+ groupName: chat.groupName,
443
+ partition: chat.partition,
444
+ threadId: chat.threadId,
445
+ threadName: chat.threadName,
446
+ inbound: 0,
447
+ outbound: 0,
448
+ lastAt: 0,
449
+ };
450
+ map.set(key, peer);
451
+ }
452
+ for (const msg of chat.messages) {
453
+ if (msg.dir === 'in')
454
+ peer.inbound++;
455
+ else if (msg.dir === 'out')
456
+ peer.outbound++;
457
+ if (msg.ts > peer.lastAt)
458
+ peer.lastAt = msg.ts;
459
+ }
460
+ if (chat.updatedAt > peer.lastAt)
461
+ peer.lastAt = chat.updatedAt;
462
+ }
463
+ return Array.from(map.values()).sort((a, b) => b.lastAt - a.lastAt);
464
+ }
465
+ function selectedChats(chats, scope) {
466
+ if (!scope)
467
+ return [];
468
+ return chats.filter(chat => scopeId(chat) === scope);
469
+ }
470
+ function messagesFor(chats, peer) {
471
+ const selected = peer
472
+ ? chats.filter(chat => chatKey(chat) === peer)
473
+ : chats;
474
+ const messages = selected.flatMap(chat => chat.messages.map(msg => ({
475
+ ...msg,
476
+ channel: chat.channel,
477
+ channelType: chat.channelType,
478
+ selfAID: chat.selfAID,
479
+ peerName: chat.peerName,
480
+ groupName: chat.groupName,
481
+ partition: chat.partition,
482
+ threadName: chat.threadName,
483
+ })));
484
+ messages.sort((a, b) => (a.ts || 0) - (b.ts || 0));
485
+ return messages.length > 1000 ? messages.slice(-1000) : messages;
486
+ }
487
+ function latestInbound(chat) {
488
+ for (let i = chat.messages.length - 1; i >= 0; i--) {
489
+ if (chat.messages[i].dir === 'in')
490
+ return chat.messages[i];
491
+ }
492
+ return undefined;
493
+ }
494
+ async function sessionTurns(active, messages) {
495
+ const baseagent = String(active.baseagent || active.agentType || '').toLowerCase();
496
+ const agentSessionId = typeof active.agentSessionId === 'string' ? active.agentSessionId : '';
497
+ const projectPath = typeof active.projectPath === 'string' ? active.projectPath : '';
498
+ if (baseagent && agentSessionId && projectPath) {
499
+ try {
500
+ const adapter = await getSessionAdapter(baseagent);
501
+ const turns = Number(adapter?.getFileInfo(projectPath, agentSessionId)?.turns);
502
+ if (Number.isFinite(turns) && turns > 0)
503
+ return turns;
504
+ }
505
+ catch { }
506
+ }
507
+ for (let i = messages.length - 1; i >= 0; i--) {
508
+ const turns = Number(messages[i].numTurns);
509
+ if (Number.isFinite(turns) && turns >= 0)
510
+ return turns;
511
+ }
512
+ return 0;
513
+ }
514
+ async function buildSessionContext(chat) {
515
+ if (!chat || chat.partition === 'legacy')
516
+ return null;
517
+ const active = chat.active || {};
518
+ const inbound = latestInbound(chat);
519
+ const actorId = String(inbound?.from
520
+ || active.metadata?.peerId
521
+ || chat.peerId
522
+ || chat.channelId
523
+ || '');
524
+ const conversationId = String(chat.groupId || chat.channelId || chat.peerId || '');
525
+ let role = inbound?.permMode || null;
526
+ let permissionMode = active.permissionMode || null;
527
+ let chatMode = null;
528
+ const peerKey = `${chat.channelType}#${encodeURIComponent(conversationId)}`;
529
+ try {
530
+ const { resolver, modelScope, peerMode } = await getContextModules();
531
+ const detail = resolver.resolvePeerRoleDetail({
532
+ selfAid: chat.selfAID,
533
+ channelKey: chat.channel,
534
+ channelType: chat.channelType,
535
+ chatType: chat.chatType === 'group' ? 'group' : 'private',
536
+ actorId,
537
+ conversationId,
538
+ peerType: inbound?.peerType || active.metadata?.peerType,
539
+ });
540
+ role = detail.effectiveRole || 'none';
541
+ permissionMode = modelScope.resolvePermissionMode({ self: chat.selfAID, peerKey, role });
542
+ chatMode = peerMode.resolveChatMode({
543
+ self: chat.selfAID,
544
+ peerKey,
545
+ role,
546
+ chatType: chat.chatType,
547
+ peerType: inbound?.peerType || active.metadata?.peerType,
548
+ });
549
+ }
550
+ catch { }
551
+ return {
552
+ sessionId: active.agentSessionId || active.id || null,
553
+ evolclawSessionId: active.id || null,
554
+ name: active.name || null,
555
+ baseagent: active.baseagent || active.agentType || null,
556
+ chatMode,
557
+ permissionMode,
558
+ role,
559
+ turns: await sessionTurns(active, chat.messages),
560
+ status: active.activeTask ? 'processing' : 'idle',
561
+ updatedAt: Number(active.updatedAt || chat.updatedAt || 0),
562
+ };
563
+ }
564
+ async function buildSnapshot(params) {
565
+ const chats = loadChats();
566
+ const scopes = buildScopes(chats);
567
+ const requestedScope = params.scope || params.aid || null;
568
+ const scope = requestedScope && !scopes.some(s => s.id === requestedScope)
569
+ ? (chats.find(chat => legacyChannelScopeId(chat) === requestedScope)
570
+ ? scopeId(chats.find(chat => legacyChannelScopeId(chat) === requestedScope))
571
+ : requestedScope)
572
+ : requestedScope;
573
+ const peer = params.peer || null;
574
+ const inScope = selectedChats(chats, scope);
575
+ const peers = scope ? mergePeerInfo(inScope) : [];
576
+ const messages = scope ? messagesFor(inScope, peer) : [];
577
+ const currentChat = peer ? inScope.find(chat => chatKey(chat) === peer) : undefined;
578
+ const sessionContext = await buildSessionContext(currentChat);
579
+ return {
580
+ scopes,
581
+ aids: scopes,
582
+ peers,
583
+ messages,
584
+ scope,
585
+ aid: scope,
586
+ peer,
587
+ sessionContext,
588
+ };
589
+ }
590
+ export const msgSource = {
591
+ kind: 'msg',
592
+ async snapshot(params = {}) {
593
+ return await buildSnapshot(params);
594
+ },
595
+ subscribe(params, push) {
596
+ const sessionsDir = resolvePaths().sessionsDir;
597
+ let watcher = null;
598
+ let debounce = null;
599
+ const fire = () => {
600
+ if (debounce)
601
+ clearTimeout(debounce);
602
+ debounce = setTimeout(async () => {
603
+ try {
604
+ push(await buildSnapshot(params));
605
+ }
606
+ catch { /* ignore */ }
607
+ }, 150);
608
+ };
609
+ try {
610
+ watcher = fs.watch(sessionsDir, { recursive: true }, (_evt, filename) => {
611
+ const name = String(filename || '');
612
+ if (name.endsWith('messages.jsonl') || name.endsWith('active.json')
613
+ || name.endsWith('thread-index.json') || (name.includes('_threads') && name.endsWith('.jsonl')))
614
+ fire();
615
+ });
616
+ }
617
+ catch { /* sessionsDir may not exist yet */ }
618
+ return () => {
619
+ if (watcher)
620
+ watcher.close();
621
+ if (debounce)
622
+ clearTimeout(debounce);
623
+ };
624
+ },
625
+ };
@@ -0,0 +1,77 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { createRequire } from 'module';
4
+ import { fileURLToPath } from 'url';
5
+ let packageRootCache = null;
6
+ function isEvolclawPackageRoot(dir) {
7
+ const pkgPath = path.join(dir, 'package.json');
8
+ if (!fs.existsSync(pkgPath))
9
+ return false;
10
+ try {
11
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
12
+ return pkg?.name === 'evolclaw';
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ function findUp(start) {
19
+ let dir = path.resolve(start);
20
+ while (true) {
21
+ if (isEvolclawPackageRoot(dir))
22
+ return dir;
23
+ const parent = path.dirname(dir);
24
+ if (parent === dir)
25
+ return null;
26
+ dir = parent;
27
+ }
28
+ }
29
+ function candidateStarts() {
30
+ const here = path.dirname(fileURLToPath(import.meta.url));
31
+ const starts = [
32
+ process.env.EVOLCLAW_PACKAGE_ROOT,
33
+ here,
34
+ process.cwd(),
35
+ process.argv[1] ? path.dirname(process.argv[1]) : undefined,
36
+ ];
37
+ return [...new Set(starts.filter((item) => !!item))];
38
+ }
39
+ export function resolveEvolclawPackageRoot() {
40
+ if (packageRootCache)
41
+ return packageRootCache;
42
+ const searched = [];
43
+ for (const start of candidateStarts()) {
44
+ searched.push(start);
45
+ const root = findUp(start);
46
+ if (root) {
47
+ packageRootCache = root;
48
+ return root;
49
+ }
50
+ }
51
+ try {
52
+ const require = createRequire(import.meta.url);
53
+ const pkgJson = require.resolve('evolclaw/package.json');
54
+ const root = path.dirname(pkgJson);
55
+ if (isEvolclawPackageRoot(root)) {
56
+ packageRootCache = root;
57
+ return root;
58
+ }
59
+ }
60
+ catch {
61
+ // Fall through to a diagnostic error.
62
+ }
63
+ throw new Error(`Main package root not found. searched=${searched.join(', ')}`);
64
+ }
65
+ export function resolveParentDistModule(...segments) {
66
+ const root = resolveEvolclawPackageRoot();
67
+ const modulePath = path.join(root, 'dist', ...segments);
68
+ if (!fs.existsSync(modulePath)) {
69
+ throw new Error(`Main package dist module not found: ${modulePath}. Run npm run build in ${root}.`);
70
+ }
71
+ return modulePath;
72
+ }
73
+ export function toFileUrl(p) {
74
+ return process.platform === 'win32'
75
+ ? new URL('file:///' + p.replace(/\\/g, '/')).href
76
+ : p;
77
+ }