evolclaw-web 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -29,6 +29,7 @@ import { gatewaySource } from './sources/gateway.js';
29
29
  import { queryStatsForDashboard, queryStatsExplorer, queryStatsByPeer, queryStatsOverview, queryUsageDetail, queryUsedModels } from './sources/stats.js';
30
30
  import { getSessionsAunDir, listLocalAids, listPeers, readMessages } from './fs-utils.js';
31
31
  import { ccProjectsDir } from './paths.js';
32
+ import { detectBaseAgents } from './sources/baseagent-detector.js';
32
33
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
33
34
  const STATIC_DIR = path.join(__dirname, 'static');
34
35
  const TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30天(滑动窗口:每次有效访问刷新 lastActive 自动续期)
@@ -486,7 +487,9 @@ function handleConnection(ws, req, log) {
486
487
  params.project = msg.project;
487
488
  if (msg.agent)
488
489
  params.agent = msg.agent;
489
- dlog(`▸ 订阅 ${msg.view}${msg.aid ? ` aid=${String(msg.aid).split('.')[0]}` : ''}${msg.peer ? ` peer=${String(msg.peer).split('.')[0]}` : ''}${msg.sessionId ? ` session=${String(msg.sessionId).slice(0, 8)}` : ''} from ${ip}`);
490
+ if (msg.baseagent)
491
+ params.baseagent = msg.baseagent;
492
+ dlog(`▸ 订阅 ${msg.view}${msg.aid ? ` aid=${String(msg.aid).split('.')[0]}` : ''}${msg.peer ? ` peer=${String(msg.peer).split('.')[0]}` : ''}${msg.sessionId ? ` session=${String(msg.sessionId).slice(0, 8)}` : ''}${msg.baseagent ? ` baseagent=${msg.baseagent}` : ''} from ${ip}`);
490
493
  await switchSubscription(msg.view, params);
491
494
  return;
492
495
  }
@@ -614,7 +617,13 @@ export async function startWatchWebServer(opts = {}) {
614
617
  return { code: pairingCode, expiresAt: pairingExpiry };
615
618
  }
