@studiomopoke/crosschat 1.8.2 → 2.0.1
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 +48 -118
- package/bin/cli.cjs +37 -57
- package/crosschat.md +73 -104
- package/dashboard/dist/assets/index-BY-IQhma.js +49 -0
- package/dashboard/dist/assets/index-CI8v9PKQ.css +1 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/hub/_hub-section-1-infra.d.ts +8 -0
- package/dist/hub/_hub-section-1-infra.d.ts.map +1 -0
- package/dist/hub/_hub-section-1-infra.js +152 -0
- package/dist/hub/_hub-section-1-infra.js.map +1 -0
- package/dist/hub/_hub-section-2-handlers.d.ts +2 -0
- package/dist/hub/_hub-section-2-handlers.d.ts.map +1 -0
- package/dist/hub/_hub-section-2-handlers.js +514 -0
- package/dist/hub/_hub-section-2-handlers.js.map +1 -0
- package/dist/hub/_hub-section-3-rest.d.ts +2 -0
- package/dist/hub/_hub-section-3-rest.d.ts.map +1 -0
- package/dist/hub/_hub-section-3-rest.js +418 -0
- package/dist/hub/_hub-section-3-rest.js.map +1 -0
- package/dist/hub/_hub-section-4-ws.d.ts +2 -0
- package/dist/hub/_hub-section-4-ws.d.ts.map +1 -0
- package/dist/hub/_hub-section-4-ws.js +367 -0
- package/dist/hub/_hub-section-4-ws.js.map +1 -0
- package/dist/hub/agent-connection.d.ts +38 -62
- package/dist/hub/agent-connection.d.ts.map +1 -1
- package/dist/hub/agent-connection.js +146 -229
- package/dist/hub/agent-connection.js.map +1 -1
- package/dist/hub/hub-server.d.ts +1 -1
- package/dist/hub/hub-server.d.ts.map +1 -1
- package/dist/hub/hub-server.js +389 -685
- package/dist/hub/hub-server.js.map +1 -1
- package/dist/hub/message-manager.d.ts +158 -0
- package/dist/hub/message-manager.d.ts.map +1 -0
- package/dist/hub/message-manager.js +443 -0
- package/dist/hub/message-manager.js.map +1 -0
- package/dist/hub/protocol.d.ts +73 -131
- package/dist/hub/protocol.d.ts.map +1 -1
- package/dist/hub/protocol.js +3 -0
- package/dist/hub/protocol.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +15 -89
- package/dist/lifecycle.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +22 -33
- package/dist/server.js.map +1 -1
- package/dist/tools/add-badge.d.ts +4 -0
- package/dist/tools/add-badge.d.ts.map +1 -0
- package/dist/tools/add-badge.js +29 -0
- package/dist/tools/add-badge.js.map +1 -0
- package/dist/tools/claim-task.js +5 -5
- package/dist/tools/claim-task.js.map +1 -1
- package/dist/tools/clear-session.d.ts +1 -2
- package/dist/tools/clear-session.d.ts.map +1 -1
- package/dist/tools/clear-session.js +7 -22
- package/dist/tools/clear-session.js.map +1 -1
- package/dist/tools/flag-as-task.d.ts +4 -0
- package/dist/tools/flag-as-task.d.ts.map +1 -0
- package/dist/tools/flag-as-task.js +31 -0
- package/dist/tools/flag-as-task.js.map +1 -0
- package/dist/tools/get-messages.d.ts +2 -1
- package/dist/tools/get-messages.d.ts.map +1 -1
- package/dist/tools/get-messages.js +19 -6
- package/dist/tools/get-messages.js.map +1 -1
- package/dist/tools/list-peers.d.ts.map +1 -1
- package/dist/tools/list-peers.js +1 -4
- package/dist/tools/list-peers.js.map +1 -1
- package/dist/tools/resolve-task.d.ts +4 -0
- package/dist/tools/resolve-task.d.ts.map +1 -0
- package/dist/tools/resolve-task.js +29 -0
- package/dist/tools/resolve-task.js.map +1 -0
- package/dist/tools/send-message.d.ts.map +1 -1
- package/dist/tools/send-message.js +6 -5
- package/dist/tools/send-message.js.map +1 -1
- package/dist/tools/set-status.js +3 -3
- package/dist/tools/set-status.js.map +1 -1
- package/dist/tools/wait-for-messages.js +1 -1
- package/dist/tools/wait-for-messages.js.map +1 -1
- package/dist/types.d.ts +4 -3
- package/dist/types.d.ts.map +1 -1
- package/hooks/permission-hook.sh +19 -18
- package/package.json +1 -1
- package/dashboard/dist/assets/index-BR-2rRm6.css +0 -1
- package/dashboard/dist/assets/index-Ci2ihChN.js +0 -49
package/dist/hub/hub-server.js
CHANGED
|
@@ -9,7 +9,7 @@ import { WebSocketServer, WebSocket } from 'ws';
|
|
|
9
9
|
import { generateId } from '../util/id.js';
|
|
10
10
|
import { isProcessAlive } from '../util/pid.js';
|
|
11
11
|
import { log, logError } from '../util/logger.js';
|
|
12
|
-
import {
|
|
12
|
+
import { MessageManager } from './message-manager.js';
|
|
13
13
|
import { encodeMessage, decodeMessage, } from './protocol.js';
|
|
14
14
|
import { createRequire } from 'node:module';
|
|
15
15
|
// ── Constants ────────────────────────────────────────────────────────
|
|
@@ -23,7 +23,6 @@ const INSTANCES_FILE = path.join(CROSSCHAT_DIR, 'instances.json');
|
|
|
23
23
|
const REGISTER_TIMEOUT_MS = 5_000;
|
|
24
24
|
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
25
25
|
const PONG_TIMEOUT_MS = 10_000;
|
|
26
|
-
const ROOM_MESSAGE_CAP = 200;
|
|
27
26
|
const IDLE_SHUTDOWN_MS = 5 * 60 * 1000; // 5 minutes with no agents → auto-shutdown
|
|
28
27
|
const PERMISSION_TTL_MS = 10 * 60 * 1000; // 10 minutes for pending permissions
|
|
29
28
|
const PERMISSION_SWEEP_INTERVAL_MS = 60_000;
|
|
@@ -95,10 +94,36 @@ function getServerVersion() {
|
|
|
95
94
|
* Start the hub server.
|
|
96
95
|
*
|
|
97
96
|
* Central hub for CrossChat: manages agent WebSocket connections, peer registry,
|
|
98
|
-
*
|
|
97
|
+
* channels, message routing, tasks (via badges), and serves the React dashboard frontend.
|
|
99
98
|
*/
|
|
100
99
|
export async function startHub() {
|
|
101
100
|
await ensureCrosschatDir();
|
|
101
|
+
// Copy the permission hook to a stable location (~/.crosschat/hooks/)
|
|
102
|
+
// so settings.json can point to a path that survives package updates.
|
|
103
|
+
try {
|
|
104
|
+
const hooksDir = path.join(CROSSCHAT_DIR, 'hooks');
|
|
105
|
+
await fs.mkdir(hooksDir, { recursive: true });
|
|
106
|
+
const srcHook = path.join(__dirname, '..', 'hooks', 'permission-hook.sh');
|
|
107
|
+
const srcHookAlt = path.join(__dirname, '..', '..', 'hooks', 'permission-hook.sh');
|
|
108
|
+
let hookSource = null;
|
|
109
|
+
for (const p of [srcHook, srcHookAlt]) {
|
|
110
|
+
try {
|
|
111
|
+
await fs.access(p);
|
|
112
|
+
hookSource = p;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
catch { /* try next */ }
|
|
116
|
+
}
|
|
117
|
+
if (hookSource) {
|
|
118
|
+
const destHook = path.join(hooksDir, 'permission-hook.sh');
|
|
119
|
+
await fs.copyFile(hookSource, destHook);
|
|
120
|
+
await fs.chmod(destHook, 0o755);
|
|
121
|
+
log(`Permission hook copied to ${destHook}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
logError('Failed to copy permission hook to stable location', err);
|
|
126
|
+
}
|
|
102
127
|
// Check for an already-running hub
|
|
103
128
|
const existingLock = await readDashboardLock();
|
|
104
129
|
if (existingLock) {
|
|
@@ -107,35 +132,23 @@ export async function startHub() {
|
|
|
107
132
|
}
|
|
108
133
|
// ── State ──────────────────────────────────────────────────────
|
|
109
134
|
const agents = new Map();
|
|
110
|
-
const
|
|
135
|
+
const channels = new Set();
|
|
111
136
|
const browserClients = new Set();
|
|
112
|
-
const
|
|
137
|
+
const browserChannels = new WeakMap();
|
|
113
138
|
const browserTokens = new Map(); // session tokens for permission decisions
|
|
114
139
|
const validTokens = new Set(); // fast lookup for REST auth
|
|
115
140
|
const pendingPermissions = new Map();
|
|
116
141
|
const instances = await loadInstances();
|
|
117
|
-
// Initialize TaskManager
|
|
118
|
-
const
|
|
119
|
-
await
|
|
120
|
-
// Digest tracking: roomId -> taskId of currently active digest task
|
|
121
|
-
const activeDigestTasks = new Map();
|
|
142
|
+
// Initialize MessageManager (replaces TaskManager + in-memory channel messages)
|
|
143
|
+
const messageManager = new MessageManager();
|
|
144
|
+
await messageManager.init();
|
|
122
145
|
// Idle shutdown: auto-shutdown when no agents are connected for IDLE_SHUTDOWN_MS
|
|
123
146
|
let idleShutdownTimer = null;
|
|
124
147
|
// Ensure digests directory exists
|
|
125
148
|
await fs.mkdir(DIGESTS_DIR, { recursive: true });
|
|
126
|
-
// Seed default
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
name: 'General',
|
|
130
|
-
messages: [],
|
|
131
|
-
createdAt: new Date().toISOString(),
|
|
132
|
-
});
|
|
133
|
-
rooms.set('crosschat', {
|
|
134
|
-
id: 'crosschat',
|
|
135
|
-
name: 'CrossChat Activity',
|
|
136
|
-
messages: [],
|
|
137
|
-
createdAt: new Date().toISOString(),
|
|
138
|
-
});
|
|
149
|
+
// Seed default channels
|
|
150
|
+
channels.add('general');
|
|
151
|
+
channels.add('crosschat');
|
|
139
152
|
// ── Helpers ────────────────────────────────────────────────────
|
|
140
153
|
function sendToWs(ws, msg) {
|
|
141
154
|
if (ws.readyState === WebSocket.OPEN) {
|
|
@@ -147,7 +160,7 @@ export async function startHub() {
|
|
|
147
160
|
}
|
|
148
161
|
async function autoRegisterInstance(cwd) {
|
|
149
162
|
const resolvedPath = path.resolve(cwd);
|
|
150
|
-
// Skip if path has unsafe characters
|
|
163
|
+
// Skip if path has unsafe characters
|
|
151
164
|
if (!/^[a-zA-Z0-9\s/\-_\.~]+$/.test(resolvedPath))
|
|
152
165
|
return;
|
|
153
166
|
// Skip if an instance with this path already exists
|
|
@@ -193,38 +206,16 @@ export async function startHub() {
|
|
|
193
206
|
pid: agent.pid,
|
|
194
207
|
status: agent.status,
|
|
195
208
|
statusDetail: agent.statusDetail,
|
|
196
|
-
|
|
209
|
+
currentChannel: agent.currentChannel,
|
|
197
210
|
connectedAt: agent.connectedAt,
|
|
198
211
|
};
|
|
199
212
|
}
|
|
200
|
-
function taskToSummary(task) {
|
|
201
|
-
return {
|
|
202
|
-
taskId: task.taskId,
|
|
203
|
-
roomId: task.roomId,
|
|
204
|
-
creatorId: task.creatorId,
|
|
205
|
-
creatorName: task.creatorName,
|
|
206
|
-
description: task.description,
|
|
207
|
-
context: task.context,
|
|
208
|
-
filter: task.filter,
|
|
209
|
-
status: task.status,
|
|
210
|
-
claimantId: task.claimantId,
|
|
211
|
-
claimantName: task.claimantName,
|
|
212
|
-
createdAt: task.createdAt,
|
|
213
|
-
updatedAt: task.updatedAt,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
213
|
// ── Mention parsing ──────────────────────────────────────────
|
|
217
|
-
/**
|
|
218
|
-
* Parse @mentions from message content.
|
|
219
|
-
* Supports @agent-name (targeted) and @here (room broadcast).
|
|
220
|
-
* Returns the list of mentioned agent names and the mention type.
|
|
221
|
-
*/
|
|
222
214
|
function parseMentions(content) {
|
|
223
215
|
const hasHere = /@here\b/i.test(content);
|
|
224
216
|
if (hasHere) {
|
|
225
217
|
return { mentions: [], mentionType: 'here' };
|
|
226
218
|
}
|
|
227
|
-
// Extract all @mentions from the content
|
|
228
219
|
const mentionPattern = /@([\w-]+)/g;
|
|
229
220
|
const rawMentions = [];
|
|
230
221
|
let match;
|
|
@@ -251,27 +242,27 @@ export async function startHub() {
|
|
|
251
242
|
return { mentions: [], mentionType: 'broadcast' };
|
|
252
243
|
}
|
|
253
244
|
// ── Broadcasting ───────────────────────────────────────────────
|
|
254
|
-
/** Broadcast a server message to all agents in a specific
|
|
255
|
-
function
|
|
245
|
+
/** Broadcast a server message to all agents in a specific channel. */
|
|
246
|
+
function broadcastToChannelAgents(channelId, msg, excludePeerId) {
|
|
256
247
|
for (const agent of agents.values()) {
|
|
257
|
-
if (agent.
|
|
248
|
+
if (agent.currentChannel === channelId && agent.peerId !== excludePeerId) {
|
|
258
249
|
sendToWs(agent.ws, msg);
|
|
259
250
|
}
|
|
260
251
|
}
|
|
261
252
|
}
|
|
262
|
-
/** Broadcast a JSON payload to all browser clients subscribed to a
|
|
263
|
-
function
|
|
253
|
+
/** Broadcast a JSON payload to all browser clients subscribed to a channel. */
|
|
254
|
+
function broadcastToChannelBrowsers(channelId, data) {
|
|
264
255
|
const payload = JSON.stringify(data);
|
|
265
256
|
for (const client of browserClients) {
|
|
266
257
|
if (client.readyState !== WebSocket.OPEN)
|
|
267
258
|
continue;
|
|
268
|
-
const joined =
|
|
269
|
-
if (joined?.has(
|
|
259
|
+
const joined = browserChannels.get(client);
|
|
260
|
+
if (joined?.has(channelId)) {
|
|
270
261
|
client.send(payload);
|
|
271
262
|
}
|
|
272
263
|
}
|
|
273
264
|
}
|
|
274
|
-
/** Broadcast to ALL browser clients (not filtered by
|
|
265
|
+
/** Broadcast to ALL browser clients (not filtered by channel). */
|
|
275
266
|
function broadcastToAllBrowsers(data) {
|
|
276
267
|
const payload = JSON.stringify(data);
|
|
277
268
|
for (const client of browserClients) {
|
|
@@ -280,13 +271,14 @@ export async function startHub() {
|
|
|
280
271
|
}
|
|
281
272
|
}
|
|
282
273
|
}
|
|
283
|
-
/** Broadcast a
|
|
284
|
-
function
|
|
274
|
+
/** Broadcast a channel message to agents AND browsers, with mention filtering. */
|
|
275
|
+
function broadcastChannelMessage(channelId, msg, excludePeerId) {
|
|
285
276
|
// Build the protocol message for agents
|
|
286
277
|
const agentMsg = {
|
|
287
|
-
type: '
|
|
288
|
-
|
|
278
|
+
type: 'channel.message',
|
|
279
|
+
channelId: msg.channelId,
|
|
289
280
|
messageId: msg.messageId,
|
|
281
|
+
threadId: msg.threadId,
|
|
290
282
|
fromPeerId: msg.fromPeerId,
|
|
291
283
|
fromName: msg.fromName,
|
|
292
284
|
content: msg.content,
|
|
@@ -295,13 +287,14 @@ export async function startHub() {
|
|
|
295
287
|
source: msg.source,
|
|
296
288
|
mentions: msg.mentions,
|
|
297
289
|
mentionType: msg.mentionType,
|
|
298
|
-
importance: msg.importance,
|
|
290
|
+
importance: msg.metadata?.importance,
|
|
291
|
+
badges: msg.badges,
|
|
299
292
|
};
|
|
300
293
|
if (msg.mentionType === 'direct' && msg.mentions && msg.mentions.length > 0) {
|
|
301
294
|
// Direct mention: only deliver to mentioned agents (+ sender echo)
|
|
302
295
|
const mentionedNamesLower = new Set(msg.mentions.map((n) => n.toLowerCase()));
|
|
303
296
|
for (const agent of agents.values()) {
|
|
304
|
-
if (agent.
|
|
297
|
+
if (agent.currentChannel !== channelId)
|
|
305
298
|
continue;
|
|
306
299
|
if (agent.peerId === excludePeerId)
|
|
307
300
|
continue;
|
|
@@ -311,171 +304,41 @@ export async function startHub() {
|
|
|
311
304
|
}
|
|
312
305
|
}
|
|
313
306
|
else {
|
|
314
|
-
// @here or broadcast: deliver to all agents in the
|
|
315
|
-
|
|
307
|
+
// @here or broadcast: deliver to all agents in the channel
|
|
308
|
+
broadcastToChannelAgents(channelId, agentMsg, excludePeerId);
|
|
316
309
|
}
|
|
317
310
|
// Always send to all browsers — dashboard users see everything
|
|
318
|
-
|
|
311
|
+
broadcastToChannelBrowsers(channelId, {
|
|
319
312
|
type: 'message',
|
|
320
313
|
messageId: msg.messageId,
|
|
321
|
-
|
|
314
|
+
channelId: msg.channelId,
|
|
315
|
+
threadId: msg.threadId,
|
|
322
316
|
username: msg.fromName,
|
|
323
317
|
text: msg.content,
|
|
324
318
|
timestamp: msg.timestamp,
|
|
325
319
|
source: msg.source,
|
|
326
320
|
mentions: msg.mentions,
|
|
327
321
|
mentionType: msg.mentionType,
|
|
328
|
-
importance: msg.importance,
|
|
322
|
+
importance: msg.metadata?.importance,
|
|
323
|
+
badges: msg.badges,
|
|
329
324
|
});
|
|
330
325
|
}
|
|
331
|
-
// ── Activity
|
|
332
|
-
/** Post a system event to the CrossChat Activity
|
|
326
|
+
// ── Activity channel ────────────────────────────────────────
|
|
327
|
+
/** Post a system event to the CrossChat Activity channel. */
|
|
333
328
|
function postActivity(content, importance = 'comment') {
|
|
334
|
-
const room = rooms.get('crosschat');
|
|
335
|
-
if (!room)
|
|
336
|
-
return;
|
|
337
329
|
const msg = {
|
|
338
330
|
messageId: generateId(),
|
|
339
|
-
|
|
331
|
+
channelId: 'crosschat',
|
|
340
332
|
fromPeerId: 'system',
|
|
341
333
|
fromName: 'system',
|
|
342
334
|
content,
|
|
343
335
|
timestamp: new Date().toISOString(),
|
|
344
336
|
source: 'system',
|
|
345
|
-
|
|
337
|
+
badges: [],
|
|
338
|
+
metadata: { importance },
|
|
346
339
|
};
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
// Drop oldest messages when cap is exceeded (no digest for activity room)
|
|
350
|
-
if (room.messages.length > ROOM_MESSAGE_CAP) {
|
|
351
|
-
room.messages = room.messages.slice(-ROOM_MESSAGE_CAP);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
// ── Digest system ─────────────────────────────────────────────
|
|
355
|
-
/**
|
|
356
|
-
* Enforce the per-room message cap. When exceeded, the oldest messages
|
|
357
|
-
* are moved to a pending digest JSONL file and a digest task is created.
|
|
358
|
-
*/
|
|
359
|
-
async function enforceRoomMessageCap(room) {
|
|
360
|
-
if (room.messages.length <= ROOM_MESSAGE_CAP)
|
|
361
|
-
return;
|
|
362
|
-
if (room.id === 'crosschat')
|
|
363
|
-
return; // activity room handles its own cap via postActivity()
|
|
364
|
-
const overflow = room.messages.length - ROOM_MESSAGE_CAP;
|
|
365
|
-
const evicted = room.messages.splice(0, overflow);
|
|
366
|
-
// Append evicted messages to pending digest file (one JSON object per line)
|
|
367
|
-
const pendingPath = path.join(DIGESTS_DIR, `${room.id}.jsonl`);
|
|
368
|
-
const lines = evicted.map((m) => JSON.stringify(m)).join('\n') + '\n';
|
|
369
|
-
await fs.appendFile(pendingPath, lines, 'utf-8');
|
|
370
|
-
log(`Room ${room.id}: evicted ${overflow} message(s) to pending digest`);
|
|
371
|
-
// Auto-delegate a digest task if one isn't already active for this room
|
|
372
|
-
const existingTaskId = activeDigestTasks.get(room.id);
|
|
373
|
-
if (existingTaskId) {
|
|
374
|
-
const existingTask = taskManager.get(existingTaskId);
|
|
375
|
-
if (existingTask && (existingTask.status === 'open' || existingTask.status === 'claimed' || existingTask.status === 'in_progress')) {
|
|
376
|
-
// A digest task is already in progress — don't create another
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
// Previous task finished or was archived — allow a new one
|
|
380
|
-
activeDigestTasks.delete(room.id);
|
|
381
|
-
}
|
|
382
|
-
// Read the pending messages as context for the digest task
|
|
383
|
-
let pendingContent;
|
|
384
|
-
try {
|
|
385
|
-
pendingContent = await fs.readFile(pendingPath, 'utf-8');
|
|
386
|
-
}
|
|
387
|
-
catch {
|
|
388
|
-
pendingContent = lines;
|
|
389
|
-
}
|
|
390
|
-
try {
|
|
391
|
-
const task = await taskManager.create({
|
|
392
|
-
roomId: room.id,
|
|
393
|
-
creatorId: 'system',
|
|
394
|
-
creatorName: 'system',
|
|
395
|
-
description: 'Summarize the following room messages into a digest',
|
|
396
|
-
context: pendingContent,
|
|
397
|
-
});
|
|
398
|
-
activeDigestTasks.set(room.id, task.taskId);
|
|
399
|
-
// Broadcast task creation to agents and browsers in the room
|
|
400
|
-
const summary = taskToSummary(task);
|
|
401
|
-
broadcastToRoomAgents(room.id, { type: 'task.created', task: summary });
|
|
402
|
-
broadcastToRoomBrowsers(room.id, { type: 'task.created', task: summary });
|
|
403
|
-
log(`Digest task ${task.taskId} created for room ${room.id}`);
|
|
404
|
-
postActivity(`Room "${room.name}" hit message cap — auto-digest initiated`);
|
|
405
|
-
}
|
|
406
|
-
catch (err) {
|
|
407
|
-
logError(`Failed to create digest task for room ${room.id}`, err);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* Handle digest task completion: persist the full digest to disk,
|
|
412
|
-
* clear the pending file, and insert a system message into the room.
|
|
413
|
-
*/
|
|
414
|
-
async function handleDigestCompletion(task) {
|
|
415
|
-
const roomId = task.roomId;
|
|
416
|
-
const digestSummary = task.result ?? '';
|
|
417
|
-
// Read the pending messages that were the basis for this digest
|
|
418
|
-
const pendingPath = path.join(DIGESTS_DIR, `${roomId}.jsonl`);
|
|
419
|
-
let pendingMessages = '';
|
|
420
|
-
try {
|
|
421
|
-
pendingMessages = await fs.readFile(pendingPath, 'utf-8');
|
|
422
|
-
}
|
|
423
|
-
catch {
|
|
424
|
-
// Pending file may have been cleared already
|
|
425
|
-
}
|
|
426
|
-
// Persist the full digest to ~/.crosschat/digests/{roomId}/{timestamp}.md
|
|
427
|
-
const roomDigestDir = path.join(DIGESTS_DIR, roomId);
|
|
428
|
-
await fs.mkdir(roomDigestDir, { recursive: true });
|
|
429
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
430
|
-
const digestPath = path.join(roomDigestDir, `${timestamp}.md`);
|
|
431
|
-
// Format original messages for the "Full messages" section
|
|
432
|
-
let formattedMessages = '';
|
|
433
|
-
if (pendingMessages.trim()) {
|
|
434
|
-
const msgLines = pendingMessages.trim().split('\n');
|
|
435
|
-
for (const line of msgLines) {
|
|
436
|
-
try {
|
|
437
|
-
const msg = JSON.parse(line);
|
|
438
|
-
formattedMessages += `**${msg.fromName}** (${msg.timestamp}): ${msg.content}\n\n`;
|
|
439
|
-
}
|
|
440
|
-
catch {
|
|
441
|
-
// Skip malformed lines
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
const digestContent = `# Room Digest: ${roomId}\n\n` +
|
|
446
|
-
`Generated: ${new Date().toISOString()}\n\n` +
|
|
447
|
-
`## Summary\n\n${digestSummary}\n\n` +
|
|
448
|
-
`## Full messages\n\n${formattedMessages || '(no messages available)'}`;
|
|
449
|
-
await fs.writeFile(digestPath, digestContent, 'utf-8');
|
|
450
|
-
// Clear the pending file
|
|
451
|
-
try {
|
|
452
|
-
await fs.unlink(pendingPath);
|
|
453
|
-
}
|
|
454
|
-
catch {
|
|
455
|
-
// Already gone
|
|
456
|
-
}
|
|
457
|
-
// Clean up tracking
|
|
458
|
-
activeDigestTasks.delete(roomId);
|
|
459
|
-
// Insert a system message into the room
|
|
460
|
-
const firstLine = digestSummary.split('\n')[0].slice(0, 200);
|
|
461
|
-
const room = rooms.get(roomId);
|
|
462
|
-
if (room) {
|
|
463
|
-
const sysMsg = {
|
|
464
|
-
messageId: generateId(),
|
|
465
|
-
roomId,
|
|
466
|
-
fromPeerId: 'system',
|
|
467
|
-
fromName: 'system',
|
|
468
|
-
content: `\u{1F4CB} Room digest: ${firstLine}\n\nFull digest saved to ${digestPath}`,
|
|
469
|
-
timestamp: new Date().toISOString(),
|
|
470
|
-
source: 'system',
|
|
471
|
-
importance: 'important',
|
|
472
|
-
};
|
|
473
|
-
room.messages.push(sysMsg);
|
|
474
|
-
broadcastRoomMessage(roomId, sysMsg);
|
|
475
|
-
}
|
|
476
|
-
log(`Digest completed for room ${roomId}: ${digestPath}`);
|
|
477
|
-
const digestRoom = rooms.get(roomId);
|
|
478
|
-
postActivity(`Digest completed for room "${digestRoom?.name ?? roomId}"`);
|
|
340
|
+
messageManager.addMessage(msg).catch((err) => logError('Failed to persist activity message', err));
|
|
341
|
+
broadcastChannelMessage('crosschat', msg);
|
|
479
342
|
}
|
|
480
343
|
// ── Agent removal ──────────────────────────────────────────────
|
|
481
344
|
function removeAgent(peerId) {
|
|
@@ -502,70 +365,21 @@ export async function startHub() {
|
|
|
502
365
|
function handleAgentStatus(agent, msg) {
|
|
503
366
|
agent.status = msg.status;
|
|
504
367
|
agent.statusDetail = msg.detail;
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
}
|
|
508
|
-
function handleJoinRoom(agent, msg) {
|
|
509
|
-
const room = rooms.get(msg.roomId);
|
|
510
|
-
if (!room) {
|
|
511
|
-
sendError(agent.ws, `Room not found: ${msg.roomId}`);
|
|
512
|
-
return;
|
|
368
|
+
if (msg.taskMessageId) {
|
|
369
|
+
log(`Agent ${agent.name} status: ${msg.status}${msg.detail ? ` (${msg.detail})` : ''} [task: ${msg.taskMessageId}]`);
|
|
513
370
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
// Notify the agent
|
|
517
|
-
sendToWs(agent.ws, { type: 'room.joined', roomId: room.id, name: room.name });
|
|
518
|
-
// Notify browsers about room membership change
|
|
519
|
-
if (oldRoom !== msg.roomId) {
|
|
520
|
-
broadcastToRoomBrowsers(oldRoom, {
|
|
521
|
-
type: 'agentLeft',
|
|
522
|
-
roomId: oldRoom,
|
|
523
|
-
peerId: agent.peerId,
|
|
524
|
-
name: agent.name,
|
|
525
|
-
});
|
|
526
|
-
broadcastToRoomBrowsers(msg.roomId, {
|
|
527
|
-
type: 'agentJoined',
|
|
528
|
-
roomId: msg.roomId,
|
|
529
|
-
peerId: agent.peerId,
|
|
530
|
-
name: agent.name,
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
log(`Agent ${agent.name} joined room ${msg.roomId}`);
|
|
534
|
-
postActivity(`${agent.name} joined room "${room.name}"`, 'chitchat');
|
|
535
|
-
}
|
|
536
|
-
function handleCreateRoom(agent, msg) {
|
|
537
|
-
if (rooms.has(msg.roomId)) {
|
|
538
|
-
sendError(agent.ws, `Room already exists: ${msg.roomId}`);
|
|
539
|
-
return;
|
|
371
|
+
else {
|
|
372
|
+
log(`Agent ${agent.name} status: ${msg.status}${msg.detail ? ` (${msg.detail})` : ''}`);
|
|
540
373
|
}
|
|
541
|
-
|
|
542
|
-
id: msg.roomId,
|
|
543
|
-
name: msg.name ?? msg.roomId,
|
|
544
|
-
messages: [],
|
|
545
|
-
createdAt: new Date().toISOString(),
|
|
546
|
-
};
|
|
547
|
-
rooms.set(msg.roomId, room);
|
|
548
|
-
// Notify the creating agent
|
|
549
|
-
sendToWs(agent.ws, { type: 'room.created', roomId: room.id, name: room.name });
|
|
550
|
-
// Notify all browser clients about the new room
|
|
551
|
-
broadcastToAllBrowsers({
|
|
552
|
-
type: 'roomCreated',
|
|
553
|
-
room: { id: room.id, name: room.name, createdAt: room.createdAt, messageCount: 0 },
|
|
554
|
-
});
|
|
555
|
-
log(`Room created: ${room.id} by ${agent.name}`);
|
|
556
|
-
postActivity(`${agent.name} created room "${room.name}"`);
|
|
374
|
+
postActivity(`${agent.name} -> ${msg.status}${msg.detail ? ` (${msg.detail})` : ''}`, 'chitchat');
|
|
557
375
|
}
|
|
558
|
-
function handleSendMessage(agent, msg) {
|
|
559
|
-
const
|
|
560
|
-
const room = rooms.get(roomId);
|
|
561
|
-
if (!room) {
|
|
562
|
-
sendError(agent.ws, `Room not found: ${roomId}`);
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
376
|
+
async function handleSendMessage(agent, msg) {
|
|
377
|
+
const channelId = agent.currentChannel;
|
|
565
378
|
const { mentions, mentionType } = parseMentions(msg.content);
|
|
566
|
-
const
|
|
379
|
+
const message = {
|
|
567
380
|
messageId: generateId(),
|
|
568
|
-
|
|
381
|
+
channelId,
|
|
382
|
+
threadId: msg.threadId,
|
|
569
383
|
fromPeerId: agent.peerId,
|
|
570
384
|
fromName: agent.name,
|
|
571
385
|
content: msg.content,
|
|
@@ -574,12 +388,13 @@ export async function startHub() {
|
|
|
574
388
|
source: 'agent',
|
|
575
389
|
mentions: mentions.length > 0 ? mentions : undefined,
|
|
576
390
|
mentionType,
|
|
577
|
-
|
|
391
|
+
badges: [],
|
|
578
392
|
};
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
393
|
+
if (msg.importance) {
|
|
394
|
+
message.metadata = { ...message.metadata, importance: msg.importance };
|
|
395
|
+
}
|
|
396
|
+
await messageManager.addMessage(message);
|
|
397
|
+
broadcastChannelMessage(channelId, message);
|
|
583
398
|
}
|
|
584
399
|
function handleListPeers(agent, msg) {
|
|
585
400
|
const peers = [];
|
|
@@ -590,285 +405,232 @@ export async function startHub() {
|
|
|
590
405
|
}
|
|
591
406
|
sendToWs(agent.ws, { type: 'peers', requestId: msg.requestId, peers });
|
|
592
407
|
}
|
|
593
|
-
|
|
408
|
+
// ── Badge & Task handlers ─────────────────────────────────────
|
|
409
|
+
/** Broadcast a badge change to all agents in the channel and all browsers. */
|
|
410
|
+
function broadcastBadgeUpdate(channelId, messageId, badge, allBadges) {
|
|
411
|
+
// Notify agents in the channel about the new badge
|
|
412
|
+
broadcastToChannelAgents(channelId, {
|
|
413
|
+
type: 'message.badgeAdded',
|
|
414
|
+
messageId,
|
|
415
|
+
badge,
|
|
416
|
+
});
|
|
417
|
+
// Notify agents with the full badge array
|
|
418
|
+
broadcastToChannelAgents(channelId, {
|
|
419
|
+
type: 'message.updated',
|
|
420
|
+
messageId,
|
|
421
|
+
badges: allBadges,
|
|
422
|
+
});
|
|
423
|
+
// Notify all browsers
|
|
424
|
+
broadcastToAllBrowsers({
|
|
425
|
+
type: 'badgeUpdate',
|
|
426
|
+
messageId,
|
|
427
|
+
badge,
|
|
428
|
+
badges: allBadges,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
async function handleFlagTask(agent, msg) {
|
|
594
432
|
try {
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
433
|
+
const meta = await messageManager.flagAsTask(msg.messageId, agent.peerId, msg.filter);
|
|
434
|
+
if (!meta) {
|
|
435
|
+
sendError(agent.ws, `Message not found: ${msg.messageId}`, msg.requestId);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const message = messageManager.getMessage(msg.messageId);
|
|
439
|
+
const badges = message?.badges ?? [];
|
|
440
|
+
// Respond to requester
|
|
441
|
+
sendToWs(agent.ws, {
|
|
442
|
+
type: 'task.flagged',
|
|
443
|
+
requestId: msg.requestId,
|
|
444
|
+
messageId: msg.messageId,
|
|
445
|
+
badges,
|
|
602
446
|
});
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
if (
|
|
606
|
-
|
|
447
|
+
// Broadcast badge update to channel
|
|
448
|
+
const taskBadge = badges.find((b) => b.type === 'task');
|
|
449
|
+
if (taskBadge && message) {
|
|
450
|
+
broadcastBadgeUpdate(message.channelId, msg.messageId, taskBadge, badges);
|
|
607
451
|
}
|
|
608
|
-
|
|
609
|
-
broadcastToRoomAgents(agent.currentRoom, { type: 'task.created', task: summary });
|
|
610
|
-
broadcastToRoomBrowsers(agent.currentRoom, { type: 'task.created', task: summary });
|
|
611
|
-
const targetDesc = task.filter?.agentId ? ` -> ${task.filter.agentId}` : '';
|
|
612
|
-
postActivity(`${agent.name} delegated task "${task.description}"${targetDesc}`);
|
|
452
|
+
postActivity(`${agent.name} flagged message as task: ${msg.messageId}`);
|
|
613
453
|
}
|
|
614
454
|
catch (err) {
|
|
615
|
-
sendError(agent.ws, err instanceof Error ? err.message : 'Failed to
|
|
455
|
+
sendError(agent.ws, err instanceof Error ? err.message : 'Failed to flag task', msg.requestId);
|
|
616
456
|
}
|
|
617
457
|
}
|
|
618
|
-
async function
|
|
458
|
+
async function handleClaimTask(agent, msg) {
|
|
619
459
|
try {
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
sendToWs(creator.ws, {
|
|
625
|
-
type: 'task.claimed',
|
|
626
|
-
taskId: task.taskId,
|
|
627
|
-
claimantId: agent.peerId,
|
|
628
|
-
claimantName: agent.name,
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
// Also notify the claimant that the claim was registered
|
|
460
|
+
const meta = await messageManager.claimTask(msg.messageId, agent.peerId, agent.name);
|
|
461
|
+
const message = messageManager.getMessage(msg.messageId);
|
|
462
|
+
const badges = message?.badges ?? [];
|
|
463
|
+
// Respond to requester
|
|
632
464
|
sendToWs(agent.ws, {
|
|
633
465
|
type: 'task.claimed',
|
|
634
|
-
|
|
466
|
+
requestId: msg.requestId,
|
|
467
|
+
messageId: msg.messageId,
|
|
635
468
|
claimantId: agent.peerId,
|
|
636
469
|
claimantName: agent.name,
|
|
637
470
|
});
|
|
638
|
-
//
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
const task = await taskManager.acceptClaim(msg.taskId, agent.peerId);
|
|
649
|
-
// Notify the claimant
|
|
650
|
-
if (task.claimantId) {
|
|
651
|
-
const claimant = findAgentById(task.claimantId);
|
|
652
|
-
if (claimant) {
|
|
653
|
-
sendToWs(claimant.ws, {
|
|
654
|
-
type: 'task.claimAccepted',
|
|
655
|
-
taskId: task.taskId,
|
|
656
|
-
assignedTo: task.claimantId,
|
|
471
|
+
// Notify the message author
|
|
472
|
+
if (message) {
|
|
473
|
+
const author = findAgentById(message.fromPeerId);
|
|
474
|
+
if (author && author.peerId !== agent.peerId) {
|
|
475
|
+
sendToWs(author.ws, {
|
|
476
|
+
type: 'task.claimed',
|
|
477
|
+
requestId: '',
|
|
478
|
+
messageId: msg.messageId,
|
|
479
|
+
claimantId: agent.peerId,
|
|
480
|
+
claimantName: agent.name,
|
|
657
481
|
});
|
|
658
482
|
}
|
|
483
|
+
// Broadcast badge update
|
|
484
|
+
const taskBadge = badges.find((b) => b.type === 'task');
|
|
485
|
+
if (taskBadge) {
|
|
486
|
+
broadcastBadgeUpdate(message.channelId, msg.messageId, taskBadge, badges);
|
|
487
|
+
}
|
|
659
488
|
}
|
|
660
|
-
|
|
661
|
-
sendToWs(agent.ws, {
|
|
662
|
-
type: 'task.claimAccepted',
|
|
663
|
-
taskId: task.taskId,
|
|
664
|
-
assignedTo: task.claimantId ?? '',
|
|
665
|
-
});
|
|
666
|
-
// Broadcast to dashboard
|
|
667
|
-
broadcastToAllBrowsers({ type: 'task.claimAccepted', taskId: task.taskId, assignedTo: task.claimantId ?? '' });
|
|
668
|
-
postActivity(`${agent.name} accepted claim from ${task.claimantName ?? 'unknown'}`, 'chitchat');
|
|
489
|
+
postActivity(`${agent.name} claimed task on message ${msg.messageId}`, 'chitchat');
|
|
669
490
|
}
|
|
670
491
|
catch (err) {
|
|
671
|
-
sendError(agent.ws, err instanceof Error ? err.message : 'Failed to
|
|
492
|
+
sendError(agent.ws, err instanceof Error ? err.message : 'Failed to claim task', msg.requestId);
|
|
672
493
|
}
|
|
673
494
|
}
|
|
674
|
-
async function
|
|
495
|
+
async function handleResolveTask(agent, msg) {
|
|
675
496
|
try {
|
|
676
|
-
const
|
|
677
|
-
const
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
if (task.creatorId !== agent.peerId)
|
|
681
|
-
notifyIds.add(task.creatorId);
|
|
682
|
-
if (task.claimantId && task.claimantId !== agent.peerId)
|
|
683
|
-
notifyIds.add(task.claimantId);
|
|
684
|
-
for (const targetId of notifyIds) {
|
|
685
|
-
const target = findAgentById(targetId);
|
|
686
|
-
if (target) {
|
|
687
|
-
sendToWs(target.ws, {
|
|
688
|
-
type: 'task.updated',
|
|
689
|
-
taskId: task.taskId,
|
|
690
|
-
note: lastNote,
|
|
691
|
-
});
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
// Also confirm to the sender
|
|
497
|
+
const meta = await messageManager.resolveTask(msg.messageId, agent.peerId, msg.status, msg.result, msg.error);
|
|
498
|
+
const message = messageManager.getMessage(msg.messageId);
|
|
499
|
+
const badges = message?.badges ?? [];
|
|
500
|
+
// Respond to requester
|
|
695
501
|
sendToWs(agent.ws, {
|
|
696
|
-
type: 'task.
|
|
697
|
-
|
|
698
|
-
|
|
502
|
+
type: 'task.resolved',
|
|
503
|
+
requestId: msg.requestId,
|
|
504
|
+
messageId: msg.messageId,
|
|
505
|
+
status: msg.status,
|
|
506
|
+
result: msg.result,
|
|
699
507
|
});
|
|
700
|
-
//
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
try {
|
|
709
|
-
const task = await taskManager.complete(msg.taskId, agent.peerId, agent.name, msg.result, msg.status, msg.error);
|
|
710
|
-
// Notify the creator (skip for system-created digest tasks)
|
|
711
|
-
if (task.creatorId !== 'system') {
|
|
712
|
-
const creator = findAgentById(task.creatorId);
|
|
713
|
-
if (creator) {
|
|
714
|
-
sendToWs(creator.ws, {
|
|
715
|
-
type: 'task.completed',
|
|
716
|
-
taskId: task.taskId,
|
|
508
|
+
// Notify the message author
|
|
509
|
+
if (message) {
|
|
510
|
+
const author = findAgentById(message.fromPeerId);
|
|
511
|
+
if (author && author.peerId !== agent.peerId) {
|
|
512
|
+
sendToWs(author.ws, {
|
|
513
|
+
type: 'task.resolved',
|
|
514
|
+
requestId: '',
|
|
515
|
+
messageId: msg.messageId,
|
|
717
516
|
status: msg.status,
|
|
718
517
|
result: msg.result,
|
|
719
518
|
});
|
|
720
519
|
}
|
|
520
|
+
// Broadcast badge update
|
|
521
|
+
const taskBadge = badges.find((b) => b.type === 'task');
|
|
522
|
+
if (taskBadge) {
|
|
523
|
+
broadcastBadgeUpdate(message.channelId, msg.messageId, taskBadge, badges);
|
|
524
|
+
}
|
|
721
525
|
}
|
|
722
|
-
// Confirm to the completer
|
|
723
|
-
sendToWs(agent.ws, {
|
|
724
|
-
type: 'task.completed',
|
|
725
|
-
taskId: task.taskId,
|
|
726
|
-
status: msg.status,
|
|
727
|
-
result: msg.result,
|
|
728
|
-
});
|
|
729
|
-
// Broadcast to dashboard
|
|
730
|
-
broadcastToAllBrowsers({ type: 'task.completed', taskId: task.taskId, status: msg.status, result: msg.result });
|
|
731
526
|
if (msg.status === 'completed') {
|
|
732
|
-
postActivity(`${agent.name} completed task
|
|
527
|
+
postActivity(`${agent.name} completed task on message ${msg.messageId}`, 'important');
|
|
733
528
|
}
|
|
734
529
|
else {
|
|
735
|
-
postActivity(`${agent.name} failed task
|
|
736
|
-
}
|
|
737
|
-
// Check if this is a digest task completion
|
|
738
|
-
if (msg.status === 'completed' && activeDigestTasks.get(task.roomId) === task.taskId) {
|
|
739
|
-
await handleDigestCompletion(task);
|
|
530
|
+
postActivity(`${agent.name} failed task on message ${msg.messageId}`, 'important');
|
|
740
531
|
}
|
|
741
532
|
}
|
|
742
533
|
catch (err) {
|
|
743
|
-
sendError(agent.ws, err instanceof Error ? err.message : 'Failed to
|
|
534
|
+
sendError(agent.ws, err instanceof Error ? err.message : 'Failed to resolve task', msg.requestId);
|
|
744
535
|
}
|
|
745
536
|
}
|
|
746
|
-
async function
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
type: 'sessionCleared',
|
|
759
|
-
roomId: agent.currentRoom,
|
|
760
|
-
clearedBy: agent.name,
|
|
761
|
-
messagesCleared,
|
|
762
|
-
});
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
if (clearTasks) {
|
|
766
|
-
tasksArchived = await taskManager.archiveTerminal(agent.currentRoom);
|
|
767
|
-
}
|
|
768
|
-
sendToWs(agent.ws, {
|
|
769
|
-
type: 'session.cleared',
|
|
770
|
-
requestId: msg.requestId,
|
|
771
|
-
messagesCleared,
|
|
772
|
-
tasksArchived,
|
|
773
|
-
});
|
|
774
|
-
log(`Session cleared by ${agent.name}: ${messagesCleared} message(s), ${tasksArchived} task(s) archived`);
|
|
775
|
-
}
|
|
776
|
-
async function handleRequestDigest(agent, msg) {
|
|
777
|
-
const room = rooms.get(agent.currentRoom);
|
|
778
|
-
if (!room || room.messages.length === 0) {
|
|
779
|
-
sendError(agent.ws, 'No messages in room to digest', msg.requestId);
|
|
780
|
-
return;
|
|
781
|
-
}
|
|
782
|
-
// Check if a digest task is already active for this room
|
|
783
|
-
const existingTaskId = activeDigestTasks.get(agent.currentRoom);
|
|
784
|
-
if (existingTaskId) {
|
|
785
|
-
const existingTask = taskManager.get(existingTaskId);
|
|
786
|
-
if (existingTask && (existingTask.status === 'open' || existingTask.status === 'claimed' || existingTask.status === 'in_progress')) {
|
|
787
|
-
sendError(agent.ws, `A digest task is already in progress for this room (task ${existingTaskId})`, msg.requestId);
|
|
537
|
+
async function handleAddBadge(agent, msg) {
|
|
538
|
+
try {
|
|
539
|
+
const badge = {
|
|
540
|
+
type: msg.badgeType,
|
|
541
|
+
value: msg.badgeValue,
|
|
542
|
+
label: msg.label,
|
|
543
|
+
addedBy: agent.peerId,
|
|
544
|
+
addedAt: new Date().toISOString(),
|
|
545
|
+
};
|
|
546
|
+
const updatedMessage = await messageManager.addBadge(msg.messageId, badge);
|
|
547
|
+
if (!updatedMessage) {
|
|
548
|
+
sendError(agent.ws, `Message not found: ${msg.messageId}`, msg.requestId);
|
|
788
549
|
return;
|
|
789
550
|
}
|
|
790
|
-
|
|
791
|
-
}
|
|
792
|
-
// Capture current messages and write to pending digest file
|
|
793
|
-
const messagesToDigest = [...room.messages];
|
|
794
|
-
const pendingPath = path.join(DIGESTS_DIR, `${room.id}.jsonl`);
|
|
795
|
-
const lines = messagesToDigest.map((m) => JSON.stringify(m)).join('\n') + '\n';
|
|
796
|
-
await fs.appendFile(pendingPath, lines, 'utf-8');
|
|
797
|
-
// Optionally clear room messages
|
|
798
|
-
const clearMessages = msg.clearMessages === true;
|
|
799
|
-
if (clearMessages) {
|
|
800
|
-
room.messages = [];
|
|
801
|
-
broadcastToRoomBrowsers(agent.currentRoom, {
|
|
802
|
-
type: 'sessionCleared',
|
|
803
|
-
roomId: agent.currentRoom,
|
|
804
|
-
clearedBy: agent.name,
|
|
805
|
-
messagesCleared: messagesToDigest.length,
|
|
806
|
-
});
|
|
807
|
-
}
|
|
808
|
-
// Create digest task
|
|
809
|
-
let pendingContent;
|
|
810
|
-
try {
|
|
811
|
-
pendingContent = await fs.readFile(pendingPath, 'utf-8');
|
|
812
|
-
}
|
|
813
|
-
catch {
|
|
814
|
-
pendingContent = lines;
|
|
815
|
-
}
|
|
816
|
-
try {
|
|
817
|
-
const task = await taskManager.create({
|
|
818
|
-
roomId: room.id,
|
|
819
|
-
creatorId: agent.peerId,
|
|
820
|
-
creatorName: agent.name,
|
|
821
|
-
description: 'Summarize the following room messages into a digest',
|
|
822
|
-
context: pendingContent,
|
|
823
|
-
});
|
|
824
|
-
activeDigestTasks.set(room.id, task.taskId);
|
|
825
|
-
// Broadcast task creation
|
|
826
|
-
const summary = taskToSummary(task);
|
|
827
|
-
broadcastToRoomAgents(room.id, { type: 'task.created', task: summary });
|
|
828
|
-
broadcastToRoomBrowsers(room.id, { type: 'task.created', task: summary });
|
|
551
|
+
// Respond to requester
|
|
829
552
|
sendToWs(agent.ws, {
|
|
830
|
-
type: '
|
|
553
|
+
type: 'badge.added',
|
|
831
554
|
requestId: msg.requestId,
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
messagesCleared: clearMessages,
|
|
555
|
+
messageId: msg.messageId,
|
|
556
|
+
badge,
|
|
835
557
|
});
|
|
836
|
-
|
|
837
|
-
|
|
558
|
+
// Broadcast badge update to channel
|
|
559
|
+
broadcastBadgeUpdate(updatedMessage.channelId, msg.messageId, badge, updatedMessage.badges);
|
|
838
560
|
}
|
|
839
561
|
catch (err) {
|
|
840
|
-
|
|
841
|
-
sendError(agent.ws, 'Failed to create digest task', msg.requestId);
|
|
562
|
+
sendError(agent.ws, err instanceof Error ? err.message : 'Failed to add badge', msg.requestId);
|
|
842
563
|
}
|
|
843
564
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
565
|
+
// ── Message retrieval ─────────────────────────────────────────
|
|
566
|
+
function handleGetMessages(agent, msg) {
|
|
567
|
+
let messages;
|
|
568
|
+
if (msg.threadId) {
|
|
569
|
+
messages = messageManager.getThreadMessages(msg.threadId);
|
|
849
570
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
571
|
+
else {
|
|
572
|
+
messages = messageManager.getChannelMessages(agent.currentChannel, {
|
|
573
|
+
limit: msg.limit,
|
|
574
|
+
afterMessageId: msg.afterMessageId,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
// Convert Messages to ChannelMessageMessage format for the response
|
|
578
|
+
const protocolMessages = messages.map((m) => ({
|
|
579
|
+
type: 'channel.message',
|
|
580
|
+
channelId: m.channelId,
|
|
581
|
+
messageId: m.messageId,
|
|
582
|
+
threadId: m.threadId,
|
|
583
|
+
fromPeerId: m.fromPeerId,
|
|
584
|
+
fromName: m.fromName,
|
|
585
|
+
content: m.content,
|
|
586
|
+
metadata: m.metadata,
|
|
587
|
+
timestamp: m.timestamp,
|
|
588
|
+
source: m.source,
|
|
589
|
+
mentions: m.mentions,
|
|
590
|
+
mentionType: m.mentionType,
|
|
591
|
+
importance: m.metadata?.importance,
|
|
592
|
+
badges: m.badges,
|
|
593
|
+
}));
|
|
594
|
+
const response = {
|
|
595
|
+
type: 'messages',
|
|
853
596
|
requestId: msg.requestId,
|
|
854
|
-
|
|
855
|
-
|
|
597
|
+
messages: protocolMessages,
|
|
598
|
+
threadId: msg.threadId,
|
|
599
|
+
};
|
|
600
|
+
sendToWs(agent.ws, response);
|
|
856
601
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
if (
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
602
|
+
// ── Session management ────────────────────────────────────────
|
|
603
|
+
async function handleClearSession(agent, msg) {
|
|
604
|
+
const clearMessages = msg.messages !== false; // default true
|
|
605
|
+
let messagesCleared = 0;
|
|
606
|
+
if (clearMessages) {
|
|
607
|
+
messagesCleared = messageManager.clearChannel(agent.currentChannel);
|
|
608
|
+
// Notify browsers that the channel was cleared
|
|
609
|
+
broadcastToChannelBrowsers(agent.currentChannel, {
|
|
610
|
+
type: 'sessionCleared',
|
|
611
|
+
channelId: agent.currentChannel,
|
|
612
|
+
clearedBy: agent.name,
|
|
613
|
+
messagesCleared,
|
|
614
|
+
});
|
|
615
|
+
}
|
|
866
616
|
sendToWs(agent.ws, {
|
|
867
|
-
type: '
|
|
617
|
+
type: 'session.cleared',
|
|
868
618
|
requestId: msg.requestId,
|
|
869
|
-
|
|
619
|
+
messagesCleared,
|
|
870
620
|
});
|
|
621
|
+
log(`Session cleared by ${agent.name}: ${messagesCleared} message(s)`);
|
|
871
622
|
}
|
|
623
|
+
// Return all handler functions so they can be used by dispatch
|
|
624
|
+
// ── Pending permission sweep ──────────────────────────────────
|
|
625
|
+
const permissionSweepInterval = setInterval(() => {
|
|
626
|
+
const now = Date.now();
|
|
627
|
+
for (const [id, perm] of pendingPermissions) {
|
|
628
|
+
if (perm.status === 'pending' && now - new Date(perm.createdAt).getTime() > PERMISSION_TTL_MS) {
|
|
629
|
+
pendingPermissions.delete(id);
|
|
630
|
+
log(`Expired stale pending permission: ${id} (${perm.toolName})`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}, PERMISSION_SWEEP_INTERVAL_MS);
|
|
872
634
|
// ── Agent message dispatch ─────────────────────────────────────
|
|
873
635
|
async function handleAgentMessage(ws, raw) {
|
|
874
636
|
const agent = findAgentByWs(ws);
|
|
@@ -893,45 +655,30 @@ export async function startHub() {
|
|
|
893
655
|
removeAgent(agent.peerId);
|
|
894
656
|
ws.close();
|
|
895
657
|
break;
|
|
896
|
-
case 'agent.joinRoom':
|
|
897
|
-
handleJoinRoom(agent, msg);
|
|
898
|
-
break;
|
|
899
|
-
case 'agent.createRoom':
|
|
900
|
-
handleCreateRoom(agent, msg);
|
|
901
|
-
break;
|
|
902
658
|
case 'agent.sendMessage':
|
|
903
|
-
handleSendMessage(agent, msg);
|
|
659
|
+
await handleSendMessage(agent, msg);
|
|
904
660
|
break;
|
|
905
661
|
case 'agent.listPeers':
|
|
906
662
|
handleListPeers(agent, msg);
|
|
907
663
|
break;
|
|
908
|
-
case '
|
|
909
|
-
|
|
910
|
-
break;
|
|
911
|
-
case 'task.claim':
|
|
912
|
-
await handleTaskClaim(agent, msg);
|
|
664
|
+
case 'agent.getMessages':
|
|
665
|
+
handleGetMessages(agent, msg);
|
|
913
666
|
break;
|
|
914
|
-
case '
|
|
915
|
-
await
|
|
667
|
+
case 'agent.flagTask':
|
|
668
|
+
await handleFlagTask(agent, msg);
|
|
916
669
|
break;
|
|
917
|
-
case '
|
|
918
|
-
await
|
|
670
|
+
case 'agent.claimTask':
|
|
671
|
+
await handleClaimTask(agent, msg);
|
|
919
672
|
break;
|
|
920
|
-
case '
|
|
921
|
-
await
|
|
673
|
+
case 'agent.resolveTask':
|
|
674
|
+
await handleResolveTask(agent, msg);
|
|
922
675
|
break;
|
|
923
|
-
case 'agent.
|
|
924
|
-
|
|
925
|
-
break;
|
|
926
|
-
case 'agent.listTasks':
|
|
927
|
-
handleListTasks(agent, msg);
|
|
676
|
+
case 'agent.addBadge':
|
|
677
|
+
await handleAddBadge(agent, msg);
|
|
928
678
|
break;
|
|
929
679
|
case 'agent.clearSession':
|
|
930
680
|
await handleClearSession(agent, msg);
|
|
931
681
|
break;
|
|
932
|
-
case 'agent.requestDigest':
|
|
933
|
-
await handleRequestDigest(agent, msg);
|
|
934
|
-
break;
|
|
935
682
|
default:
|
|
936
683
|
sendError(ws, `Unknown message type: ${msg.type}`);
|
|
937
684
|
}
|
|
@@ -971,90 +718,100 @@ export async function startHub() {
|
|
|
971
718
|
// Serve dashboard React frontend
|
|
972
719
|
const distPath = path.join(__dirname, '..', '..', 'dashboard', 'dist');
|
|
973
720
|
app.use(express.static(distPath));
|
|
974
|
-
// ── REST:
|
|
975
|
-
app.get('/api/
|
|
976
|
-
|
|
977
|
-
id,
|
|
978
|
-
name,
|
|
979
|
-
createdAt,
|
|
980
|
-
messageCount: messages.length,
|
|
981
|
-
}));
|
|
982
|
-
res.json(list);
|
|
983
|
-
});
|
|
984
|
-
app.post('/api/rooms', (req, res) => {
|
|
985
|
-
const { name } = req.body;
|
|
986
|
-
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
|
987
|
-
res.status(400).json({ error: 'Room name is required' });
|
|
988
|
-
return;
|
|
989
|
-
}
|
|
990
|
-
const id = generateId();
|
|
991
|
-
const room = { id, name: name.trim(), messages: [], createdAt: new Date().toISOString() };
|
|
992
|
-
rooms.set(id, room);
|
|
993
|
-
broadcastToAllBrowsers({
|
|
994
|
-
type: 'roomCreated',
|
|
995
|
-
room: { id, name: room.name, createdAt: room.createdAt, messageCount: 0 },
|
|
996
|
-
});
|
|
997
|
-
res.status(201).json({ id, name: room.name, createdAt: room.createdAt });
|
|
721
|
+
// ── REST: Channels ──────────────────────────────────────────────
|
|
722
|
+
app.get('/api/channels', (_req, res) => {
|
|
723
|
+
res.json([...channels].map((id) => ({ id, name: id })));
|
|
998
724
|
});
|
|
999
|
-
app.get('/api/
|
|
1000
|
-
const
|
|
1001
|
-
if (!
|
|
1002
|
-
res.status(404).json({ error: '
|
|
725
|
+
app.get('/api/channels/:channelId/messages', (req, res) => {
|
|
726
|
+
const channelId = req.params.channelId;
|
|
727
|
+
if (!channels.has(channelId)) {
|
|
728
|
+
res.status(404).json({ error: 'Channel not found' });
|
|
1003
729
|
return;
|
|
1004
730
|
}
|
|
1005
|
-
|
|
1006
|
-
const
|
|
1007
|
-
|
|
1008
|
-
roomId: m.roomId,
|
|
1009
|
-
username: m.fromName,
|
|
1010
|
-
text: m.content,
|
|
1011
|
-
timestamp: m.timestamp,
|
|
1012
|
-
source: m.source,
|
|
1013
|
-
mentions: m.mentions,
|
|
1014
|
-
mentionType: m.mentionType,
|
|
1015
|
-
importance: m.importance,
|
|
1016
|
-
}));
|
|
731
|
+
const limit = req.query.limit ? parseInt(req.query.limit, 10) : undefined;
|
|
732
|
+
const afterMessageId = req.query.afterMessageId;
|
|
733
|
+
const messages = messageManager.getChannelMessages(channelId, { limit, afterMessageId });
|
|
1017
734
|
res.json(messages);
|
|
1018
735
|
});
|
|
1019
|
-
app.post('/api/
|
|
1020
|
-
const
|
|
1021
|
-
if (!
|
|
1022
|
-
res.status(404).json({ error: '
|
|
736
|
+
app.post('/api/channels/:channelId/messages', async (req, res) => {
|
|
737
|
+
const channelId = req.params.channelId;
|
|
738
|
+
if (!channels.has(channelId)) {
|
|
739
|
+
res.status(404).json({ error: 'Channel not found' });
|
|
1023
740
|
return;
|
|
1024
741
|
}
|
|
1025
|
-
const { username, text } = req.body;
|
|
742
|
+
const { username, text, threadId, metadata } = req.body;
|
|
1026
743
|
if (!username || !text) {
|
|
1027
744
|
res.status(400).json({ error: 'username and text are required' });
|
|
1028
745
|
return;
|
|
1029
746
|
}
|
|
1030
747
|
const content = text.trim();
|
|
1031
748
|
const { mentions, mentionType } = parseMentions(content);
|
|
1032
|
-
const
|
|
749
|
+
const message = {
|
|
1033
750
|
messageId: generateId(),
|
|
1034
|
-
|
|
751
|
+
channelId,
|
|
752
|
+
threadId: threadId || undefined,
|
|
1035
753
|
fromPeerId: 'dashboard-user',
|
|
1036
754
|
fromName: username.trim(),
|
|
1037
755
|
content,
|
|
1038
|
-
metadata: undefined,
|
|
1039
756
|
timestamp: new Date().toISOString(),
|
|
1040
757
|
source: 'user',
|
|
1041
758
|
mentions: mentions.length > 0 ? mentions : undefined,
|
|
1042
759
|
mentionType,
|
|
760
|
+
badges: [],
|
|
761
|
+
metadata: metadata || undefined,
|
|
1043
762
|
};
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
763
|
+
await messageManager.addMessage(message);
|
|
764
|
+
broadcastChannelMessage(channelId, message);
|
|
765
|
+
res.status(201).json(message);
|
|
766
|
+
});
|
|
767
|
+
app.get('/api/channels/:channelId/messages/:messageId/thread', (req, res) => {
|
|
768
|
+
const threadId = req.params.messageId;
|
|
769
|
+
const messages = messageManager.getThreadMessages(threadId);
|
|
770
|
+
res.json(messages);
|
|
771
|
+
});
|
|
772
|
+
app.post('/api/channels/:channelId/messages/:messageId/flag-task', async (req, res) => {
|
|
773
|
+
const messageId = req.params.messageId;
|
|
774
|
+
const { addedBy, filter } = req.body;
|
|
775
|
+
const meta = await messageManager.flagAsTask(messageId, addedBy || 'dashboard-user', filter || undefined);
|
|
776
|
+
if (!meta) {
|
|
777
|
+
res.status(404).json({ error: 'Message not found' });
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
const message = messageManager.getMessage(messageId);
|
|
781
|
+
if (message) {
|
|
782
|
+
broadcastToAllBrowsers({
|
|
783
|
+
type: 'message.updated',
|
|
784
|
+
messageId,
|
|
785
|
+
badges: message.badges,
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
res.json(meta);
|
|
789
|
+
});
|
|
790
|
+
app.post('/api/channels/:channelId/messages/:messageId/badges', async (req, res) => {
|
|
791
|
+
const messageId = req.params.messageId;
|
|
792
|
+
const { type: badgeType, value, label, addedBy } = req.body;
|
|
793
|
+
if (!badgeType || !value) {
|
|
794
|
+
res.status(400).json({ error: 'type and value are required' });
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
const badge = {
|
|
798
|
+
type: badgeType,
|
|
799
|
+
value,
|
|
800
|
+
label: label || undefined,
|
|
801
|
+
addedBy: addedBy || 'dashboard-user',
|
|
802
|
+
addedAt: new Date().toISOString(),
|
|
803
|
+
};
|
|
804
|
+
const message = await messageManager.addBadge(messageId, badge);
|
|
805
|
+
if (!message) {
|
|
806
|
+
res.status(404).json({ error: 'Message not found' });
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
broadcastToAllBrowsers({
|
|
810
|
+
type: 'message.badgeAdded',
|
|
811
|
+
messageId,
|
|
812
|
+
badge,
|
|
1057
813
|
});
|
|
814
|
+
res.json({ messageId, badge });
|
|
1058
815
|
});
|
|
1059
816
|
// ── REST: Peers ────────────────────────────────────────────────
|
|
1060
817
|
app.get('/api/peers', (_req, res) => {
|
|
@@ -1064,86 +821,48 @@ export async function startHub() {
|
|
|
1064
821
|
}
|
|
1065
822
|
res.json(peers);
|
|
1066
823
|
});
|
|
1067
|
-
// ── REST:
|
|
1068
|
-
app.
|
|
1069
|
-
const
|
|
1070
|
-
if (
|
|
1071
|
-
res.status(400).json({ error: '
|
|
824
|
+
// ── REST: Agent lookup by parent PID (for permission hook) ─────
|
|
825
|
+
app.get('/api/agents/by-parent-pid/:pid', (req, res) => {
|
|
826
|
+
const parentPid = parseInt(req.params.pid, 10);
|
|
827
|
+
if (isNaN(parentPid)) {
|
|
828
|
+
res.status(400).json({ error: 'Invalid PID' });
|
|
1072
829
|
return;
|
|
1073
830
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
description,
|
|
1080
|
-
context: context || undefined,
|
|
1081
|
-
filter: filter || undefined,
|
|
1082
|
-
});
|
|
1083
|
-
const summary = taskToSummary(task);
|
|
1084
|
-
broadcastToRoomAgents(roomId, { type: 'task.created', task: summary });
|
|
1085
|
-
broadcastToRoomBrowsers(roomId, { type: 'task.created', task: summary });
|
|
1086
|
-
res.json(summary);
|
|
1087
|
-
}
|
|
1088
|
-
catch (err) {
|
|
1089
|
-
const message = err instanceof Error ? err.message : 'Failed to create task';
|
|
1090
|
-
res.status(400).json({ error: message });
|
|
831
|
+
for (const agent of agents.values()) {
|
|
832
|
+
if (agent.parentPid === parentPid) {
|
|
833
|
+
res.json({ peerId: agent.peerId, name: agent.name, cwd: agent.cwd });
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
1091
836
|
}
|
|
837
|
+
res.status(404).json({ error: 'No agent found with that parent PID' });
|
|
1092
838
|
});
|
|
839
|
+
// ── REST: Tasks ────────────────────────────────────────────────
|
|
1093
840
|
app.get('/api/tasks', (req, res) => {
|
|
1094
841
|
const filter = {};
|
|
1095
842
|
if (req.query.status)
|
|
1096
843
|
filter.status = req.query.status;
|
|
1097
|
-
if (req.query.
|
|
1098
|
-
filter.
|
|
1099
|
-
if (req.query.
|
|
1100
|
-
filter.
|
|
1101
|
-
const tasks =
|
|
844
|
+
if (req.query.channelId)
|
|
845
|
+
filter.channelId = req.query.channelId;
|
|
846
|
+
if (req.query.claimantId)
|
|
847
|
+
filter.claimantId = req.query.claimantId;
|
|
848
|
+
const tasks = messageManager.listTasks(Object.keys(filter).length > 0 ? filter : undefined);
|
|
1102
849
|
res.json(tasks);
|
|
1103
850
|
});
|
|
1104
|
-
app.get('/api/tasks/:id', (req, res) => {
|
|
1105
|
-
const task = taskManager.get(req.params.id);
|
|
1106
|
-
if (!task) {
|
|
1107
|
-
res.status(404).json({ error: 'Task not found' });
|
|
1108
|
-
return;
|
|
1109
|
-
}
|
|
1110
|
-
res.json(task);
|
|
1111
|
-
});
|
|
1112
|
-
app.post('/api/tasks/:id/archive', async (req, res) => {
|
|
1113
|
-
try {
|
|
1114
|
-
const task = await taskManager.archive(req.params.id);
|
|
1115
|
-
res.json(task);
|
|
1116
|
-
}
|
|
1117
|
-
catch (err) {
|
|
1118
|
-
const message = err instanceof Error ? err.message : 'Failed to archive task';
|
|
1119
|
-
res.status(400).json({ error: message });
|
|
1120
|
-
}
|
|
1121
|
-
});
|
|
1122
851
|
// ── REST: Clear session ──────────────────────────────────────────
|
|
1123
|
-
app.post('/api/
|
|
1124
|
-
const
|
|
1125
|
-
if (!
|
|
1126
|
-
res.status(404).json({ error: '
|
|
852
|
+
app.post('/api/channels/:channelId/clear', (req, res) => {
|
|
853
|
+
const channelId = req.params.channelId;
|
|
854
|
+
if (!channels.has(channelId)) {
|
|
855
|
+
res.status(404).json({ error: 'Channel not found' });
|
|
1127
856
|
return;
|
|
1128
857
|
}
|
|
1129
|
-
const
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
messagesCleared
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
type: 'sessionCleared',
|
|
1138
|
-
roomId: req.params.roomId,
|
|
1139
|
-
clearedBy: 'dashboard',
|
|
1140
|
-
messagesCleared,
|
|
1141
|
-
});
|
|
1142
|
-
}
|
|
1143
|
-
if (clearTasks) {
|
|
1144
|
-
tasksArchived = await taskManager.archiveTerminal(req.params.roomId);
|
|
1145
|
-
}
|
|
1146
|
-
res.json({ messagesCleared, tasksArchived });
|
|
858
|
+
const messagesCleared = messageManager.clearChannel(channelId);
|
|
859
|
+
broadcastToAllBrowsers({
|
|
860
|
+
type: 'sessionCleared',
|
|
861
|
+
channelId,
|
|
862
|
+
clearedBy: 'dashboard',
|
|
863
|
+
messagesCleared,
|
|
864
|
+
});
|
|
865
|
+
res.json({ messagesCleared });
|
|
1147
866
|
});
|
|
1148
867
|
// ── REST: Permissions ────────────────────────────────────────────
|
|
1149
868
|
app.get('/api/permissions', (_req, res) => {
|
|
@@ -1320,19 +1039,13 @@ export async function startHub() {
|
|
|
1320
1039
|
const agentName = `agent-${instance.id.slice(0, 8)}`;
|
|
1321
1040
|
const script = `tell application "Terminal"
|
|
1322
1041
|
activate
|
|
1323
|
-
do script "cd \\"${escapedPath}\\" &&
|
|
1042
|
+
do script "cd \\"${escapedPath}\\" && CROSSCHAT_NAME=${agentName} claude crosschat"
|
|
1324
1043
|
end tell`;
|
|
1325
1044
|
spawn('osascript', ['-e', script], { detached: true, stdio: 'ignore' }).unref();
|
|
1326
1045
|
log(`Launched Claude Code in: ${instance.path}`);
|
|
1327
1046
|
postActivity(`Launched Claude Code at ${instance.path}`);
|
|
1328
1047
|
res.json({ launched: true, instanceId: instance.id, path: instance.path });
|
|
1329
1048
|
});
|
|
1330
|
-
// ── REST: Hub control ──────────────────────────────────────────────
|
|
1331
|
-
app.post('/api/hub/shutdown', (_req, res) => {
|
|
1332
|
-
res.json({ shutting_down: true });
|
|
1333
|
-
// Delay slightly so the response is sent before shutdown
|
|
1334
|
-
setTimeout(() => shutdown('API request'), 100);
|
|
1335
|
-
});
|
|
1336
1049
|
// ── API 404 — reject unmatched API routes before SPA fallback ──
|
|
1337
1050
|
app.use('/api', (_req, res) => {
|
|
1338
1051
|
res.status(404).json({ error: 'API route not found' });
|
|
@@ -1369,7 +1082,7 @@ end tell`;
|
|
|
1369
1082
|
agentWss.emit('connection', ws, request);
|
|
1370
1083
|
});
|
|
1371
1084
|
}
|
|
1372
|
-
else if (pathname === '/ws') {
|
|
1085
|
+
else if (pathname === '/ws' || pathname === '/ws/browser') {
|
|
1373
1086
|
browserWss.handleUpgrade(request, socket, head, (ws) => {
|
|
1374
1087
|
browserWss.emit('connection', ws, request);
|
|
1375
1088
|
});
|
|
@@ -1406,10 +1119,11 @@ end tell`;
|
|
|
1406
1119
|
name: msg.name,
|
|
1407
1120
|
cwd: msg.cwd,
|
|
1408
1121
|
pid: msg.pid,
|
|
1122
|
+
parentPid: msg.parentPid,
|
|
1409
1123
|
ws,
|
|
1410
1124
|
status: 'available',
|
|
1411
1125
|
statusDetail: undefined,
|
|
1412
|
-
|
|
1126
|
+
currentChannel: 'general',
|
|
1413
1127
|
connectedAt: new Date().toISOString(),
|
|
1414
1128
|
};
|
|
1415
1129
|
// If an agent with the same peerId is already connected, close the old one
|
|
@@ -1469,7 +1183,7 @@ end tell`;
|
|
|
1469
1183
|
// ── Browser WebSocket handler ──────────────────────────────────
|
|
1470
1184
|
browserWss.on('connection', (ws) => {
|
|
1471
1185
|
browserClients.add(ws);
|
|
1472
|
-
|
|
1186
|
+
browserChannels.set(ws, new Set());
|
|
1473
1187
|
// Issue a session token for this dashboard connection
|
|
1474
1188
|
const sessionToken = generateId();
|
|
1475
1189
|
browserTokens.set(ws, sessionToken);
|
|
@@ -1486,30 +1200,30 @@ end tell`;
|
|
|
1486
1200
|
}
|
|
1487
1201
|
switch (data.type) {
|
|
1488
1202
|
case 'join': {
|
|
1489
|
-
const
|
|
1490
|
-
if (!
|
|
1491
|
-
ws.send(JSON.stringify({ type: 'error', error: '
|
|
1203
|
+
const channelId = data.channelId;
|
|
1204
|
+
if (!channelId || !channels.has(channelId)) {
|
|
1205
|
+
ws.send(JSON.stringify({ type: 'error', error: 'Channel not found' }));
|
|
1492
1206
|
return;
|
|
1493
1207
|
}
|
|
1494
|
-
|
|
1208
|
+
browserChannels.get(ws).add(channelId);
|
|
1495
1209
|
if (!data.silent) {
|
|
1496
|
-
|
|
1210
|
+
broadcastToChannelBrowsers(channelId, {
|
|
1497
1211
|
type: 'userJoined',
|
|
1498
|
-
|
|
1212
|
+
channelId,
|
|
1499
1213
|
username: data.username || 'Anonymous',
|
|
1500
1214
|
});
|
|
1501
1215
|
}
|
|
1502
1216
|
break;
|
|
1503
1217
|
}
|
|
1504
1218
|
case 'message': {
|
|
1505
|
-
const
|
|
1506
|
-
if (!
|
|
1219
|
+
const channelId = data.channelId;
|
|
1220
|
+
if (!channelId || !channels.has(channelId) || !data.username || !data.text)
|
|
1507
1221
|
return;
|
|
1508
1222
|
const wsContent = data.text.trim();
|
|
1509
1223
|
const { mentions: wsMentions, mentionType: wsMentionType } = parseMentions(wsContent);
|
|
1510
1224
|
const chatMsg = {
|
|
1511
1225
|
messageId: generateId(),
|
|
1512
|
-
|
|
1226
|
+
channelId,
|
|
1513
1227
|
fromPeerId: 'dashboard-user',
|
|
1514
1228
|
fromName: data.username.trim(),
|
|
1515
1229
|
content: wsContent,
|
|
@@ -1518,15 +1232,15 @@ end tell`;
|
|
|
1518
1232
|
source: 'user',
|
|
1519
1233
|
mentions: wsMentions.length > 0 ? wsMentions : undefined,
|
|
1520
1234
|
mentionType: wsMentionType,
|
|
1235
|
+
badges: [],
|
|
1521
1236
|
};
|
|
1522
|
-
|
|
1523
|
-
enforceRoomMessageCap(room).catch((err) => logError('Error enforcing room message cap', err));
|
|
1237
|
+
messageManager.addMessage(chatMsg).catch((err) => logError('Error persisting browser message', err));
|
|
1524
1238
|
// Broadcast with mention filtering
|
|
1525
|
-
|
|
1239
|
+
broadcastChannelMessage(channelId, chatMsg);
|
|
1526
1240
|
break;
|
|
1527
1241
|
}
|
|
1528
1242
|
case 'leave': {
|
|
1529
|
-
|
|
1243
|
+
browserChannels.get(ws)?.delete(data.channelId);
|
|
1530
1244
|
break;
|
|
1531
1245
|
}
|
|
1532
1246
|
}
|
|
@@ -1537,7 +1251,7 @@ end tell`;
|
|
|
1537
1251
|
validTokens.delete(token);
|
|
1538
1252
|
browserTokens.delete(ws);
|
|
1539
1253
|
browserClients.delete(ws);
|
|
1540
|
-
|
|
1254
|
+
// WeakMap entry for browserChannels is automatically GC'd
|
|
1541
1255
|
});
|
|
1542
1256
|
ws.on('error', (err) => {
|
|
1543
1257
|
logError('Browser WebSocket error', err);
|
|
@@ -1567,16 +1281,6 @@ end tell`;
|
|
|
1567
1281
|
}
|
|
1568
1282
|
}
|
|
1569
1283
|
}, HEARTBEAT_INTERVAL_MS);
|
|
1570
|
-
// ── Pending permission sweep ──────────────────────────────────
|
|
1571
|
-
const permissionSweepInterval = setInterval(() => {
|
|
1572
|
-
const now = Date.now();
|
|
1573
|
-
for (const [id, perm] of pendingPermissions) {
|
|
1574
|
-
if (perm.status === 'pending' && now - new Date(perm.createdAt).getTime() > PERMISSION_TTL_MS) {
|
|
1575
|
-
pendingPermissions.delete(id);
|
|
1576
|
-
log(`Expired stale pending permission: ${id} (${perm.toolName})`);
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
}, PERMISSION_SWEEP_INTERVAL_MS);
|
|
1580
1284
|
// ── Start listening ────────────────────────────────────────────
|
|
1581
1285
|
const configPort = process.env.CROSSCHAT_DASHBOARD_PORT
|
|
1582
1286
|
? parseInt(process.env.CROSSCHAT_DASHBOARD_PORT, 10)
|
|
@@ -1605,7 +1309,7 @@ end tell`;
|
|
|
1605
1309
|
});
|
|
1606
1310
|
await writeDashboardLock(actualPort);
|
|
1607
1311
|
log(`Hub started on port ${actualPort} (pid ${process.pid})`);
|
|
1608
|
-
// Post startup event to the activity
|
|
1312
|
+
// Post startup event to the activity channel
|
|
1609
1313
|
postActivity(`Hub started on port ${actualPort}`, 'important');
|
|
1610
1314
|
// ── Graceful shutdown ──────────────────────────────────────────
|
|
1611
1315
|
let shuttingDown = false;
|