agentgui 1.0.94 → 1.0.96
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/.prd +6 -0
- package/database.js +43 -1
- package/lib/claude-runner.js +24 -22
- package/package.json +1 -1
- package/server.js +133 -146
- package/static/index.html +422 -977
- package/static/js/client.js +229 -144
- package/static/js/features.js +43 -25
- package/static/js/websocket-manager.js +1 -1
package/.prd
CHANGED
package/database.js
CHANGED
|
@@ -198,7 +198,9 @@ try {
|
|
|
198
198
|
gitBranch: 'TEXT',
|
|
199
199
|
sourcePath: 'TEXT',
|
|
200
200
|
lastSyncedAt: 'INTEGER',
|
|
201
|
-
workingDirectory: 'TEXT'
|
|
201
|
+
workingDirectory: 'TEXT',
|
|
202
|
+
claudeSessionId: 'TEXT',
|
|
203
|
+
isStreaming: 'INTEGER DEFAULT 0'
|
|
202
204
|
};
|
|
203
205
|
|
|
204
206
|
let addedColumns = false;
|
|
@@ -286,6 +288,46 @@ export const queries = {
|
|
|
286
288
|
};
|
|
287
289
|
},
|
|
288
290
|
|
|
291
|
+
setClaudeSessionId(conversationId, claudeSessionId) {
|
|
292
|
+
const stmt = db.prepare('UPDATE conversations SET claudeSessionId = ?, updated_at = ? WHERE id = ?');
|
|
293
|
+
stmt.run(claudeSessionId, Date.now(), conversationId);
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
getClaudeSessionId(conversationId) {
|
|
297
|
+
const stmt = db.prepare('SELECT claudeSessionId FROM conversations WHERE id = ?');
|
|
298
|
+
const row = stmt.get(conversationId);
|
|
299
|
+
return row?.claudeSessionId || null;
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
setIsStreaming(conversationId, isStreaming) {
|
|
303
|
+
const stmt = db.prepare('UPDATE conversations SET isStreaming = ?, updated_at = ? WHERE id = ?');
|
|
304
|
+
stmt.run(isStreaming ? 1 : 0, Date.now(), conversationId);
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
getIsStreaming(conversationId) {
|
|
308
|
+
const stmt = db.prepare('SELECT isStreaming FROM conversations WHERE id = ?');
|
|
309
|
+
const row = stmt.get(conversationId);
|
|
310
|
+
return row?.isStreaming === 1;
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
markSessionIncomplete(sessionId, errorMsg) {
|
|
314
|
+
const stmt = db.prepare('UPDATE sessions SET status = ?, error = ?, completed_at = ? WHERE id = ?');
|
|
315
|
+
stmt.run('incomplete', errorMsg || 'unknown', Date.now(), sessionId);
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
getSessionsProcessingLongerThan(minutes) {
|
|
319
|
+
const cutoff = Date.now() - (minutes * 60 * 1000);
|
|
320
|
+
const stmt = db.prepare('SELECT * FROM sessions WHERE status = ? AND started_at < ?');
|
|
321
|
+
return stmt.all('pending', cutoff);
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
cleanupOrphanedSessions(days) {
|
|
325
|
+
const cutoff = Date.now() - (days * 24 * 60 * 60 * 1000);
|
|
326
|
+
const stmt = db.prepare('DELETE FROM sessions WHERE status = ? AND started_at < ?');
|
|
327
|
+
const result = stmt.run('pending', cutoff);
|
|
328
|
+
return result.changes || 0;
|
|
329
|
+
},
|
|
330
|
+
|
|
289
331
|
createMessage(conversationId, role, content, idempotencyKey = null) {
|
|
290
332
|
if (idempotencyKey) {
|
|
291
333
|
const cached = this.getIdempotencyKey(idempotencyKey);
|
package/lib/claude-runner.js
CHANGED
|
@@ -1,23 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Configuration for Claude runner
|
|
5
|
-
* @typedef {Object} ClaudeRunnerConfig
|
|
6
|
-
* @property {boolean} [skipPermissions=false] - Use --dangerously-skip-permissions flag
|
|
7
|
-
* @property {boolean} [verbose=true] - Use --verbose flag
|
|
8
|
-
* @property {string} [outputFormat='stream-json'] - Output format (stream-json, json, text)
|
|
9
|
-
* @property {number} [timeout=300000] - Timeout in milliseconds (default 5 minutes)
|
|
10
|
-
* @property {boolean} [print=true] - Use --print flag
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Run Claude with streaming JSON output
|
|
15
|
-
* @param {string} prompt - The prompt to send to Claude
|
|
16
|
-
* @param {string} cwd - Working directory
|
|
17
|
-
* @param {string} agentId - Agent identifier (for logging)
|
|
18
|
-
* @param {ClaudeRunnerConfig} [config={}] - Configuration options
|
|
19
|
-
* @returns {Promise<Array>} Array of parsed JSON objects from Claude output
|
|
20
|
-
*/
|
|
21
3
|
export async function runClaudeWithStreaming(prompt, cwd, agentId = 'claude-code', config = {}) {
|
|
22
4
|
return new Promise((resolve, reject) => {
|
|
23
5
|
const {
|
|
@@ -25,20 +7,25 @@ export async function runClaudeWithStreaming(prompt, cwd, agentId = 'claude-code
|
|
|
25
7
|
verbose = true,
|
|
26
8
|
outputFormat = 'stream-json',
|
|
27
9
|
timeout = 300000,
|
|
28
|
-
print = true
|
|
10
|
+
print = true,
|
|
11
|
+
resumeSessionId = null,
|
|
12
|
+
systemPrompt = null,
|
|
13
|
+
onEvent = null
|
|
29
14
|
} = config;
|
|
30
15
|
|
|
31
|
-
// Build flags array
|
|
32
16
|
const flags = [];
|
|
33
17
|
if (print) flags.push('--print');
|
|
34
18
|
if (verbose) flags.push('--verbose');
|
|
35
19
|
flags.push(`--output-format=${outputFormat}`);
|
|
36
20
|
if (skipPermissions) flags.push('--dangerously-skip-permissions');
|
|
21
|
+
if (resumeSessionId) flags.push('--resume', resumeSessionId);
|
|
22
|
+
if (systemPrompt) flags.push('--append-system-prompt', systemPrompt);
|
|
37
23
|
|
|
38
24
|
const proc = spawn('claude', flags, { cwd });
|
|
39
25
|
let jsonBuffer = '';
|
|
40
26
|
const outputs = [];
|
|
41
27
|
let timedOut = false;
|
|
28
|
+
let sessionId = null;
|
|
42
29
|
|
|
43
30
|
const timeoutHandle = setTimeout(() => {
|
|
44
31
|
timedOut = true;
|
|
@@ -61,6 +48,16 @@ export async function runClaudeWithStreaming(prompt, cwd, agentId = 'claude-code
|
|
|
61
48
|
try {
|
|
62
49
|
const parsed = JSON.parse(line);
|
|
63
50
|
outputs.push(parsed);
|
|
51
|
+
|
|
52
|
+
if (parsed.session_id) {
|
|
53
|
+
sessionId = parsed.session_id;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (onEvent) {
|
|
57
|
+
try { onEvent(parsed); } catch (e) {
|
|
58
|
+
console.error(`[claude-runner] onEvent error: ${e.message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
64
61
|
} catch (e) {
|
|
65
62
|
console.error(`[claude-runner] JSON parse error on line: ${line.substring(0, 100)}`);
|
|
66
63
|
}
|
|
@@ -79,12 +76,17 @@ export async function runClaudeWithStreaming(prompt, cwd, agentId = 'claude-code
|
|
|
79
76
|
if (code === 0) {
|
|
80
77
|
if (jsonBuffer.trim()) {
|
|
81
78
|
try {
|
|
82
|
-
|
|
79
|
+
const parsed = JSON.parse(jsonBuffer);
|
|
80
|
+
outputs.push(parsed);
|
|
81
|
+
if (parsed.session_id) sessionId = parsed.session_id;
|
|
82
|
+
if (onEvent) {
|
|
83
|
+
try { onEvent(parsed); } catch (e) {}
|
|
84
|
+
}
|
|
83
85
|
} catch (e) {
|
|
84
86
|
console.error(`[claude-runner] Final JSON parse error: ${jsonBuffer.substring(0, 100)}`);
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
|
-
resolve(outputs);
|
|
89
|
+
resolve({ outputs, sessionId });
|
|
88
90
|
} else {
|
|
89
91
|
reject(new Error(`Claude CLI exited with code ${code} for agent ${agentId}`));
|
|
90
92
|
}
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -13,10 +13,11 @@ const express = require('express');
|
|
|
13
13
|
const Busboy = require('busboy');
|
|
14
14
|
const fsbrowse = require('fsbrowse');
|
|
15
15
|
|
|
16
|
-
// System prompt for Claude to format responses as HTML
|
|
17
16
|
const SYSTEM_PROMPT = `Always write your responses in ripple-ui enhanced HTML. Avoid overriding light/dark mode CSS variables. Use all the benefits of HTML to express technical details with proper semantic markup, tables, code blocks, headings, and lists. Write clean, well-structured HTML that respects the existing design system.`;
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
const activeExecutions = new Map();
|
|
19
|
+
const messageQueues = new Map();
|
|
20
|
+
|
|
20
21
|
const debugLog = (msg) => {
|
|
21
22
|
const timestamp = new Date().toISOString();
|
|
22
23
|
console.error(`[${timestamp}] ${msg}`);
|
|
@@ -234,19 +235,30 @@ const server = http.createServer(async (req, res) => {
|
|
|
234
235
|
const agentId = body.agentId || 'claude-code';
|
|
235
236
|
const skipPermissions = body.skipPermissions || false;
|
|
236
237
|
|
|
237
|
-
debugLog(`[stream] Starting stream: conversationId=${conversationId}, agentId=${agentId}, skipPermissions=${skipPermissions}`);
|
|
238
|
-
|
|
239
|
-
// Create user message and session immediately
|
|
240
238
|
const userMessage = queries.createMessage(conversationId, 'user', prompt);
|
|
241
|
-
const session = queries.createSession(conversationId);
|
|
242
239
|
queries.createEvent('message.created', { role: 'user', messageId: userMessage.id }, conversationId);
|
|
240
|
+
|
|
241
|
+
broadcastSync({ type: 'message_created', conversationId, message: userMessage, timestamp: Date.now() });
|
|
242
|
+
|
|
243
|
+
if (activeExecutions.has(conversationId)) {
|
|
244
|
+
debugLog(`[stream] Conversation ${conversationId} is busy, queuing message`);
|
|
245
|
+
if (!messageQueues.has(conversationId)) messageQueues.set(conversationId, []);
|
|
246
|
+
messageQueues.get(conversationId).push({ content: prompt, agentId, skipPermissions, messageId: userMessage.id });
|
|
247
|
+
|
|
248
|
+
const queueLength = messageQueues.get(conversationId).length;
|
|
249
|
+
broadcastSync({ type: 'queue_status', conversationId, queueLength, messageId: userMessage.id, timestamp: Date.now() });
|
|
250
|
+
|
|
251
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
252
|
+
res.end(JSON.stringify({ message: userMessage, queued: true, queuePosition: queueLength }));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const session = queries.createSession(conversationId);
|
|
243
257
|
queries.createEvent('session.created', { messageId: userMessage.id, sessionId: session.id }, conversationId, session.id);
|
|
244
258
|
|
|
245
|
-
// Send immediate response with session info
|
|
246
259
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
247
260
|
res.end(JSON.stringify({ message: userMessage, session, streamId: session.id }));
|
|
248
261
|
|
|
249
|
-
// Emit streaming start event
|
|
250
262
|
broadcastSync({
|
|
251
263
|
type: 'streaming_start',
|
|
252
264
|
sessionId: session.id,
|
|
@@ -256,7 +268,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
256
268
|
timestamp: Date.now()
|
|
257
269
|
});
|
|
258
270
|
|
|
259
|
-
// Fire-and-forget streaming with error handling
|
|
260
271
|
processMessageWithStreaming(conversationId, userMessage.id, session.id, prompt, agentId, skipPermissions)
|
|
261
272
|
.catch(err => debugLog(`[stream] Uncaught error: ${err.message}`));
|
|
262
273
|
return;
|
|
@@ -450,58 +461,63 @@ function serveFile(filePath, res) {
|
|
|
450
461
|
|
|
451
462
|
async function processMessageWithStreaming(conversationId, messageId, sessionId, content, agentId, skipPermissions = false) {
|
|
452
463
|
const startTime = Date.now();
|
|
464
|
+
activeExecutions.set(conversationId, true);
|
|
465
|
+
queries.setIsStreaming(conversationId, true);
|
|
466
|
+
|
|
453
467
|
try {
|
|
454
|
-
debugLog(`[stream] Starting: conversationId=${conversationId}, sessionId=${sessionId}
|
|
468
|
+
debugLog(`[stream] Starting: conversationId=${conversationId}, sessionId=${sessionId}`);
|
|
455
469
|
|
|
456
470
|
const conv = queries.getConversation(conversationId);
|
|
457
471
|
const cwd = conv?.workingDirectory || '/config';
|
|
458
|
-
const
|
|
472
|
+
const resumeSessionId = conv?.claudeSessionId || null;
|
|
459
473
|
|
|
460
|
-
debugLog(`[stream] Calling runClaudeWithStreaming with config: skipPermissions=${skipPermissions}`);
|
|
461
|
-
const config = {
|
|
462
|
-
skipPermissions,
|
|
463
|
-
verbose: true,
|
|
464
|
-
outputFormat: 'stream-json',
|
|
465
|
-
timeout: 1800000, // 30 minutes
|
|
466
|
-
print: true
|
|
467
|
-
};
|
|
468
|
-
|
|
469
|
-
// Prepend system prompt to user content
|
|
470
|
-
const promptWithSystem = `${SYSTEM_PROMPT}\n\n${content}`;
|
|
471
|
-
|
|
472
|
-
const outputs = await runClaudeWithStreaming(promptWithSystem, cwd, actualAgentId, config);
|
|
473
|
-
debugLog(`[stream] Claude returned ${outputs.length} streaming outputs`);
|
|
474
|
-
|
|
475
|
-
// Process streaming outputs similar to processMessage
|
|
476
|
-
// But emit WebSocket events for each block
|
|
477
474
|
let allBlocks = [];
|
|
478
|
-
let lastAssistantMessage = null;
|
|
479
475
|
let eventCount = 0;
|
|
480
476
|
|
|
481
|
-
|
|
482
|
-
if (
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
477
|
+
const onEvent = (parsed) => {
|
|
478
|
+
if (parsed.type === 'assistant' && parsed.message?.content) {
|
|
479
|
+
for (const block of parsed.message.content) {
|
|
480
|
+
allBlocks.push(block);
|
|
481
|
+
eventCount++;
|
|
482
|
+
broadcastSync({
|
|
483
|
+
type: 'streaming_progress',
|
|
484
|
+
sessionId,
|
|
485
|
+
conversationId,
|
|
486
|
+
block,
|
|
487
|
+
blockIndex: allBlocks.length - 1,
|
|
488
|
+
timestamp: Date.now()
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
} else if (parsed.type === 'result' && parsed.result && allBlocks.length === 0) {
|
|
488
492
|
broadcastSync({
|
|
489
493
|
type: 'streaming_progress',
|
|
490
494
|
sessionId,
|
|
491
495
|
conversationId,
|
|
492
|
-
|
|
496
|
+
block: { type: 'text', text: parsed.result },
|
|
497
|
+
blockIndex: 0,
|
|
498
|
+
isResult: true,
|
|
493
499
|
timestamp: Date.now()
|
|
494
500
|
});
|
|
495
|
-
eventCount++;
|
|
496
|
-
} else if (output.type === 'tool_result' && output.result) {
|
|
497
|
-
debugLog(`[stream] Found tool result`);
|
|
498
|
-
allBlocks.push({
|
|
499
|
-
type: 'tool_result',
|
|
500
|
-
result: output.result,
|
|
501
|
-
tool_use_id: output.tool_use_id
|
|
502
|
-
});
|
|
503
|
-
eventCount++;
|
|
504
501
|
}
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
const config = {
|
|
505
|
+
skipPermissions,
|
|
506
|
+
verbose: true,
|
|
507
|
+
outputFormat: 'stream-json',
|
|
508
|
+
timeout: 1800000,
|
|
509
|
+
print: true,
|
|
510
|
+
resumeSessionId,
|
|
511
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
512
|
+
onEvent
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
const { outputs, sessionId: claudeSessionId } = await runClaudeWithStreaming(content, cwd, agentId || 'claude-code', config);
|
|
516
|
+
debugLog(`[stream] Claude returned ${outputs.length} outputs, sessionId=${claudeSessionId}`);
|
|
517
|
+
|
|
518
|
+
if (claudeSessionId && !conv?.claudeSessionId) {
|
|
519
|
+
queries.setClaudeSessionId(conversationId, claudeSessionId);
|
|
520
|
+
debugLog(`[stream] Stored claudeSessionId=${claudeSessionId}`);
|
|
505
521
|
}
|
|
506
522
|
|
|
507
523
|
let messageContent = null;
|
|
@@ -511,28 +527,20 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
511
527
|
blocks: allBlocks,
|
|
512
528
|
timestamp: Date.now()
|
|
513
529
|
});
|
|
514
|
-
debugLog(`[stream] Storing full execution with ${allBlocks.length} blocks`);
|
|
515
530
|
} else {
|
|
516
531
|
let textParts = [];
|
|
517
532
|
for (const output of outputs) {
|
|
518
|
-
if (
|
|
519
|
-
textParts.push(output);
|
|
520
|
-
} else if (output.text) {
|
|
521
|
-
textParts.push(output.text);
|
|
522
|
-
} else if (output.content?.text) {
|
|
523
|
-
textParts.push(output.content.text);
|
|
524
|
-
} else if (output.result) {
|
|
533
|
+
if (output.type === 'result' && output.result) {
|
|
525
534
|
textParts.push(String(output.result));
|
|
535
|
+
} else if (typeof output === 'string') {
|
|
536
|
+
textParts.push(output);
|
|
526
537
|
}
|
|
527
538
|
}
|
|
528
539
|
messageContent = textParts.join('\n').trim();
|
|
529
|
-
debugLog(`[stream] Storing text response: "${messageContent.substring(0, 100)}..."`);
|
|
530
540
|
}
|
|
531
541
|
|
|
532
542
|
if (messageContent) {
|
|
533
|
-
debugLog(`[stream] Creating assistant message`);
|
|
534
543
|
const assistantMessage = queries.createMessage(conversationId, 'assistant', messageContent);
|
|
535
|
-
debugLog(`[stream] Created message with id: ${assistantMessage.id}`);
|
|
536
544
|
broadcastSync({
|
|
537
545
|
type: 'streaming_complete',
|
|
538
546
|
sessionId,
|
|
@@ -541,30 +549,25 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
541
549
|
eventCount,
|
|
542
550
|
timestamp: Date.now()
|
|
543
551
|
});
|
|
544
|
-
|
|
545
|
-
|
|
552
|
+
broadcastSync({
|
|
553
|
+
type: 'message_created',
|
|
554
|
+
conversationId,
|
|
555
|
+
message: assistantMessage,
|
|
556
|
+
timestamp: Date.now()
|
|
557
|
+
});
|
|
546
558
|
}
|
|
547
559
|
|
|
548
|
-
debugLog(`[stream]
|
|
560
|
+
debugLog(`[stream] Completed: ${outputs.length} outputs, ${eventCount} events`);
|
|
549
561
|
} catch (error) {
|
|
550
562
|
const elapsed = Date.now() - startTime;
|
|
551
563
|
debugLog(`[stream] Error after ${elapsed}ms: ${error.message}`);
|
|
552
564
|
|
|
553
|
-
// Mark session as incomplete for recovery
|
|
554
|
-
try {
|
|
555
|
-
const sessionStatus = error.message.includes('timeout') ? 'timeout' : 'error';
|
|
556
|
-
queries.markSessionIncomplete(sessionId, error.message);
|
|
557
|
-
debugLog(`[stream] Session ${sessionId} marked as incomplete (${sessionStatus})`);
|
|
558
|
-
} catch (err) {
|
|
559
|
-
debugLog(`[stream] Failed to mark session: ${err.message}`);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
565
|
broadcastSync({
|
|
563
566
|
type: 'streaming_error',
|
|
564
567
|
sessionId,
|
|
565
568
|
conversationId,
|
|
566
569
|
error: error.message,
|
|
567
|
-
recoverable: elapsed < 60000,
|
|
570
|
+
recoverable: elapsed < 60000,
|
|
568
571
|
timestamp: Date.now()
|
|
569
572
|
});
|
|
570
573
|
|
|
@@ -575,102 +578,91 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
575
578
|
message: errorMessage,
|
|
576
579
|
timestamp: Date.now()
|
|
577
580
|
});
|
|
581
|
+
} finally {
|
|
582
|
+
activeExecutions.delete(conversationId);
|
|
583
|
+
queries.setIsStreaming(conversationId, false);
|
|
584
|
+
drainMessageQueue(conversationId);
|
|
578
585
|
}
|
|
579
586
|
}
|
|
580
587
|
|
|
588
|
+
function drainMessageQueue(conversationId) {
|
|
589
|
+
const queue = messageQueues.get(conversationId);
|
|
590
|
+
if (!queue || queue.length === 0) return;
|
|
591
|
+
|
|
592
|
+
const next = queue.shift();
|
|
593
|
+
if (queue.length === 0) messageQueues.delete(conversationId);
|
|
594
|
+
|
|
595
|
+
debugLog(`[queue] Draining next message for ${conversationId}`);
|
|
596
|
+
|
|
597
|
+
const session = queries.createSession(conversationId);
|
|
598
|
+
queries.createEvent('session.created', { messageId: next.messageId, sessionId: session.id }, conversationId, session.id);
|
|
599
|
+
|
|
600
|
+
broadcastSync({
|
|
601
|
+
type: 'streaming_start',
|
|
602
|
+
sessionId: session.id,
|
|
603
|
+
conversationId,
|
|
604
|
+
messageId: next.messageId,
|
|
605
|
+
agentId: next.agentId,
|
|
606
|
+
timestamp: Date.now()
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
broadcastSync({
|
|
610
|
+
type: 'queue_status',
|
|
611
|
+
conversationId,
|
|
612
|
+
queueLength: queue?.length || 0,
|
|
613
|
+
timestamp: Date.now()
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
processMessageWithStreaming(conversationId, next.messageId, session.id, next.content, next.agentId, next.skipPermissions)
|
|
617
|
+
.catch(err => debugLog(`[queue] Error processing queued message: ${err.message}`));
|
|
618
|
+
}
|
|
619
|
+
|
|
581
620
|
async function processMessage(conversationId, messageId, content, agentId) {
|
|
582
621
|
try {
|
|
583
622
|
debugLog(`[processMessage] Starting: conversationId=${conversationId}, agentId=${agentId}`);
|
|
584
623
|
|
|
585
624
|
const conv = queries.getConversation(conversationId);
|
|
586
625
|
const cwd = conv?.workingDirectory || '/config';
|
|
587
|
-
const
|
|
626
|
+
const resumeSessionId = conv?.claudeSessionId || null;
|
|
588
627
|
|
|
589
|
-
|
|
590
|
-
let contentStr = content;
|
|
591
|
-
if (typeof content === 'object') {
|
|
592
|
-
contentStr = JSON.stringify(content);
|
|
593
|
-
}
|
|
628
|
+
let contentStr = typeof content === 'object' ? JSON.stringify(content) : content;
|
|
594
629
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
debugLog(`[processMessage] Claude returned ${outputs.length} outputs`);
|
|
630
|
+
const { outputs, sessionId: claudeSessionId } = await runClaudeWithStreaming(contentStr, cwd, agentId || 'claude-code', {
|
|
631
|
+
resumeSessionId,
|
|
632
|
+
systemPrompt: SYSTEM_PROMPT
|
|
633
|
+
});
|
|
600
634
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
635
|
+
if (claudeSessionId && !conv?.claudeSessionId) {
|
|
636
|
+
queries.setClaudeSessionId(conversationId, claudeSessionId);
|
|
637
|
+
}
|
|
604
638
|
|
|
639
|
+
let allBlocks = [];
|
|
605
640
|
for (const output of outputs) {
|
|
606
641
|
if (output.type === 'assistant' && output.message?.content) {
|
|
607
|
-
debugLog(`[processMessage] Found assistant message with ${output.message.content.length} content blocks`);
|
|
608
|
-
lastAssistantMessage = output.message;
|
|
609
642
|
allBlocks.push(...(output.message.content || []));
|
|
610
|
-
} else if (output.type === 'tool_result' && output.result) {
|
|
611
|
-
debugLog(`[processMessage] Found tool result: ${typeof output.result}`);
|
|
612
|
-
allBlocks.push({
|
|
613
|
-
type: 'tool_result',
|
|
614
|
-
result: output.result,
|
|
615
|
-
tool_use_id: output.tool_use_id
|
|
616
|
-
});
|
|
617
643
|
}
|
|
618
644
|
}
|
|
619
645
|
|
|
620
|
-
// Store full message structure if we have execution data, otherwise fallback to text
|
|
621
646
|
let messageContent = null;
|
|
622
|
-
|
|
623
647
|
if (allBlocks.length > 0) {
|
|
624
|
-
|
|
625
|
-
messageContent = JSON.stringify({
|
|
626
|
-
type: 'claude_execution',
|
|
627
|
-
blocks: allBlocks,
|
|
628
|
-
timestamp: Date.now()
|
|
629
|
-
});
|
|
630
|
-
debugLog(`[processMessage] Storing full execution with ${allBlocks.length} blocks`);
|
|
648
|
+
messageContent = JSON.stringify({ type: 'claude_execution', blocks: allBlocks, timestamp: Date.now() });
|
|
631
649
|
} else {
|
|
632
|
-
// Fallback: extract text for simple responses
|
|
633
650
|
let textParts = [];
|
|
634
651
|
for (const output of outputs) {
|
|
635
|
-
if (
|
|
636
|
-
|
|
637
|
-
} else if (output.text) {
|
|
638
|
-
textParts.push(output.text);
|
|
639
|
-
} else if (output.content?.text) {
|
|
640
|
-
textParts.push(output.content.text);
|
|
641
|
-
} else if (output.result) {
|
|
642
|
-
textParts.push(String(output.result));
|
|
643
|
-
}
|
|
652
|
+
if (output.type === 'result' && output.result) textParts.push(String(output.result));
|
|
653
|
+
else if (typeof output === 'string') textParts.push(output);
|
|
644
654
|
}
|
|
645
655
|
messageContent = textParts.join('\n').trim();
|
|
646
|
-
debugLog(`[processMessage] Storing text response: "${messageContent.substring(0, 100)}..."`);
|
|
647
656
|
}
|
|
648
657
|
|
|
649
658
|
if (messageContent) {
|
|
650
|
-
debugLog(`[processMessage] Creating assistant message`);
|
|
651
659
|
const assistantMessage = queries.createMessage(conversationId, 'assistant', messageContent);
|
|
652
|
-
|
|
653
|
-
broadcastSync({
|
|
654
|
-
type: 'message_created',
|
|
655
|
-
conversationId,
|
|
656
|
-
message: assistantMessage,
|
|
657
|
-
timestamp: Date.now()
|
|
658
|
-
});
|
|
659
|
-
} else {
|
|
660
|
-
debugLog(`[processMessage] No response content extracted!`);
|
|
660
|
+
broadcastSync({ type: 'message_created', conversationId, message: assistantMessage, timestamp: Date.now() });
|
|
661
661
|
}
|
|
662
|
-
|
|
663
|
-
debugLog(`[processMessage] ✅ Completed: ${outputs.length} outputs received`);
|
|
664
662
|
} catch (error) {
|
|
665
663
|
debugLog(`[processMessage] Error: ${error.message}`);
|
|
666
|
-
debugLog(`[processMessage] Stack: ${error.stack}`);
|
|
667
664
|
const errorMessage = queries.createMessage(conversationId, 'assistant', `Error: ${error.message}`);
|
|
668
|
-
broadcastSync({
|
|
669
|
-
type: 'message_created',
|
|
670
|
-
conversationId,
|
|
671
|
-
message: errorMessage,
|
|
672
|
-
timestamp: Date.now()
|
|
673
|
-
});
|
|
665
|
+
broadcastSync({ type: 'message_created', conversationId, message: errorMessage, timestamp: Date.now() });
|
|
674
666
|
}
|
|
675
667
|
}
|
|
676
668
|
|
|
@@ -700,11 +692,14 @@ wss.on('connection', (ws, req) => {
|
|
|
700
692
|
try {
|
|
701
693
|
const data = JSON.parse(msg);
|
|
702
694
|
if (data.type === 'subscribe') {
|
|
703
|
-
ws.subscriptions.add(data.sessionId);
|
|
704
|
-
|
|
695
|
+
if (data.sessionId) ws.subscriptions.add(data.sessionId);
|
|
696
|
+
if (data.conversationId) ws.subscriptions.add(`conv-${data.conversationId}`);
|
|
697
|
+
const subTarget = data.sessionId || data.conversationId;
|
|
698
|
+
debugLog(`[WebSocket] Client ${ws.clientId} subscribed to ${subTarget}`);
|
|
705
699
|
ws.send(JSON.stringify({
|
|
706
700
|
type: 'subscription_confirmed',
|
|
707
701
|
sessionId: data.sessionId,
|
|
702
|
+
conversationId: data.conversationId,
|
|
708
703
|
timestamp: Date.now()
|
|
709
704
|
}));
|
|
710
705
|
} else if (data.type === 'unsubscribe') {
|
|
@@ -742,25 +737,17 @@ wss.on('connection', (ws, req) => {
|
|
|
742
737
|
|
|
743
738
|
function broadcastSync(event) {
|
|
744
739
|
const data = JSON.stringify(event);
|
|
745
|
-
const isStreamingEvent = event.type && event.type.startsWith('streaming_');
|
|
746
|
-
const targetSessionId = event.sessionId || (event.conversationId && `conv-${event.conversationId}`);
|
|
747
740
|
|
|
748
741
|
for (const ws of syncClients) {
|
|
749
742
|
if (ws.readyState !== 1) continue;
|
|
750
743
|
|
|
751
744
|
let shouldSend = false;
|
|
752
745
|
|
|
753
|
-
if (
|
|
754
|
-
// Streaming events require sessionId subscription
|
|
755
|
-
shouldSend = ws.subscriptions && ws.subscriptions.has(targetSessionId);
|
|
756
|
-
} else if (event.sessionId) {
|
|
757
|
-
// Regular session events require sessionId subscription
|
|
758
|
-
shouldSend = ws.subscriptions && ws.subscriptions.has(event.sessionId);
|
|
759
|
-
} else if (event.type === 'message_created' || event.type === 'conversation_created') {
|
|
760
|
-
// Global events sent to all clients
|
|
746
|
+
if (event.sessionId && ws.subscriptions?.has(event.sessionId)) {
|
|
761
747
|
shouldSend = true;
|
|
762
|
-
} else {
|
|
763
|
-
|
|
748
|
+
} else if (event.conversationId && ws.subscriptions?.has(`conv-${event.conversationId}`)) {
|
|
749
|
+
shouldSend = true;
|
|
750
|
+
} else if (event.type === 'message_created' || event.type === 'conversation_created' || event.type === 'conversations_updated' || event.type === 'conversation_deleted' || event.type === 'queue_status') {
|
|
764
751
|
shouldSend = true;
|
|
765
752
|
}
|
|
766
753
|
|