616
619
  const server = http.createServer((req, res) => {
617
- if (req.method === 'GET' && (req.url || '') === '/api/pair-code') {
620
+ if (req.method === 'GET' && (req.url || '') === '/api/available-baseagents') {
621
+ // Base agent 可用性检测 API
622
+ const available = detectBaseAgents();
623
+ res.writeHead(200, { 'Content-Type': 'application/json' });
624
+ res.end(JSON.stringify(available));
625
+ }
626
+ else if (req.method === 'GET' && (req.url || '') === '/api/pair-code') {
618
627
  // 取码 API:仅真本地直连可用(socket 回环 + 无 x-aun-provider-aid)。
619
628
  // 隧道回连虽然 socket 也是 127.0.0.1,但有 x-aun-provider-aid 头 → 拒绝,
620
629
  // 防止远程访客通过隧道拿到配对码(安全漏洞)。
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * AID 数据源 — 复用 daemon 的 IPC socket(与 evolclaw `watch aid` 同源)。
3
3
  *
4
- * daemon 运行时:拉 aun-aids / aun-aid-stats / status / evolagent.list。
4
+ * daemon 运行时:拉 aun-aids / aun-aid-stats / agent-stats / status / evolagent.list。
5
5
  * daemon 未运行时:降级到读 instance/ 目录的 aid-*.jsonl(精简版 scanInstances)。
6
6
  * IPC 无推送能力,故 1s 轮询 + JSON diff,仅在变化时 push。
7
7
  */
@@ -83,9 +83,10 @@ async function fetchVersion(socket) {
83
83
  }
84
84
  async function buildSnapshot() {
85
85
  const p = resolvePaths();
86
- const [aidsResp, statsResp, statusResp, agentsResp, version] = await Promise.all([
86
+ const [aidsResp, statsResp, agentStatsResp, statusResp, agentsResp, version] = await Promise.all([
87
87
  ipcQuery(p.socket, { type: 'aun-aids' }),
88
88
  ipcQuery(p.socket, { type: 'aun-aid-stats' }),
89
+ ipcQuery(p.socket, { type: 'agent-stats' }),
89
90
  ipcQuery(p.socket, { type: 'status' }),
90
91
  ipcQuery(p.socket, { type: 'evolagent.list' }),
91
92
  fetchVersion(p.socket),
@@ -105,6 +106,7 @@ async function buildSnapshot() {
105
106
  daemonRunning: true,
106
107
  aids: aidsResp?.aids ?? [],
107
108
  stats: statsResp?.stats ?? [],
109
+ agentStats: agentStatsResp?.stats ?? [],
108
110
  status: statusResp ?? null,
109
111
  agents: agentsResp?.agents ?? [],
110
112
  version: version ?? null,
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Base agent 环境检测 — 检测 Claude 和 Codex 的可用性。
3
+ *
4
+ * 检测条件:
5
+ * - Claude: ~/.claude/projects/ 目录存在
6
+ * - Codex: ~/.codex/sessions/ 和 ~/.codex/state_*.sqlite 存在,且 Node 版本 >= 22.5
7
+ */
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import os from 'os';
11
+ import { createRequire } from 'module';
12
+ const requireFromHere = createRequire(import.meta.url);
13
+ let _cached = null;
14
+ /**
15
+ * 检测 node:sqlite 模块是否可用(Node 22.5+)
16
+ */
17
+ function checkSqliteAvailable() {
18
+ try {
19
+ requireFromHere('node:sqlite');
20
+ return true;
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
26
+ /**
27
+ * 检测 Codex state_*.sqlite 文件是否存在
28
+ */
29
+ function checkCodexStateDb() {
30
+ const codexHome = path.join(os.homedir(), '.codex');
31
+ if (!fs.existsSync(codexHome))
32
+ return false;
33
+ try {
34
+ const files = fs.readdirSync(codexHome).filter(f => /^state_\d+\.sqlite$/.test(f));
35
+ return files.length > 0;
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ /**
42
+ * 检测 Claude projects 目录是否存在
43
+ */
44
+ function checkClaudeProjects() {
45
+ const claudeProjects = path.join(os.homedir(), '.claude', 'projects');
46
+ return fs.existsSync(claudeProjects);
47
+ }
48
+ /**
49
+ * 检测 Codex sessions 目录是否存在
50
+ */
51
+ function checkCodexSessions() {
52
+ const codexSessions = path.join(os.homedir(), '.codex', 'sessions');
53
+ return fs.existsSync(codexSessions);
54
+ }
55
+ /**
56
+ * 检测所有 base agent 的可用性
57
+ * 结果会缓存,避免重复检测
58
+ */
59
+ export function detectBaseAgents() {
60
+ if (_cached)
61
+ return _cached;
62
+ const claude = checkClaudeProjects();
63
+ const codex = checkCodexSessions() && checkCodexStateDb() && checkSqliteAvailable();
64
+ _cached = { claude, codex };
65
+ return _cached;
66
+ }
67
+ /**
68
+ * 重置缓存(测试用)
69
+ */
70
+ export function resetDetectionCache() {
71
+ _cached = null;
72
+ }
@@ -1,38 +1,372 @@
1
1
  /**
2
- * 消息数据源 fs-utils 的数据层函数 + fs.watch 文件监听。
2
+ * 消息数据源:扫描 data/sessions 下所有渠道的 messages.jsonl。
3
3
  *
4
- * snapshot:
5
- * - aid: 本地 AID 列表(含收发统计)
6
- * - 有 aid 无 peer: 该 AID 的对端列表 + 全部消息
7
- * - 有 aid 有 peer: 该对端的消息
8
- * subscribe: fs.watch(sessions/aun, recursive),messages.jsonl 变化时防抖 150ms 后重推。
4
+ * 左列按 agent 聚合;中列列出该 agent 下所有渠道的私聊/群聊会话。
5
+ * 兼容旧参数名 aid/peer:前端的 aid 实际上传 scope id。
9
6
  */
10
7
  import fs from 'fs';
11
- import { getSessionsAunDir, listLocalAids, loadAidInfo, loadPeerInfos, loadAllMessages, readMessages, } from '../fs-utils.js';
12
- function buildSnapshot(params) {
13
- const aunDir = getSessionsAunDir();
14
- if (!fs.existsSync(aunDir)) {
15
- return { aids: [], peers: [], messages: [], aid: null, peer: null };
8
+ import path from 'path';
9
+ import { resolvePaths } from '../paths.js';
10
+ import { readAllJsonlLines, readJsonFile, } from '../fs-utils.js';
11
+ function safeId(value) {
12
+ return Buffer.from(value, 'utf8').toString('base64url');
13
+ }
14
+ function findMessageFiles(root) {
15
+ const out = [];
16
+ const visit = (dir) => {
17
+ let entries;
18
+ try {
19
+ entries = fs.readdirSync(dir, { withFileTypes: true });
20
+ }
21
+ catch {
22
+ return;
23
+ }
24
+ for (const entry of entries) {
25
+ const full = path.join(dir, entry.name);
26
+ if (entry.isDirectory())
27
+ visit(full);
28
+ else if (entry.isFile() && entry.name === 'messages.jsonl')
29
+ out.push(full);
30
+ }
31
+ };
32
+ visit(root);
33
+ return out;
34
+ }
35
+ function listConfiguredAgentAids(root) {
36
+ const agentsDir = path.join(root, 'agents');
37
+ const aids = new Set();
38
+ let entries;
39
+ try {
40
+ entries = fs.readdirSync(agentsDir, { withFileTypes: true });
16
41
  }
17
- const aid = params.aid || null;
18
- const peer = params.peer || null;
19
- const aids = listLocalAids(aunDir)
20
- .map(a => loadAidInfo(aunDir, a))
21
- .sort((a, b) => (b.totalIn + b.totalOut) - (a.totalIn + a.totalOut));
22
- if (!aid) {
23
- return { aids, peers: [], messages: [], aid: null, peer: null };
42
+ catch {
43
+ return aids;
44
+ }
45
+ for (const entry of entries) {
46
+ if (!entry.isDirectory())
47
+ continue;
48
+ const config = readJsonFile(path.join(agentsDir, entry.name, 'config.json'));
49
+ const aid = typeof config?.aid === 'string' ? config.aid : entry.name;
50
+ if (looksLikeAID(aid))
51
+ aids.add(aid);
52
+ }
53
+ return aids;
54
+ }
55
+ function decodeDirSegment(seg) {
56
+ return seg.replace(/%([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
57
+ }
58
+ function parseChannelKey(value) {
59
+ if (typeof value !== 'string')
60
+ return null;
61
+ const parts = value.split('#');
62
+ if (parts.length !== 3)
63
+ return null;
64
+ const [type, selfAID, name] = parts;
65
+ if (!type || !selfAID || !name)
66
+ return null;
67
+ return { type, selfAID, name };
68
+ }
69
+ function looksLikeAID(value) {
70
+ return !!value && value !== 'self' && value !== '_unknown' && /^[^./#\s]+\.[^/#\s]+/.test(value);
71
+ }
72
+ function parseLegacySessionKey(value, channelType) {
73
+ if (typeof value !== 'string')
74
+ return null;
75
+ const parts = value.split('#');
76
+ if (parts.length < 3 || parts[0] !== channelType)
77
+ return null;
78
+ if (!looksLikeAID(parts[1]))
79
+ return null;
80
+ return { type: parts[0], selfAID: parts[1], name: parts[2] || 'main' };
81
+ }
82
+ function pathIdentity(sessionsDir, dirPath) {
83
+ const relParts = path.relative(sessionsDir, dirPath).split(path.sep).filter(Boolean);
84
+ if (!relParts.length)
85
+ return {};
86
+ const firstKey = parseChannelKey(decodeDirSegment(relParts[0]));
87
+ if (firstKey) {
88
+ return {
89
+ channelType: firstKey.type,
90
+ channel: `${firstKey.type}#${firstKey.selfAID}#${firstKey.name}`,
91
+ channelId: relParts[1] ? decodeDirSegment(relParts[1]) : undefined,
92
+ selfAID: firstKey.selfAID,
93
+ };
24
94
  }
25
- const peers = loadPeerInfos(aunDir, aid);
26
- let messages;
27
- if (peer) {
28
- messages = readMessages(aunDir, aid, peer);
29
- if (messages.length > 1000)
30
- messages = messages.slice(-1000);
95
+ if (relParts[0] === 'aun' && relParts.length >= 3) {
96
+ const selfAID = decodeDirSegment(relParts[1]);
97
+ return {
98
+ channelType: 'aun',
99
+ channel: looksLikeAID(selfAID) ? `aun#${selfAID}#main` : undefined,
100
+ channelId: decodeDirSegment(relParts[2]),
101
+ selfAID: looksLikeAID(selfAID) ? selfAID : undefined,
102
+ };
31
103
  }
32
- else {
33
- messages = loadAllMessages(aunDir, aid);
104
+ return {
105
+ channelType: decodeDirSegment(relParts[0]),
106
+ channelId: relParts[1] ? decodeDirSegment(relParts[1]) : undefined,
107
+ };
108
+ }
109
+ function inferSelfAID(active, messages, hints) {
110
+ if (looksLikeAID(active?.selfAID))
111
+ return String(active.selfAID);
112
+ if (looksLikeAID(hints.selfAID))
113
+ return hints.selfAID;
114
+ const channelKey = parseChannelKey(active?.channel) || parseChannelKey(active?.channelType);
115
+ if (channelKey)
116
+ return channelKey.selfAID;
117
+ const sessionKey = parseLegacySessionKey(active?.sessionKey, hints.channelType || active?.channelType || '');
118
+ if (sessionKey)
119
+ return sessionKey.selfAID;
120
+ const out = messages.find(m => m.dir === 'out' && m.from && m.from !== 'self');
121
+ if (out?.from)
122
+ return String(out.from);
123
+ const inboundTo = messages.find(m => m.dir === 'in' && m.to && m.to !== 'self');
124
+ if (inboundTo?.to)
125
+ return String(inboundTo.to);
126
+ return 'unknown';
127
+ }
128
+ function normalizeChatIdentity(sessionsDir, dirPath, active, messages) {
129
+ const pathHints = pathIdentity(sessionsDir, dirPath);
130
+ const activeChannelKey = parseChannelKey(active?.channel);
131
+ const activeTypeKey = parseChannelKey(active?.channelType);
132
+ const msgChannelType = messages.find(m => m.channelType)?.channelType;
133
+ const rawType = String(activeChannelKey?.type
134
+ || activeTypeKey?.type
135
+ || active?.channelType
136
+ || msgChannelType
137
+ || pathHints.channelType
138
+ || 'unknown');
139
+ const channelType = parseChannelKey(rawType)?.type || rawType;
140
+ const selfAID = inferSelfAID(active, messages, { ...pathHints, channelType });
141
+ const rawChannel = typeof active?.channel === 'string' && active.channel
142
+ ? active.channel
143
+ : pathHints.channel || `${channelType}#${selfAID}#main`;
144
+ const channelKey = parseChannelKey(rawChannel);
145
+ const channel = channelKey
146
+ ? `${channelKey.type}#${channelKey.selfAID}#${channelKey.name}`
147
+ : (selfAID !== 'unknown' ? `${channelType}#${selfAID}#main` : rawChannel);
148
+ const channelId = String(active.channelId || pathHints.channelId || path.basename(dirPath));
149
+ return { channelType, channel, channelId, selfAID };
150
+ }
151
+ function chatDisplayId(active, messages) {
152
+ const metadata = active?.metadata ?? {};
153
+ const chatType = active?.chatType || messages.find(m => m.chatType)?.chatType || 'private';
154
+ const groupId = chatType === 'group'
155
+ ? String(metadata.groupId || active?.channelId || messages.find(m => m.groupId)?.groupId || '')
156
+ : null;
157
+ const groupName = chatType === 'group' && metadata.groupName ? String(metadata.groupName) : null;
158
+ if (chatType === 'group') {
159
+ return {
160
+ peerId: groupId || String(active?.channelId || 'group'),
161
+ peerName: groupName,
162
+ groupId,
163
+ groupName,
164
+ };
34
165
  }
35
- return { aids, peers, messages, aid, peer };
166
+ const peerId = String(metadata.peerId || active?.channelId || messages.find(m => m.dir === 'in')?.from || messages.find(m => m.dir === 'out')?.to || 'unknown');
167
+ return {
168
+ peerId,
169
+ peerName: metadata.peerName ? String(metadata.peerName) : null,
170
+ groupId: null,
171
+ groupName: null,
172
+ };
173
+ }
174
+ function loadChats() {
175
+ const paths = resolvePaths();
176
+ const sessionsDir = paths.sessionsDir;
177
+ const configuredAids = listConfiguredAgentAids(paths.root);
178
+ const files = findMessageFiles(sessionsDir);
179
+ const chats = [];
180
+ for (const file of files) {
181
+ const dirPath = path.dirname(file);
182
+ const active = readJsonFile(path.join(dirPath, 'active.json')) ?? {};
183
+ const messages = readAllJsonlLines(file);
184
+ if (!messages.length)
185
+ continue;
186
+ const identity = normalizeChatIdentity(sessionsDir, dirPath, active, messages);
187
+ const { channelType, channel, channelId, selfAID } = identity;
188
+ if (configuredAids.size && !configuredAids.has(selfAID))
189
+ continue;
190
+ const chatType = String(active.chatType || messages.find(m => m.chatType)?.chatType || 'private');
191
+ const display = chatDisplayId({ ...active, chatType, channelId }, messages);
192
+ const lastMsgAt = messages.reduce((max, m) => Math.max(max, m.ts || 0), 0);
193
+ const updatedAt = Math.max(Number(active.updatedAt || 0), lastMsgAt);
194
+ chats.push({
195
+ id: safeId(`${selfAID}\n${channelType}\n${channelId}\n${chatType}`),
196
+ dirPath,
197
+ channelType,
198
+ channel,
199
+ channelId,
200
+ selfAID,
201
+ chatType,
202
+ peerId: display.peerId,
203
+ peerName: display.peerName,
204
+ groupId: display.groupId,
205
+ groupName: display.groupName,
206
+ updatedAt,
207
+ messages,
208
+ });
209
+ }
210
+ return chats;
211
+ }
212
+ function scopeId(chat) {
213
+ return safeId(chat.selfAID);
214
+ }
215
+ function legacyChannelScopeId(chat) {
216
+ return safeId(`${chat.selfAID}\n${chat.channelType}\n${chat.channel}`);
217
+ }
218
+ function buildScopes(chats) {
219
+ const map = new Map();
220
+ const seenChats = new Map();
221
+ const seenChannels = new Map();
222
+ const seenChannelTypes = new Map();
223
+ for (const chat of chats) {
224
+ const id = scopeId(chat);
225
+ let scope = map.get(id);
226
+ if (!scope) {
227
+ scope = {
228
+ id,
229
+ aid: id,
230
+ selfAID: chat.selfAID,
231
+ totalIn: 0,
232
+ totalOut: 0,
233
+ peerCount: 0,
234
+ groupCount: 0,
235
+ channelCount: 0,
236
+ channelTypes: [],
237
+ channels: [],
238
+ lastAt: 0,
239
+ };
240
+ map.set(id, scope);
241
+ seenChats.set(id, new Set());
242
+ seenChannels.set(id, new Set());
243
+ seenChannelTypes.set(id, new Set());
244
+ }
245
+ const channelSeen = seenChannels.get(id);
246
+ if (!channelSeen.has(chat.channel)) {
247
+ channelSeen.add(chat.channel);
248
+ scope.channels.push(chat.channel);
249
+ scope.channelCount = channelSeen.size;
250
+ }
251
+ const typeSeen = seenChannelTypes.get(id);
252
+ if (!typeSeen.has(chat.channelType)) {
253
+ typeSeen.add(chat.channelType);
254
+ scope.channelTypes.push(chat.channelType);
255
+ scope.channelTypes.sort();
256
+ }
257
+ let hasIn = false;
258
+ let hasOut = false;
259
+ for (const msg of chat.messages) {
260
+ if (msg.dir === 'in') {
261
+ scope.totalIn++;
262
+ hasIn = true;
263
+ }
264
+ else if (msg.dir === 'out') {
265
+ scope.totalOut++;
266
+ hasOut = true;
267
+ }
268
+ if (msg.ts > scope.lastAt)
269
+ scope.lastAt = msg.ts;
270
+ }
271
+ if (hasIn || hasOut) {
272
+ const chatKey = `${chat.channel}\n${chat.channelId}\n${chat.chatType}`;
273
+ const seen = seenChats.get(id);
274
+ if (!seen.has(chatKey)) {
275
+ seen.add(chatKey);
276
+ scope.peerCount++;
277
+ if (chat.chatType === 'group')
278
+ scope.groupCount++;
279
+ }
280
+ }
281
+ if (chat.updatedAt > scope.lastAt)
282
+ scope.lastAt = chat.updatedAt;
283
+ }
284
+ return Array.from(map.values())
285
+ .sort((a, b) => b.lastAt - a.lastAt);
286
+ }
287
+ function channelName(channel) {
288
+ return parseChannelKey(channel)?.name ?? null;
289
+ }
290
+ function chatKey(chat) {
291
+ return safeId(`${chat.channelType}\n${chat.channel}\n${chat.channelId}\n${chat.chatType}`);
292
+ }
293
+ function mergePeerInfo(chats) {
294
+ const map = new Map();
295
+ for (const chat of chats) {
296
+ const key = chatKey(chat);
297
+ let peer = map.get(key);
298
+ if (!peer) {
299
+ peer = {
300
+ id: key,
301
+ peerId: key,
302
+ peerName: chat.chatType === 'group' ? (chat.groupName || chat.groupId || chat.channelId) : (chat.peerName || chat.peerId),
303
+ channel: chat.channel,
304
+ channelType: chat.channelType,
305
+ channelName: channelName(chat.channel),
306
+ chatType: chat.chatType,
307
+ groupId: chat.groupId,
308
+ groupName: chat.groupName,
309
+ inbound: 0,
310
+ outbound: 0,
311
+ lastAt: 0,
312
+ };
313
+ map.set(key, peer);
314
+ }
315
+ for (const msg of chat.messages) {
316
+ if (msg.dir === 'in')
317
+ peer.inbound++;
318
+ else if (msg.dir === 'out')
319
+ peer.outbound++;
320
+ if (msg.ts > peer.lastAt)
321
+ peer.lastAt = msg.ts;
322
+ }
323
+ if (chat.updatedAt > peer.lastAt)
324
+ peer.lastAt = chat.updatedAt;
325
+ }
326
+ return Array.from(map.values()).sort((a, b) => b.lastAt - a.lastAt);
327
+ }
328
+ function selectedChats(chats, scope) {
329
+ if (!scope)
330
+ return [];
331
+ return chats.filter(chat => scopeId(chat) === scope);
332
+ }
333
+ function messagesFor(chats, peer) {
334
+ const selected = peer
335
+ ? chats.filter(chat => chatKey(chat) === peer)
336
+ : chats;
337
+ const messages = selected.flatMap(chat => chat.messages.map(msg => ({
338
+ ...msg,
339
+ channel: chat.channel,
340
+ channelType: chat.channelType,
341
+ selfAID: chat.selfAID,
342
+ peerName: chat.peerName,
343
+ groupName: chat.groupName,
344
+ })));
345
+ messages.sort((a, b) => (a.ts || 0) - (b.ts || 0));
346
+ return messages.length > 1000 ? messages.slice(-1000) : messages;
347
+ }
348
+ function buildSnapshot(params) {
349
+ const chats = loadChats();
350
+ const scopes = buildScopes(chats);
351
+ const requestedScope = params.scope || params.aid || null;
352
+ const scope = requestedScope && !scopes.some(s => s.id === requestedScope)
353
+ ? (chats.find(chat => legacyChannelScopeId(chat) === requestedScope)
354
+ ? scopeId(chats.find(chat => legacyChannelScopeId(chat) === requestedScope))
355
+ : requestedScope)
356
+ : requestedScope;
357
+ const peer = params.peer || null;
358
+ const inScope = selectedChats(chats, scope);
359
+ const peers = scope ? mergePeerInfo(inScope) : [];
360
+ const messages = scope ? messagesFor(inScope, peer) : [];
361
+ return {
362
+ scopes,
363
+ aids: scopes,
364
+ peers,
365
+ messages,
366
+ scope,
367
+ aid: scope,
368
+ peer,
369
+ };
36
370
  }
37
371
  export const msgSource = {
38
372
  kind: 'msg',
@@ -40,7 +374,7 @@ export const msgSource = {
40
374
  return buildSnapshot(params);
41
375
  },
42
376
  subscribe(params, push) {
43
- const aunDir = getSessionsAunDir();
377
+ const sessionsDir = resolvePaths().sessionsDir;
44
378
  let watcher = null;
45
379
  let debounce = null;
46
380
  const fire = () => {
@@ -54,12 +388,13 @@ export const msgSource = {
54
388
  }, 150);
55
389
  };
56
390
  try {
57
- watcher = fs.watch(aunDir, { recursive: true }, (_evt, filename) => {
58
- if (filename && String(filename).endsWith('messages.jsonl'))
391
+ watcher = fs.watch(sessionsDir, { recursive: true }, (_evt, filename) => {
392
+ const name = String(filename || '');
393
+ if (name.endsWith('messages.jsonl') || name.endsWith('active.json'))
59
394
  fire();
60
395
  });
61
396
  }
62
- catch { /* aunDir may not exist yet */ }
397
+ catch { /* sessionsDir may not exist yet */ }
63
398
  return () => {
64
399
  if (watcher)
65
400
  watcher.close();