agentgui 1.0.32 → 1.0.34

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/server.js CHANGED
@@ -9,6 +9,13 @@ import { queries } from './database.js';
9
9
  import ACPConnection from './acp-launcher.js';
10
10
  import { ResponseFormatter } from './response-formatter.js';
11
11
  import { HTMLWrapper } from './html-wrapper.js';
12
+ import { SessionStateStore } from './state-manager.js';
13
+
14
+ // Debug logging to file
15
+ const debugLog = (msg) => {
16
+ const timestamp = new Date().toISOString();
17
+ console.error(`[${timestamp}] ${msg}`);
18
+ };
12
19
 
13
20
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
21
  const PORT = process.env.PORT || 3000;
@@ -21,26 +28,62 @@ if (!fs.existsSync(staticDir)) fs.mkdirSync(staticDir, { recursive: true });
21
28
  // ACP connection pool keyed by agentId
22
29
  const acpPool = new Map();
23
30
 
31
+ // Global session state store - tracks ALL prompt processing with explicit states
32
+ const sessionStateStore = new SessionStateStore();
33
+
34
+ // Periodic cleanup of old sessions
35
+ setInterval(() => {
36
+ sessionStateStore.cleanup(3600000); // Clean sessions older than 1 hour
37
+ }, 600000); // Run every 10 minutes
38
+
39
+ /**
40
+ * Get or create ACP connection with timeout protection
41
+ */
24
42
  async function getACP(agentId, cwd) {
25
43
  let conn = acpPool.get(agentId);
26
- if (conn?.isRunning()) return conn;
44
+ if (conn?.isRunning()) {
45
+ console.log(`[getACP] Returning cached connection for ${agentId}`);
46
+ return conn;
47
+ }
27
48
 
49
+ console.log(`[getACP] Creating new ACP connection for ${agentId}`);
28
50
  conn = new ACPConnection();
29
51
  const agentType = agentId === 'opencode' ? 'opencode' : 'claude-code';
30
52
 
53
+ // Wrap entire init in timeout to prevent indefinite hangs
54
+ return Promise.race([
55
+ initializeACP(conn, agentType, cwd, agentId),
56
+ new Promise((_, reject) =>
57
+ setTimeout(() => reject(new Error('ACP initialization timeout (>60s)')), 60000)
58
+ )
59
+ ]);
60
+ }
61
+
62
+ /**
63
+ * Initialize ACP with all steps
64
+ */
65
+ async function initializeACP(conn, agentType, cwd, agentId) {
31
66
  try {
67
+ console.log(`[getACP] Step 1: Connecting to ${agentType}...`);
32
68
  await conn.connect(agentType, cwd);
69
+ console.log(`[getACP] Step 2: Connected, initializing...`);
33
70
  await conn.initialize();
71
+ console.log(`[getACP] Step 3: Initialized, creating session...`);
34
72
  await conn.newSession(cwd);
73
+ console.log(`[getACP] Step 4: Session created, setting mode...`);
35
74
  await conn.setSessionMode('bypassPermissions');
75
+ console.log(`[getACP] Step 5: Injecting skills...`);
36
76
  // Inject system prompt to ensure HTML/RippleUI formatting
37
77
  await conn.injectSkills();
78
+ console.log(`[getACP] Step 6: Injecting system context...`);
38
79
  await conn.injectSystemContext();
80
+ console.log(`[getACP] Step 7: All initialization complete, caching connection`);
39
81
  acpPool.set(agentId, conn);
40
- console.log(`ACP connection ready for ${agentId} in ${cwd}`);
82
+ console.log(`[getACP] ✅ ACP connection ready for ${agentId} in ${cwd}`);
41
83
  return conn;
42
84
  } catch (err) {
43
- console.error(`Failed to initialize ACP connection for ${agentId}: ${err.message}`);
85
+ console.error(`[getACP] ❌ ERROR: Failed to initialize ACP connection for ${agentId}: ${err.message}`);
86
+ console.error(`[getACP] Stack: ${err.stack}`);
44
87
  acpPool.delete(agentId);
45
88
  if (conn) await conn.terminate();
46
89
  throw new Error(`ACP initialization failed for ${agentId}: ${err.message}`);
@@ -154,10 +197,12 @@ const server = http.createServer(async (req, res) => {
154
197
  broadcastSync({ type: 'message_created', conversationId, message });
155
198
  const session = queries.createSession(conversationId);
156
199
  queries.createEvent('session.created', { messageId: message.id, sessionId: session.id }, conversationId, session.id);
157
- res.writeHead(201, { 'Content-Type': 'application/json' });
158
- res.end(JSON.stringify({ message, session, idempotencyKey }));
159
- processMessage(conversationId, message.id, session.id, body.content, body.agentId, body.folderContext);
160
- return;
200
+ res.writeHead(201, { 'Content-Type': 'application/json' });
201
+ res.end(JSON.stringify({ message, session, idempotencyKey }));
202
+ // Fire-and-forget with proper error handling
203
+ processMessage(conversationId, message.id, session.id, body.content, body.agentId, body.folderContext)
204
+ .catch(err => debugLog(`[processMessage] Uncaught error: ${err.message}`));
205
+ return;
161
206
  }
162
207
  }
163
208
 
@@ -200,6 +245,14 @@ const server = http.createServer(async (req, res) => {
200
245
  return;
201
246
  }
202
247
 
248
+ // Diagnostics endpoint - shows ALL active and recent sessions
249
+ if (routePath === '/api/diagnostics/sessions' && req.method === 'GET') {
250
+ const diagnostics = sessionStateStore.getDiagnostics();
251
+ res.writeHead(200, { 'Content-Type': 'application/json' });
252
+ res.end(JSON.stringify(diagnostics, null, 2));
253
+ return;
254
+ }
255
+
203
256
  if (routePath === '/api/import/claude-code' && req.method === 'GET') {
204
257
  const result = queries.importClaudeCodeConversations();
205
258
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -306,77 +359,177 @@ function serveFile(filePath, res) {
306
359
  });
307
360
  }
308
361
 
362
+ /**
363
+ * Process a user message through the Claude Code ACP with explicit state tracking
364
+ * This is now fully predictable with no hidden failures
365
+ */
309
366
  async function processMessage(conversationId, messageId, sessionId, content, agentId, folderContext) {
367
+ // Create state manager for this session
368
+ const stateManager = sessionStateStore.create(sessionId, conversationId, messageId, 120000);
369
+
310
370
  try {
311
- queries.updateSession(sessionId, { status: 'processing' });
312
- queries.createEvent('session.processing', { sessionId }, conversationId, sessionId);
313
- broadcastSync({ type: 'session_updated', sessionId, status: 'processing' });
371
+ console.log(`[processMessage] Starting: conversationId=${conversationId}, sessionId=${sessionId}`);
372
+ console.log(`[processMessage] Initial state: ${stateManager.getState()}`);
373
+
374
+ // STATE: PENDING → ACQUIRING_ACP
375
+ stateManager.transition(stateManager.constructor.STATES.ACQUIRING_ACP, {
376
+ reason: 'Connecting to ACP',
377
+ data: {}
378
+ });
314
379
 
315
380
  const cwd = folderContext?.path || '/config';
316
- const conn = await getACP(agentId || 'claude-code', cwd);
317
-
318
- let fullText = '';
319
- const blocks = [];
320
- const updateChunks = []; // Track all message chunks in order
321
- conn.onUpdate = (params) => {
322
- const u = params.update;
323
- if (!u) return;
324
- const kind = u.sessionUpdate;
325
- if (kind === 'agent_message_chunk' && u.content?.text) {
326
- fullText += u.content.text;
327
- updateChunks.push({ type: 'text', content: u.content.text, timestamp: Date.now() });
328
- } else if (kind === 'html_content' && u.content?.html) {
329
- blocks.push({ type: 'html', html: u.content.html, title: u.content.title, id: u.content.id });
330
- updateChunks.push({ type: 'html', content: u.content.html, title: u.content.title, timestamp: Date.now() });
331
- } else if (kind === 'image_content' && u.content?.path) {
332
- const imageUrl = BASE_URL + '/api/image/' + encodeURIComponent(u.content.path);
333
- blocks.push({ type: 'image', path: u.content.path, url: imageUrl, title: u.content.title, alt: u.content.alt });
334
- updateChunks.push({ type: 'image', path: u.content.path, url: imageUrl, title: u.content.title, timestamp: Date.now() });
381
+ const actualAgentId = agentId || 'claude-code';
382
+
383
+ try {
384
+ const conn = await getACP(actualAgentId, cwd);
385
+
386
+ // STATE: ACQUIRING_ACP ACP_ACQUIRED
387
+ stateManager.transition(stateManager.constructor.STATES.ACP_ACQUIRED, {
388
+ reason: 'ACP connection established',
389
+ data: { acpConnectionTime: Date.now() }
390
+ });
391
+
392
+ let fullText = '';
393
+ const blocks = [];
394
+ const updateChunks = [];
395
+
396
+ // Setup response accumulation
397
+ conn.onUpdate = (params) => {
398
+ const u = params.update;
399
+ if (!u) return;
400
+ const kind = u.sessionUpdate;
401
+ if (kind === 'agent_message_chunk' && u.content?.text) {
402
+ fullText += u.content.text;
403
+ updateChunks.push({ type: 'text', content: u.content.text, timestamp: Date.now() });
404
+ } else if (kind === 'html_content' && u.content?.html) {
405
+ blocks.push({ type: 'html', html: u.content.html, title: u.content.title, id: u.content.id });
406
+ updateChunks.push({ type: 'html', content: u.content.html, title: u.content.title, timestamp: Date.now() });
407
+ } else if (kind === 'image_content' && u.content?.path) {
408
+ const imageUrl = BASE_URL + '/api/image/' + encodeURIComponent(u.content.path);
409
+ blocks.push({ type: 'image', path: u.content.path, url: imageUrl, title: u.content.title, alt: u.content.alt });
410
+ updateChunks.push({ type: 'image', path: u.content.path, url: imageUrl, title: u.content.title, timestamp: Date.now() });
411
+ }
412
+ };
413
+
414
+ // STATE: ACP_ACQUIRED → SENDING_PROMPT
415
+ stateManager.transition(stateManager.constructor.STATES.SENDING_PROMPT, {
416
+ reason: 'Sending prompt to ACP',
417
+ data: {}
418
+ });
419
+
420
+ console.log(`[processMessage] Sending prompt to ACP (${content.length} chars)`);
421
+ const result = await conn.sendPrompt(content);
422
+ conn.onUpdate = null;
423
+
424
+ // STATE: SENDING_PROMPT → PROCESSING
425
+ stateManager.transition(stateManager.constructor.STATES.PROCESSING, {
426
+ reason: 'ACP processing complete, formatting response',
427
+ data: { promptSentTime: Date.now(), responseReceivedTime: Date.now() }
428
+ });
429
+
430
+ console.log(`[processMessage] ACP returned: stopReason=${result?.stopReason}, fullText=${fullText.length} chars`);
431
+
432
+ // Format response
433
+ let responseText = fullText || result?.result || (result?.stopReason ? `Completed: ${result.stopReason}` : 'No response.');
434
+
435
+ // Wrap response in HTML if needed
436
+ const isHTML = responseText.trim().startsWith('<');
437
+ if (!isHTML) {
438
+ responseText = HTMLWrapper.wrapResponse(responseText);
335
439
  }
336
- };
440
+
441
+ // Segment and format
442
+ const segments = ResponseFormatter.segmentResponse(responseText);
443
+ const metadata = ResponseFormatter.extractMetadata(responseText);
444
+
445
+ const messageContent = blocks.length > 0 ? {
446
+ text: responseText,
447
+ blocks,
448
+ segments,
449
+ metadata,
450
+ updateChunks,
451
+ isHTML: true
452
+ } : {
453
+ text: responseText,
454
+ segments,
455
+ metadata,
456
+ updateChunks,
457
+ isHTML: true
458
+ };
459
+
460
+ // Save response to database
461
+ const assistantMessage = queries.createMessage(conversationId, 'assistant', messageContent);
462
+ queries.updateSession(sessionId, { status: 'completed', response: { text: responseText, messageId: assistantMessage.id }, completed_at: Date.now() });
463
+ queries.createEvent('session.completed', { messageId: assistantMessage.id }, conversationId, sessionId);
464
+
465
+ // Broadcast to connected clients
466
+ broadcastSync({ type: 'session_updated', sessionId, status: 'completed', message: assistantMessage });
467
+
468
+ // STATE: PROCESSING → COMPLETED
469
+ stateManager.transition(stateManager.constructor.STATES.COMPLETED, {
470
+ reason: 'Response successfully generated and saved',
471
+ data: {
472
+ fullText,
473
+ blocks,
474
+ responseLength: responseText.length,
475
+ messageId: assistantMessage.id
476
+ }
477
+ });
337
478
 
338
- const result = await conn.sendPrompt(content);
339
- conn.onUpdate = null;
479
+ console.log(`[processMessage] Session completed: ${stateManager.getSummary().duration}`);
480
+
481
+ } catch (acpError) {
482
+ console.error(`[processMessage] ACP Error: ${acpError.message}`);
483
+ console.error(`[processMessage] Stack: ${acpError.stack}`);
484
+
485
+ // STATE: → ERROR
486
+ stateManager.transition(stateManager.constructor.STATES.ERROR, {
487
+ reason: `ACP error: ${acpError.message}`,
488
+ data: {
489
+ error: acpError.message,
490
+ stackTrace: acpError.stack
491
+ }
492
+ });
340
493
 
341
- let responseText = fullText || result?.result || (result?.stopReason ? `Completed: ${result.stopReason}` : 'No response.');
342
-
343
- // Wrap response in HTML if it's not already
344
- const isHTML = responseText.trim().startsWith('<');
345
- if (!isHTML) {
346
- responseText = HTMLWrapper.wrapResponse(responseText);
494
+ // Save error to database
495
+ const errorMsg = `ACP Error: ${acpError.message}`;
496
+ queries.createMessage(conversationId, 'assistant', errorMsg);
497
+ queries.updateSession(sessionId, { status: 'error', error: acpError.message, completed_at: Date.now() });
498
+ queries.createEvent('session.error', { error: acpError.message, stack: acpError.stack }, conversationId, sessionId);
499
+ broadcastSync({ type: 'session_updated', sessionId, status: 'error', error: acpError.message });
500
+
501
+ // Clean up ACP connection on error
502
+ acpPool.delete(actualAgentId);
503
+ throw acpError;
347
504
  }
348
-
349
- // Segment and format the response for better display
350
- const segments = ResponseFormatter.segmentResponse(responseText);
351
- const metadata = ResponseFormatter.extractMetadata(responseText);
352
-
353
- const messageContent = blocks.length > 0 ? {
354
- text: responseText,
355
- blocks,
356
- segments,
357
- metadata,
358
- updateChunks,
359
- isHTML: true
360
- } : {
361
- text: responseText,
362
- segments,
363
- metadata,
364
- updateChunks,
365
- isHTML: true
366
- };
367
-
368
- const assistantMessage = queries.createMessage(conversationId, 'assistant', messageContent);
369
- queries.updateSession(sessionId, { status: 'completed', response: { text: responseText, messageId: assistantMessage.id }, completed_at: Date.now() });
370
- queries.createEvent('session.completed', { messageId: assistantMessage.id }, conversationId, sessionId);
371
-
372
- broadcastSync({ type: 'session_updated', sessionId, status: 'completed', message: assistantMessage });
373
- } catch (e) {
374
- console.error('processMessage error:', e.message);
375
- queries.createMessage(conversationId, 'assistant', `Error: ${e.message}`);
376
- queries.updateSession(sessionId, { status: 'error', error: e.message, completed_at: Date.now() });
377
- queries.createEvent('session.error', { error: e.message }, conversationId, sessionId);
378
- broadcastSync({ type: 'session_updated', sessionId, status: 'error', error: e.message });
379
- acpPool.delete(agentId || 'claude-code');
505
+
506
+ } catch (fatalError) {
507
+ console.error(`[processMessage] Fatal error: ${fatalError.message}`);
508
+ console.error(`[processMessage] Stack: ${fatalError.stack}`);
509
+
510
+ // Ensure state is in error
511
+ if (!stateManager.isTerminal()) {
512
+ stateManager.transition(stateManager.constructor.STATES.ERROR, {
513
+ reason: `Fatal error: ${fatalError.message}`,
514
+ data: {
515
+ error: fatalError.message,
516
+ stackTrace: fatalError.stack
517
+ }
518
+ });
519
+ }
520
+
521
+ // Log full state history for debugging
522
+ const summary = stateManager.getSummary();
523
+ console.error(`[processMessage] State history: ${JSON.stringify(summary, null, 2)}`);
524
+
525
+ } finally {
526
+ // Cleanup: remove from state store after completion
527
+ setTimeout(() => {
528
+ sessionStateStore.remove(sessionId);
529
+ }, 5000);
530
+
531
+ // Log final state
532
+ console.log(`[processMessage] Final state: ${stateManager.getState()}`);
380
533
  }
381
534
  }
382
535