agent-teams-dashboard 0.3.1 → 0.4.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/README.md +3 -0
- package/README.zh-TW.md +3 -0
- package/dist/assets/index-CsXA0cmK.js +51 -0
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/server-dist/server/teamsApi.js +8 -11
- package/server-dist/server/teamsCache.js +175 -18
- package/server-dist/server/teamsWatcher.js +5 -5
- package/server-dist/server/wsServer.js +11 -4
- package/dist/assets/index-B8t0Tabx.js +0 -51
package/dist/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Agent Teams Dashboard</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-CsXA0cmK.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-CsK61Xi-.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
package/package.json
CHANGED
|
@@ -13,12 +13,12 @@ export async function handleTeamsApi(req, res) {
|
|
|
13
13
|
const path = url.pathname;
|
|
14
14
|
// GET /api/snapshot
|
|
15
15
|
if (path === '/api/snapshot') {
|
|
16
|
-
json(res, cache.
|
|
16
|
+
json(res, cache.getLeanSnapshot());
|
|
17
17
|
return true;
|
|
18
18
|
}
|
|
19
19
|
// GET /api/teams
|
|
20
20
|
if (path === '/api/teams') {
|
|
21
|
-
const snapshot = cache.
|
|
21
|
+
const snapshot = cache.getLeanSnapshot();
|
|
22
22
|
json(res, snapshot.teams);
|
|
23
23
|
return true;
|
|
24
24
|
}
|
|
@@ -26,7 +26,7 @@ export async function handleTeamsApi(req, res) {
|
|
|
26
26
|
const tasksMatch = path.match(/^\/api\/teams\/([^/]+)\/tasks$/);
|
|
27
27
|
if (tasksMatch) {
|
|
28
28
|
const teamId = decodeURIComponent(tasksMatch[1]);
|
|
29
|
-
const snapshot = cache.
|
|
29
|
+
const snapshot = cache.getLeanSnapshot();
|
|
30
30
|
const team = snapshot.teams.find((t) => t.config.name === teamId);
|
|
31
31
|
if (!team) {
|
|
32
32
|
notFound(res);
|
|
@@ -39,7 +39,7 @@ export async function handleTeamsApi(req, res) {
|
|
|
39
39
|
const teamMatch = path.match(/^\/api\/teams\/([^/]+)$/);
|
|
40
40
|
if (teamMatch) {
|
|
41
41
|
const teamId = decodeURIComponent(teamMatch[1]);
|
|
42
|
-
const snapshot = cache.
|
|
42
|
+
const snapshot = cache.getLeanSnapshot();
|
|
43
43
|
const team = snapshot.teams.find((t) => t.config.name === teamId);
|
|
44
44
|
if (!team) {
|
|
45
45
|
notFound(res);
|
|
@@ -69,14 +69,11 @@ export async function handleTeamsApi(req, res) {
|
|
|
69
69
|
const agentMatch = path.match(/^\/api\/agents\/([^/]+)\/activity$/);
|
|
70
70
|
if (agentMatch) {
|
|
71
71
|
const agentId = decodeURIComponent(agentMatch[1]);
|
|
72
|
-
let entries = cache.getAgentActivity(agentId);
|
|
73
72
|
const limitParam = url.searchParams.get('limit');
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
}
|
|
73
|
+
const offsetParam = url.searchParams.get('offset');
|
|
74
|
+
const limit = limitParam ? parseInt(limitParam, 10) : undefined;
|
|
75
|
+
const offset = offsetParam ? parseInt(offsetParam, 10) : undefined;
|
|
76
|
+
const entries = cache.getAgentActivity(agentId, limit && !isNaN(limit) && limit > 0 ? limit : undefined, offset && !isNaN(offset) && offset >= 0 ? offset : undefined);
|
|
80
77
|
json(res, entries);
|
|
81
78
|
return true;
|
|
82
79
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
1
|
+
import { readdir, readFile, stat, open } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { EventEmitter } from 'node:events';
|
|
@@ -7,6 +7,7 @@ const TEAMS_DIR = join(CLAUDE_DIR, 'teams');
|
|
|
7
7
|
const TASKS_DIR = join(CLAUDE_DIR, 'tasks');
|
|
8
8
|
const PROJECTS_DIR = join(CLAUDE_DIR, 'projects');
|
|
9
9
|
const MAX_ENTRIES_PER_AGENT = 200;
|
|
10
|
+
const MAX_FILE_AGE_MS = 7 * 24 * 60 * 60 * 1000; // Only read JSONL files modified within 7 days
|
|
10
11
|
// In-memory caches
|
|
11
12
|
const teams = new Map();
|
|
12
13
|
const tasks = new Map();
|
|
@@ -14,6 +15,10 @@ const agentEntries = new Map();
|
|
|
14
15
|
const agentOffsets = new Map();
|
|
15
16
|
const teamFileMtimes = new Map(); // team name -> latest mtime (ms)
|
|
16
17
|
const knownProjectDirs = new Set(); // all project dir names under ~/.claude/projects/
|
|
18
|
+
const changedAgentIds = new Set(); // agentIds with new entries since last clear
|
|
19
|
+
const newEntriesBuffer = new Map(); // agentId -> entries added since last clear
|
|
20
|
+
const teamLeadSessions = new Map(); // leadSessionId -> team info
|
|
21
|
+
const subagentToMember = new Map(); // subagent filePath -> memberAgentId
|
|
17
22
|
export const onChange = new EventEmitter();
|
|
18
23
|
// --- Helpers ---
|
|
19
24
|
async function safeReaddir(dir) {
|
|
@@ -63,6 +68,20 @@ export async function refreshTeams() {
|
|
|
63
68
|
};
|
|
64
69
|
teams.set(config.name, config);
|
|
65
70
|
currentNames.add(config.name);
|
|
71
|
+
// Build team lead session mapping for subagent resolution
|
|
72
|
+
const leadSessionId = parsed.leadSessionId;
|
|
73
|
+
if (leadSessionId && Array.isArray(parsed.members)) {
|
|
74
|
+
const members = parsed.members
|
|
75
|
+
.filter((m) => m.agentType !== 'team-lead' && m.prompt)
|
|
76
|
+
.map((m) => ({
|
|
77
|
+
agentId: m.agentId ?? '',
|
|
78
|
+
name: m.name ?? '',
|
|
79
|
+
prompt: (m.prompt ?? '').slice(0, 300), // first 300 chars is enough for matching
|
|
80
|
+
}));
|
|
81
|
+
if (members.length > 0) {
|
|
82
|
+
teamLeadSessions.set(leadSessionId, { teamName: config.name, members });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
66
85
|
// Track config file mtime
|
|
67
86
|
const configStat = await safeFileStat(configPath);
|
|
68
87
|
if (configStat) {
|
|
@@ -159,42 +178,80 @@ export async function scanAgentJsonl() {
|
|
|
159
178
|
// Subagent JSONL: agent-*.jsonl under session/subagents/
|
|
160
179
|
const subagentsDir = join(entryPath, 'subagents');
|
|
161
180
|
const files = await safeReaddir(subagentsDir);
|
|
181
|
+
const parentSessionId = entry; // the session UUID folder name
|
|
162
182
|
for (const file of files) {
|
|
163
183
|
if (!file.startsWith('agent-') || !file.endsWith('.jsonl'))
|
|
164
184
|
continue;
|
|
165
|
-
await readNewEntries(join(subagentsDir, file), false, projDir);
|
|
185
|
+
await readNewEntries(join(subagentsDir, file), false, projDir, parentSessionId);
|
|
166
186
|
}
|
|
167
187
|
}
|
|
168
188
|
}
|
|
169
189
|
}
|
|
170
|
-
async function readNewEntries(filePath, isSessionFile, projectDir) {
|
|
190
|
+
async function readNewEntries(filePath, isSessionFile, projectDir, parentSessionId) {
|
|
171
191
|
const fileStat = await safeFileStat(filePath);
|
|
172
192
|
if (!fileStat)
|
|
173
193
|
return;
|
|
194
|
+
// Skip files not modified within the recency window
|
|
195
|
+
if (Date.now() - fileStat.mtimeMs > MAX_FILE_AGE_MS)
|
|
196
|
+
return;
|
|
174
197
|
const currentOffset = agentOffsets.get(filePath) ?? 0;
|
|
175
198
|
const fileSize = fileStat.size;
|
|
176
199
|
if (fileSize <= currentOffset)
|
|
177
200
|
return;
|
|
178
|
-
|
|
179
|
-
|
|
201
|
+
// Read only the new bytes from currentOffset to avoid byte/char offset mismatch
|
|
202
|
+
let newContent;
|
|
203
|
+
try {
|
|
204
|
+
const fh = await open(filePath, 'r');
|
|
205
|
+
try {
|
|
206
|
+
const buf = Buffer.alloc(fileSize - currentOffset);
|
|
207
|
+
await fh.read(buf, 0, buf.length, currentOffset);
|
|
208
|
+
newContent = buf.toString('utf-8');
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
await fh.close();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
180
215
|
return;
|
|
181
|
-
|
|
216
|
+
}
|
|
182
217
|
agentOffsets.set(filePath, fileSize);
|
|
183
218
|
const lines = newContent.split('\n').filter(Boolean);
|
|
184
219
|
for (const line of lines) {
|
|
185
220
|
try {
|
|
186
221
|
const parsed = JSON.parse(line);
|
|
187
|
-
// For session files,
|
|
222
|
+
// For session files, process team entries AND regular conversation entries
|
|
188
223
|
if (isSessionFile) {
|
|
189
224
|
const teamName = parsed.teamName;
|
|
190
|
-
|
|
225
|
+
// Skip non-message entries (file-history-snapshot, progress, queue-operation, system)
|
|
226
|
+
if (!parsed.type || (parsed.type !== 'user' && parsed.type !== 'assistant'))
|
|
191
227
|
continue;
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
228
|
+
let fullAgentId;
|
|
229
|
+
let slug;
|
|
230
|
+
if (teamName) {
|
|
231
|
+
// Team session: use name@team format
|
|
232
|
+
const agentName = parsed.agentName || 'team-lead';
|
|
233
|
+
fullAgentId = `${agentName}@${teamName}`;
|
|
234
|
+
slug = agentName;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
// Check if this session belongs to a team lead
|
|
238
|
+
const sessionId = parsed.sessionId ?? '';
|
|
239
|
+
if (!sessionId)
|
|
240
|
+
continue;
|
|
241
|
+
const teamInfo = teamLeadSessions.get(sessionId);
|
|
242
|
+
if (teamInfo) {
|
|
243
|
+
fullAgentId = `team-lead@${teamInfo.teamName}`;
|
|
244
|
+
slug = 'team-lead';
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
// Regular conversation: use sessionId as agentId
|
|
248
|
+
fullAgentId = `session:${sessionId}`;
|
|
249
|
+
slug = parsed.slug || sessionId.slice(0, 8);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
195
252
|
const entry = {
|
|
196
253
|
agentId: fullAgentId,
|
|
197
|
-
slug
|
|
254
|
+
slug,
|
|
198
255
|
sessionId: parsed.sessionId ?? '',
|
|
199
256
|
type: parsed.type ?? 'assistant',
|
|
200
257
|
message: {
|
|
@@ -215,15 +272,69 @@ async function readNewEntries(filePath, isSessionFile, projectDir) {
|
|
|
215
272
|
agentEntries.set(fullAgentId, arr);
|
|
216
273
|
}
|
|
217
274
|
arr.push(entry);
|
|
275
|
+
changedAgentIds.add(fullAgentId);
|
|
276
|
+
let buf = newEntriesBuffer.get(fullAgentId);
|
|
277
|
+
if (!buf) {
|
|
278
|
+
buf = [];
|
|
279
|
+
newEntriesBuffer.set(fullAgentId, buf);
|
|
280
|
+
}
|
|
281
|
+
buf.push(entry);
|
|
218
282
|
if (arr.length > MAX_ENTRIES_PER_AGENT) {
|
|
219
283
|
arr.splice(0, arr.length - MAX_ENTRIES_PER_AGENT);
|
|
220
284
|
}
|
|
285
|
+
// Update slug from assistant entries (they carry the slug)
|
|
286
|
+
if (parsed.slug && arr.length > 0) {
|
|
287
|
+
for (const e of arr) {
|
|
288
|
+
e.slug = parsed.slug;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
221
291
|
continue;
|
|
222
292
|
}
|
|
223
|
-
//
|
|
293
|
+
// Skip non-message entries for subagent files too
|
|
294
|
+
if (!parsed.type || (parsed.type !== 'user' && parsed.type !== 'assistant'))
|
|
295
|
+
continue;
|
|
296
|
+
// Subagent JSONL: resolve team member agentId if possible
|
|
297
|
+
let resolvedAgentId = parsed.agentId ?? '';
|
|
298
|
+
let resolvedSlug = parsed.slug ?? '';
|
|
299
|
+
// Try to resolve subagent hash -> team member agentId
|
|
300
|
+
if (resolvedAgentId && parentSessionId) {
|
|
301
|
+
const cached = subagentToMember.get(filePath);
|
|
302
|
+
if (cached) {
|
|
303
|
+
// Successfully matched to a team member
|
|
304
|
+
resolvedSlug = cached.split('@')[0];
|
|
305
|
+
resolvedAgentId = cached;
|
|
306
|
+
}
|
|
307
|
+
else if (cached === undefined) {
|
|
308
|
+
// Not yet checked — try matching
|
|
309
|
+
// Try to match user message to team member prompt
|
|
310
|
+
const teamInfo = teamLeadSessions.get(parentSessionId);
|
|
311
|
+
if (teamInfo) {
|
|
312
|
+
if (parsed.type === 'user') {
|
|
313
|
+
const msgContent = Array.isArray(parsed.message?.content)
|
|
314
|
+
? parsed.message.content.map((c) => c.text ?? '').join('')
|
|
315
|
+
: String(parsed.message?.content ?? '');
|
|
316
|
+
let matched = false;
|
|
317
|
+
for (const member of teamInfo.members) {
|
|
318
|
+
if (member.prompt && msgContent.includes(member.prompt.slice(0, 100))) {
|
|
319
|
+
subagentToMember.set(filePath, member.agentId);
|
|
320
|
+
resolvedAgentId = member.agentId;
|
|
321
|
+
resolvedSlug = member.name;
|
|
322
|
+
matched = true;
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// No match: mark as checked so we don't retry, but keep original hash agentId
|
|
327
|
+
if (!matched) {
|
|
328
|
+
subagentToMember.set(filePath, '');
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// For assistant entries before we've seen a user entry, don't set fallback yet
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
224
335
|
const entry = {
|
|
225
|
-
agentId:
|
|
226
|
-
slug:
|
|
336
|
+
agentId: resolvedAgentId,
|
|
337
|
+
slug: resolvedSlug,
|
|
227
338
|
sessionId: parsed.sessionId ?? '',
|
|
228
339
|
type: parsed.type ?? 'assistant',
|
|
229
340
|
message: {
|
|
@@ -246,6 +357,13 @@ async function readNewEntries(filePath, isSessionFile, projectDir) {
|
|
|
246
357
|
agentEntries.set(entry.agentId, arr);
|
|
247
358
|
}
|
|
248
359
|
arr.push(entry);
|
|
360
|
+
changedAgentIds.add(entry.agentId);
|
|
361
|
+
let buf = newEntriesBuffer.get(entry.agentId);
|
|
362
|
+
if (!buf) {
|
|
363
|
+
buf = [];
|
|
364
|
+
newEntriesBuffer.set(entry.agentId, buf);
|
|
365
|
+
}
|
|
366
|
+
buf.push(entry);
|
|
249
367
|
if (arr.length > MAX_ENTRIES_PER_AGENT) {
|
|
250
368
|
arr.splice(0, arr.length - MAX_ENTRIES_PER_AGENT);
|
|
251
369
|
}
|
|
@@ -257,7 +375,9 @@ async function readNewEntries(filePath, isSessionFile, projectDir) {
|
|
|
257
375
|
}
|
|
258
376
|
// --- Snapshot assembly ---
|
|
259
377
|
function buildTeamOverview(teamName) {
|
|
260
|
-
const
|
|
378
|
+
const stored = teams.get(teamName) ?? { name: teamName, members: [] };
|
|
379
|
+
// Clone so we can add dynamic members without mutating the cache
|
|
380
|
+
const config = { ...stored, members: [...stored.members] };
|
|
261
381
|
const teamTasks = tasks.get(teamName) ?? [];
|
|
262
382
|
const taskStats = {
|
|
263
383
|
total: teamTasks.length,
|
|
@@ -267,6 +387,7 @@ function buildTeamOverview(teamName) {
|
|
|
267
387
|
};
|
|
268
388
|
// Build agentSlugs: map agentId -> slug from agentEntries
|
|
269
389
|
const agentSlugs = {};
|
|
390
|
+
const knownMemberIds = new Set(config.members.map(m => m.agentId));
|
|
270
391
|
for (const member of config.members) {
|
|
271
392
|
// Look for agent entries matching this member's agentId
|
|
272
393
|
const entries = agentEntries.get(member.agentId);
|
|
@@ -448,9 +569,45 @@ export function getSnapshot() {
|
|
|
448
569
|
}
|
|
449
570
|
return { teams: teamOverviews, unmatchedAgents, agentActivity: activity, projects: buildProjectOverviews() };
|
|
450
571
|
}
|
|
572
|
+
export function getLeanSnapshot() {
|
|
573
|
+
const teamOverviews = [];
|
|
574
|
+
const matchedAgentIds = new Set();
|
|
575
|
+
for (const teamName of teams.keys()) {
|
|
576
|
+
const overview = buildTeamOverview(teamName);
|
|
577
|
+
teamOverviews.push(overview);
|
|
578
|
+
for (const member of overview.config.members) {
|
|
579
|
+
matchedAgentIds.add(member.agentId);
|
|
580
|
+
matchedAgentIds.add(member.agentId.split('@')[0]);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
const unmatchedAgents = [];
|
|
584
|
+
for (const [agentId, entries] of agentEntries) {
|
|
585
|
+
if (!matchedAgentIds.has(agentId) && entries.length > 0) {
|
|
586
|
+
const last = entries[entries.length - 1];
|
|
587
|
+
unmatchedAgents.push({ agentId, slug: last.slug, sessionId: last.sessionId });
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return { teams: teamOverviews, unmatchedAgents, projects: buildProjectOverviews() };
|
|
591
|
+
}
|
|
592
|
+
export function getAndClearNewEntries() {
|
|
593
|
+
const result = new Map(newEntriesBuffer);
|
|
594
|
+
changedAgentIds.clear();
|
|
595
|
+
newEntriesBuffer.clear();
|
|
596
|
+
return result;
|
|
597
|
+
}
|
|
451
598
|
// --- Query ---
|
|
452
|
-
export function getAgentActivity(agentId) {
|
|
453
|
-
|
|
599
|
+
export function getAgentActivity(agentId, limit, offset) {
|
|
600
|
+
const entries = agentEntries.get(agentId) ?? [];
|
|
601
|
+
if (offset !== undefined && limit !== undefined) {
|
|
602
|
+
// offset counts from the end: offset=0 means latest `limit` entries
|
|
603
|
+
const end = entries.length - offset;
|
|
604
|
+
const start = Math.max(0, end - limit);
|
|
605
|
+
return entries.slice(start, Math.max(start, end));
|
|
606
|
+
}
|
|
607
|
+
if (limit !== undefined) {
|
|
608
|
+
return entries.slice(-limit);
|
|
609
|
+
}
|
|
610
|
+
return entries;
|
|
454
611
|
}
|
|
455
612
|
export function getAgentSessions(agentId) {
|
|
456
613
|
const entries = agentEntries.get(agentId);
|
|
@@ -32,7 +32,7 @@ function startTasksWatcher() {
|
|
|
32
32
|
const onTaskChange = debounce(async (_event, filename) => {
|
|
33
33
|
if (!filename) {
|
|
34
34
|
// Full refresh if no filename
|
|
35
|
-
for (const team of (await import('./teamsCache.js')).
|
|
35
|
+
for (const team of (await import('./teamsCache.js')).getLeanSnapshot().teams) {
|
|
36
36
|
await cache.refreshTasks(team.config.name);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -65,7 +65,7 @@ function startTeamsWatcher() {
|
|
|
65
65
|
const onTeamChange = debounce(async () => {
|
|
66
66
|
await cache.refreshTeams();
|
|
67
67
|
// Also refresh tasks in case new team appeared
|
|
68
|
-
for (const team of cache.
|
|
68
|
+
for (const team of cache.getLeanSnapshot().teams) {
|
|
69
69
|
await cache.refreshTasks(team.config.name);
|
|
70
70
|
}
|
|
71
71
|
cache.onChange.emit('change');
|
|
@@ -102,10 +102,10 @@ function startFullRefreshPoller() {
|
|
|
102
102
|
let lastSnapshot = '';
|
|
103
103
|
fullRefreshTimer = setInterval(async () => {
|
|
104
104
|
await cache.refreshTeams();
|
|
105
|
-
for (const team of cache.
|
|
105
|
+
for (const team of cache.getLeanSnapshot().teams) {
|
|
106
106
|
await cache.refreshTasks(team.config.name);
|
|
107
107
|
}
|
|
108
|
-
const snapshot = JSON.stringify(cache.
|
|
108
|
+
const snapshot = JSON.stringify(cache.getLeanSnapshot());
|
|
109
109
|
if (snapshot !== lastSnapshot) {
|
|
110
110
|
lastSnapshot = snapshot;
|
|
111
111
|
cache.onChange.emit('change');
|
|
@@ -117,7 +117,7 @@ function startJsonlPoller() {
|
|
|
117
117
|
let lastSnapshot = '';
|
|
118
118
|
jsonlTimer = setInterval(async () => {
|
|
119
119
|
await cache.scanAgentJsonl();
|
|
120
|
-
const snapshot = JSON.stringify(cache.
|
|
120
|
+
const snapshot = JSON.stringify(cache.getLeanSnapshot());
|
|
121
121
|
if (snapshot !== lastSnapshot) {
|
|
122
122
|
lastSnapshot = snapshot;
|
|
123
123
|
cache.onChange.emit('change');
|
|
@@ -20,8 +20,8 @@ export function initWebSocket(server) {
|
|
|
20
20
|
ws.isAlive = true;
|
|
21
21
|
const clientCount = wss.clients.size;
|
|
22
22
|
console.log(`[ws] Client connected (total: ${clientCount})`);
|
|
23
|
-
// Send initial snapshot
|
|
24
|
-
const snapshot = cache.
|
|
23
|
+
// Send initial lean snapshot (no agentActivity)
|
|
24
|
+
const snapshot = cache.getLeanSnapshot();
|
|
25
25
|
ws.send(JSON.stringify({ type: 'snapshot', data: snapshot }));
|
|
26
26
|
ws.on('pong', () => {
|
|
27
27
|
ws.isAlive = true;
|
|
@@ -54,9 +54,16 @@ export function initWebSocket(server) {
|
|
|
54
54
|
wss.on('close', () => {
|
|
55
55
|
clearInterval(heartbeat);
|
|
56
56
|
});
|
|
57
|
-
// Listen for cache changes and broadcast
|
|
57
|
+
// Listen for cache changes and broadcast lean snapshot + deltas
|
|
58
58
|
cache.onChange.on('change', () => {
|
|
59
|
-
const snap = cache.
|
|
59
|
+
const snap = cache.getLeanSnapshot();
|
|
60
60
|
broadcast({ type: 'snapshot', data: snap });
|
|
61
|
+
// Broadcast only new entries as deltas
|
|
62
|
+
const newEntries = cache.getAndClearNewEntries();
|
|
63
|
+
for (const [agentId, entries] of newEntries) {
|
|
64
|
+
if (entries.length > 0) {
|
|
65
|
+
broadcast({ type: 'agent_entries_delta', agentId, entries });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
61
68
|
});
|
|
62
69
|
}
|