agentgui 1.0.64 → 1.0.66

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/lib/types.ts ADDED
@@ -0,0 +1,245 @@
1
+ /**
2
+ * TYPES.TS - Complete type definitions for the separated data and sync system
3
+ * Guarantees type safety across CLI tests, server, and client
4
+ * Immutable by design - all structures are readonly
5
+ */
6
+
7
+ // ============================================================================
8
+ // CONVERSATION TYPES
9
+ // ============================================================================
10
+
11
+ export interface Conversation {
12
+ readonly id: string;
13
+ readonly agentId: string;
14
+ readonly title: string | null;
15
+ readonly created_at: number;
16
+ readonly updated_at: number;
17
+ readonly status: ConversationStatus;
18
+ readonly agentType?: string;
19
+ readonly source?: 'gui' | 'imported';
20
+ readonly externalId?: string;
21
+ readonly firstPrompt?: string;
22
+ readonly messageCount?: number;
23
+ readonly projectPath?: string;
24
+ readonly gitBranch?: string;
25
+ readonly sourcePath?: string;
26
+ readonly lastSyncedAt?: number;
27
+ }
28
+
29
+ export type ConversationStatus = 'active' | 'archived' | 'deleted';
30
+
31
+ export interface ConversationCreateInput {
32
+ agentId: string;
33
+ title?: string | null;
34
+ }
35
+
36
+ export interface ConversationUpdateInput {
37
+ title?: string;
38
+ status?: ConversationStatus;
39
+ }
40
+
41
+ // ============================================================================
42
+ // MESSAGE TYPES
43
+ // ============================================================================
44
+
45
+ export interface Message {
46
+ readonly id: string;
47
+ readonly conversationId: string;
48
+ readonly role: MessageRole;
49
+ readonly content: string;
50
+ readonly created_at: number;
51
+ }
52
+
53
+ export type MessageRole = 'user' | 'assistant' | 'system';
54
+
55
+ export interface MessageCreateInput {
56
+ conversationId: string;
57
+ role: MessageRole;
58
+ content: string;
59
+ idempotencyKey?: string;
60
+ }
61
+
62
+ // ============================================================================
63
+ // SESSION TYPES (for message processing)
64
+ // ============================================================================
65
+
66
+ export interface Session {
67
+ readonly id: string;
68
+ readonly conversationId: string;
69
+ readonly status: SessionStatus;
70
+ readonly started_at: number;
71
+ readonly completed_at?: number;
72
+ readonly response?: SessionResponse;
73
+ readonly error?: string;
74
+ }
75
+
76
+ export type SessionStatus = 'pending' | 'processing' | 'completed' | 'error' | 'cancelled';
77
+
78
+ export interface SessionResponse {
79
+ readonly text: string;
80
+ readonly messageId: string;
81
+ }
82
+
83
+ // ============================================================================
84
+ // SYNC STATE TYPES
85
+ // ============================================================================
86
+
87
+ export type SyncState = 'idle' | 'loading' | 'synced' | 'error' | 'offline' | 'reconciling';
88
+
89
+ export interface SyncStatus {
90
+ readonly state: SyncState;
91
+ readonly lastSyncTime?: number;
92
+ readonly nextRetryTime?: number;
93
+ readonly error?: string;
94
+ readonly retryCount: number;
95
+ readonly maxRetries: number;
96
+ }
97
+
98
+ export interface SyncEvent {
99
+ readonly type: SyncEventType;
100
+ readonly timestamp: number;
101
+ readonly data: Record<string, unknown>;
102
+ }
103
+
104
+ export type SyncEventType =
105
+ | 'conversation_created'
106
+ | 'conversation_updated'
107
+ | 'conversation_deleted'
108
+ | 'message_created'
109
+ | 'message_updated'
110
+ | 'message_deleted'
111
+ | 'sync_started'
112
+ | 'sync_completed'
113
+ | 'sync_failed'
114
+ | 'offline_detected'
115
+ | 'online_detected';
116
+
117
+ // ============================================================================
118
+ // PAGINATION TYPES
119
+ // ============================================================================
120
+
121
+ export interface PaginationParams {
122
+ readonly limit: number;
123
+ readonly offset: number;
124
+ }
125
+
126
+ export interface PaginatedResult<T> {
127
+ readonly items: readonly T[];
128
+ readonly total: number;
129
+ readonly limit: number;
130
+ readonly offset: number;
131
+ readonly hasMore: boolean;
132
+ }
133
+
134
+ // ============================================================================
135
+ // IDEMPOTENCY TYPES
136
+ // ============================================================================
137
+
138
+ export interface IdempotencyKey {
139
+ readonly key: string;
140
+ readonly value: string;
141
+ readonly created_at: number;
142
+ readonly ttl: number;
143
+ }
144
+
145
+ // ============================================================================
146
+ // ERROR TYPES
147
+ // ============================================================================
148
+
149
+ export class SyncError extends Error {
150
+ constructor(
151
+ public code: string,
152
+ public message: string,
153
+ public retryable: boolean = false,
154
+ public context?: Record<string, unknown>
155
+ ) {
156
+ super(message);
157
+ this.name = 'SyncError';
158
+ }
159
+ }
160
+
161
+ export type ErrorCode =
162
+ | 'DATABASE_ERROR'
163
+ | 'NETWORK_ERROR'
164
+ | 'SYNC_CONFLICT'
165
+ | 'VALIDATION_ERROR'
166
+ | 'NOT_FOUND'
167
+ | 'UNAUTHORIZED'
168
+ | 'TIMEOUT'
169
+ | 'UNKNOWN';
170
+
171
+ // ============================================================================
172
+ // STATE MACHINE CONTEXT
173
+ // ============================================================================
174
+
175
+ export interface SyncMachineContext {
176
+ readonly conversationId?: string;
177
+ readonly messageId?: string;
178
+ readonly lastError?: Error;
179
+ readonly retryCount: number;
180
+ readonly syncData: Record<string, unknown>;
181
+ }
182
+
183
+ // ============================================================================
184
+ // API RESPONSE TYPES
185
+ // ============================================================================
186
+
187
+ export interface ApiResponse<T> {
188
+ readonly data?: T;
189
+ readonly error?: string;
190
+ readonly timestamp: number;
191
+ }
192
+
193
+ export interface ConversationsListResponse {
194
+ readonly conversations: readonly Conversation[];
195
+ readonly total: number;
196
+ }
197
+
198
+ export interface MessagesListResponse {
199
+ readonly messages: readonly Message[];
200
+ readonly total: number;
201
+ readonly hasMore: boolean;
202
+ }
203
+
204
+ // ============================================================================
205
+ // VALIDATION RESULT TYPES
206
+ // ============================================================================
207
+
208
+ export interface ValidationResult {
209
+ readonly valid: boolean;
210
+ readonly errors: readonly ValidationError[];
211
+ }
212
+
213
+ export interface ValidationError {
214
+ readonly field: string;
215
+ readonly message: string;
216
+ readonly value?: unknown;
217
+ }
218
+
219
+ // ============================================================================
220
+ // CONFLICT RESOLUTION TYPES
221
+ // ============================================================================
222
+
223
+ export type ConflictResolutionStrategy = 'last-write-wins' | 'server-wins' | 'client-wins';
224
+
225
+ export interface ConflictInfo {
226
+ readonly localVersion: unknown;
227
+ readonly remoteVersion: unknown;
228
+ readonly resolution: ConflictResolutionStrategy;
229
+ }
230
+
231
+ // ============================================================================
232
+ // RECOVERY TYPES
233
+ // ============================================================================
234
+
235
+ export interface RecoveryCheckpoint {
236
+ readonly timestamp: number;
237
+ readonly synced: boolean;
238
+ readonly data: Record<string, unknown>;
239
+ }
240
+
241
+ export interface RecoveryState {
242
+ readonly lastCheckpoint?: RecoveryCheckpoint;
243
+ readonly pendingOperations: readonly unknown[];
244
+ readonly offline: boolean;
245
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.64",
3
+ "version": "1.0.66",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -3,14 +3,9 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { WebSocketServer } from 'ws';
6
- import os from 'os';
7
6
  import { execSync } from 'child_process';
8
7
  import { queries } from './database.js';
9
- import ACPConnection from './acp-launcher.js';
10
- import { SessionStateStore } from './state-manager.js';
11
- import { StreamHandler } from './stream-handler.js';
12
- import { StateValidator } from './state-validator.js';
13
- import { getConversationSync } from './conversation-sync.js';
8
+ import { runClaudeWithStreaming } from './lib/claude-runner.js';
14
9
 
15
10
  // Debug logging to file
16
11
  const debugLog = (msg) => {
@@ -26,71 +21,6 @@ const watch = process.argv.includes('--no-watch') ? false : (process.argv.includ
26
21
  const staticDir = path.join(__dirname, 'static');
27
22
  if (!fs.existsSync(staticDir)) fs.mkdirSync(staticDir, { recursive: true });
28
23
 
29
- // ACP connection pool keyed by agentId
30
- const acpPool = new Map();
31
-
32
- // Global session state store - tracks ALL prompt processing with explicit states
33
- const sessionStateStore = new SessionStateStore();
34
-
35
- // Periodic cleanup of old sessions
36
- setInterval(() => {
37
- sessionStateStore.cleanup(3600000); // Clean sessions older than 1 hour
38
- }, 600000); // Run every 10 minutes
39
-
40
- /**
41
- * Get or create ACP connection with timeout protection
42
- */
43
- async function getACP(agentId, cwd) {
44
- let conn = acpPool.get(agentId);
45
- if (conn?.isRunning()) {
46
- console.log(`[getACP] Returning cached connection for ${agentId}`);
47
- return conn;
48
- }
49
-
50
- console.log(`[getACP] Creating new ACP connection for ${agentId}`);
51
- conn = new ACPConnection();
52
- const agentType = agentId === 'opencode' ? 'opencode' : 'claude-code';
53
-
54
- // Wrap entire init in timeout to prevent indefinite hangs
55
- return Promise.race([
56
- initializeACP(conn, agentType, cwd, agentId),
57
- new Promise((_, reject) =>
58
- setTimeout(() => reject(new Error('ACP initialization timeout (>60s)')), 60000)
59
- )
60
- ]);
61
- }
62
-
63
- /**
64
- * Initialize ACP with all steps
65
- */
66
- async function initializeACP(conn, agentType, cwd, agentId) {
67
- try {
68
- console.log(`[getACP] Step 1: Connecting to ${agentType}...`);
69
- await conn.connect(agentType, cwd);
70
- console.log(`[getACP] Step 2: Connected, initializing...`);
71
- await conn.initialize();
72
- console.log(`[getACP] Step 3: Initialized, creating session...`);
73
- await conn.newSession(cwd);
74
- console.log(`[getACP] Step 4: Session created, setting mode...`);
75
- await conn.setSessionMode('bypassPermissions');
76
- console.log(`[getACP] Step 5: Injecting skills...`);
77
- // Inject system prompt to ensure HTML/RippleUI formatting
78
- await conn.injectSkills();
79
- console.log(`[getACP] Step 6: Injecting system context...`);
80
- await conn.injectSystemContext();
81
- console.log(`[getACP] Step 7: All initialization complete, caching connection`);
82
- acpPool.set(agentId, conn);
83
- console.log(`[getACP] ✅ ACP connection ready for ${agentId} in ${cwd}`);
84
- return conn;
85
- } catch (err) {
86
- console.error(`[getACP] ❌ ERROR: Failed to initialize ACP connection for ${agentId}: ${err.message}`);
87
- console.error(`[getACP] Stack: ${err.stack}`);
88
- acpPool.delete(agentId);
89
- if (conn) await conn.terminate();
90
- throw new Error(`ACP initialization failed for ${agentId}: ${err.message}`);
91
- }
92
- }
93
-
94
24
  function discoverAgents() {
95
25
  const agents = [];
96
26
  const binaries = [
@@ -199,13 +129,13 @@ const server = http.createServer(async (req, res) => {
199
129
  const idempotencyKey = body.idempotencyKey || null;
200
130
  const message = queries.createMessage(conversationId, 'user', body.content, idempotencyKey);
201
131
  queries.createEvent('message.created', { role: 'user', messageId: message.id }, conversationId);
202
- broadcastSync({ type: 'message_created', conversationId, message });
132
+ broadcastSync({ type: 'message_created', conversationId, message, timestamp: Date.now() });
203
133
  const session = queries.createSession(conversationId);
204
134
  queries.createEvent('session.created', { messageId: message.id, sessionId: session.id }, conversationId, session.id);
205
135
  res.writeHead(201, { 'Content-Type': 'application/json' });
206
136
  res.end(JSON.stringify({ message, session, idempotencyKey }));
207
137
  // Fire-and-forget with proper error handling
208
- processMessage(conversationId, message.id, session.id, body.content, body.agentId, body.folderContext)
138
+ processMessage(conversationId, message.id, body.content, body.agentId)
209
139
  .catch(err => debugLog(`[processMessage] Uncaught error: ${err.message}`));
210
140
  return;
211
141
  }
@@ -250,45 +180,6 @@ const server = http.createServer(async (req, res) => {
250
180
  return;
251
181
  }
252
182
 
253
- // Diagnostics endpoint - shows ALL active and recent sessions
254
- if (routePath === '/api/diagnostics/sessions' && req.method === 'GET') {
255
- const diagnostics = sessionStateStore.getDiagnostics();
256
- res.writeHead(200, { 'Content-Type': 'application/json' });
257
- res.end(JSON.stringify(diagnostics, null, 2));
258
- return;
259
- }
260
-
261
- const streamUpdatesMatch = routePath.match(/^\/api\/sessions\/([^/]+)\/stream-updates$/);
262
- if (streamUpdatesMatch && req.method === 'GET') {
263
- const sessionId = streamUpdatesMatch[1];
264
- const updates = queries.getSessionStreamUpdates(sessionId);
265
- res.writeHead(200, { 'Content-Type': 'application/json' });
266
- res.end(JSON.stringify({ sessionId, updates, count: updates.length }));
267
- return;
268
- }
269
-
270
- const stateRecoveryMatch = routePath.match(/^\/api\/sessions\/([^/]+)\/state-recovery$/);
271
- if (stateRecoveryMatch && req.method === 'GET') {
272
- const sessionId = stateRecoveryMatch[1];
273
- const state = StateValidator.getSessionState(sessionId);
274
- if (!state) {
275
- res.writeHead(404, { 'Content-Type': 'application/json' });
276
- res.end(JSON.stringify({ error: 'Session not found' }));
277
- return;
278
- }
279
- res.writeHead(200, { 'Content-Type': 'application/json' });
280
- res.end(JSON.stringify(state));
281
- return;
282
- }
283
-
284
- const stateValidationMatch = routePath.match(/^\/api\/sessions\/([^/]+)\/validate$/);
285
- if (stateValidationMatch && req.method === 'GET') {
286
- const sessionId = stateValidationMatch[1];
287
- const validation = StateValidator.validateSession(sessionId);
288
- res.writeHead(200, { 'Content-Type': 'application/json' });
289
- res.end(JSON.stringify(validation));
290
- return;
291
- }
292
183
 
293
184
  if (routePath === '/api/import/claude-code' && req.method === 'GET') {
294
185
  const result = queries.importClaudeCodeConversations();
@@ -396,157 +287,90 @@ function serveFile(filePath, res) {
396
287
  });
397
288
  }
398
289
 
399
- /**
400
- * Process a user message through the Claude Code ACP with real-time streaming
401
- * Updates are persisted to database and broadcast to clients immediately
402
- */
403
- async function processMessage(conversationId, messageId, sessionId, content, agentId, folderContext) {
404
- // Create state manager for this session
405
- const stateManager = sessionStateStore.create(sessionId, conversationId, messageId, 120000);
406
-
290
+ async function processMessage(conversationId, messageId, content, agentId) {
407
291
  try {
408
- console.log(`[processMessage] Starting: conversationId=${conversationId}, sessionId=${sessionId}`);
409
- console.log(`[processMessage] Initial state: ${stateManager.getState()}`);
410
-
411
- // STATE: PENDING → ACQUIRING_ACP
412
- stateManager.transition(stateManager.constructor.STATES.ACQUIRING_ACP, {
413
- reason: 'Connecting to ACP',
414
- data: {}
415
- });
292
+ debugLog(`[processMessage] Starting: conversationId=${conversationId}, agentId=${agentId}`);
416
293
 
417
- const cwd = folderContext?.path || '/config';
294
+ const cwd = '/config';
418
295
  const actualAgentId = agentId || 'claude-code';
419
296
 
420
- try {
421
- const conn = await getACP(actualAgentId, cwd);
422
-
423
- // STATE: ACQUIRING_ACP → ACP_ACQUIRED
424
- stateManager.transition(stateManager.constructor.STATES.ACP_ACQUIRED, {
425
- reason: 'ACP connection established',
426
- data: { acpConnectionTime: Date.now() }
427
- });
428
-
429
- // Create stream handler for real-time persistence and broadcasting
430
- const streamHandler = new StreamHandler(sessionId, conversationId, broadcastSync);
431
- let fullText = '';
432
-
433
- // Setup response streaming
434
- conn.onUpdate = (params) => {
435
- streamHandler.handleUpdate(params, BASE_URL);
436
- const u = params.update;
437
- if (u?.sessionUpdate === 'agent_message_chunk' && u.content?.text) {
438
- fullText += u.content.text;
439
- }
440
- };
441
-
442
- // STATE: ACP_ACQUIRED → SENDING_PROMPT
443
- stateManager.transition(stateManager.constructor.STATES.SENDING_PROMPT, {
444
- reason: 'Sending prompt to ACP',
445
- data: {}
446
- });
447
-
448
- console.log(`[processMessage] Sending prompt to ACP (${content.length} chars)`);
449
- const result = await conn.sendPrompt(content);
450
- conn.onUpdate = null;
451
-
452
- // STATE: SENDING_PROMPT → PROCESSING
453
- stateManager.transition(stateManager.constructor.STATES.PROCESSING, {
454
- reason: 'ACP processing complete, formatting response',
455
- data: { promptSentTime: Date.now(), responseReceivedTime: Date.now() }
456
- });
457
-
458
- console.log(`[processMessage] ACP returned: stopReason=${result?.stopReason}, streamUpdates=${streamHandler.getUpdateCount()}`);
459
-
460
- // Save agent's complete response as-is, without any processing
461
- // The agent workflow must flow naturally - no HTML extraction or interference
462
- const responseText = fullText || result?.result || 'No response.';
463
-
464
- const messageContent = {
465
- text: responseText,
466
- streamUpdatesCount: streamHandler.getUpdateCount()
467
- };
468
-
469
- // Save consolidated response to database
470
- const assistantMessage = queries.createMessage(conversationId, 'assistant', messageContent);
471
- queries.updateSession(sessionId, {
472
- status: 'completed',
473
- response: { text: responseText, messageId: assistantMessage.id },
474
- completed_at: Date.now()
475
- });
476
- queries.createEvent('session.completed', { messageId: assistantMessage.id }, conversationId, sessionId);
297
+ debugLog(`[processMessage] Calling runClaudeWithStreaming with prompt: "${content.substring(0, 50)}..."`);
298
+ const outputs = await runClaudeWithStreaming(content, cwd, actualAgentId);
299
+ debugLog(`[processMessage] Claude returned ${outputs.length} outputs`);
300
+
301
+ // Collect all message blocks to preserve full execution details
302
+ let allBlocks = [];
303
+ let lastAssistantMessage = null;
304
+
305
+ for (const output of outputs) {
306
+ if (output.type === 'assistant' && output.message?.content) {
307
+ debugLog(`[processMessage] Found assistant message with ${output.message.content.length} content blocks`);
308
+ lastAssistantMessage = output.message;
309
+ allBlocks.push(...(output.message.content || []));
310
+ } else if (output.type === 'tool_result' && output.result) {
311
+ debugLog(`[processMessage] Found tool result: ${typeof output.result}`);
312
+ allBlocks.push({
313
+ type: 'tool_result',
314
+ result: output.result,
315
+ tool_use_id: output.tool_use_id
316
+ });
317
+ }
318
+ }
477
319
 
478
- // Broadcast final consolidated response
479
- broadcastSync({
480
- type: 'session_updated',
481
- sessionId,
482
- status: 'completed',
483
- message: assistantMessage
484
- });
320
+ // Store full message structure if we have execution data, otherwise fallback to text
321
+ let messageContent = null;
485
322
 
486
- // STATE: PROCESSING COMPLETED
487
- stateManager.transition(stateManager.constructor.STATES.COMPLETED, {
488
- reason: 'Response successfully generated and saved',
489
- data: {
490
- responseLength: responseText.length,
491
- messageId: assistantMessage.id,
492
- streamUpdates: streamHandler.getUpdateCount()
493
- }
323
+ if (allBlocks.length > 0) {
324
+ // Store full message structure as JSON for proper rendering
325
+ messageContent = JSON.stringify({
326
+ type: 'claude_execution',
327
+ blocks: allBlocks,
328
+ timestamp: Date.now()
494
329
  });
495
-
496
- console.log(`[processMessage] ✅ Session completed with ${streamHandler.getUpdateCount()} stream updates: ${stateManager.getSummary().duration}`);
497
-
498
- } catch (acpError) {
499
- console.error(`[processMessage] ACP Error: ${acpError.message}`);
500
- console.error(`[processMessage] Stack: ${acpError.stack}`);
501
-
502
- // STATE: ERROR
503
- stateManager.transition(stateManager.constructor.STATES.ERROR, {
504
- reason: `ACP error: ${acpError.message}`,
505
- data: {
506
- error: acpError.message,
507
- stackTrace: acpError.stack
330
+ debugLog(`[processMessage] Storing full execution with ${allBlocks.length} blocks`);
331
+ } else {
332
+ // Fallback: extract text for simple responses
333
+ let textParts = [];
334
+ for (const output of outputs) {
335
+ if (typeof output === 'string') {
336
+ textParts.push(output);
337
+ } else if (output.text) {
338
+ textParts.push(output.text);
339
+ } else if (output.content?.text) {
340
+ textParts.push(output.content.text);
341
+ } else if (output.result) {
342
+ textParts.push(String(output.result));
508
343
  }
509
- });
510
-
511
- // Save error to database
512
- const errorMsg = `ACP Error: ${acpError.message}`;
513
- queries.createMessage(conversationId, 'assistant', errorMsg);
514
- queries.updateSession(sessionId, { status: 'error', error: acpError.message, completed_at: Date.now() });
515
- queries.createEvent('session.error', { error: acpError.message, stack: acpError.stack }, conversationId, sessionId);
516
- broadcastSync({ type: 'session_updated', sessionId, status: 'error', error: acpError.message });
517
-
518
- // Clean up ACP connection on error
519
- acpPool.delete(actualAgentId);
520
- throw acpError;
344
+ }
345
+ messageContent = textParts.join('\n').trim();
346
+ debugLog(`[processMessage] Storing text response: "${messageContent.substring(0, 100)}..."`);
521
347
  }
522
348
 
523
- } catch (fatalError) {
524
- console.error(`[processMessage] Fatal error: ${fatalError.message}`);
525
- console.error(`[processMessage] Stack: ${fatalError.stack}`);
526
-
527
- // Ensure state is in error
528
- if (!stateManager.isTerminal()) {
529
- stateManager.transition(stateManager.constructor.STATES.ERROR, {
530
- reason: `Fatal error: ${fatalError.message}`,
531
- data: {
532
- error: fatalError.message,
533
- stackTrace: fatalError.stack
534
- }
349
+ if (messageContent) {
350
+ debugLog(`[processMessage] Creating assistant message`);
351
+ const assistantMessage = queries.createMessage(conversationId, 'assistant', messageContent);
352
+ debugLog(`[processMessage] Created message with id: ${assistantMessage.id}`);
353
+ broadcastSync({
354
+ type: 'message_created',
355
+ conversationId,
356
+ message: assistantMessage,
357
+ timestamp: Date.now()
535
358
  });
359
+ } else {
360
+ debugLog(`[processMessage] No response content extracted!`);
536
361
  }
537
362
 
538
- // Log full state history for debugging
539
- const summary = stateManager.getSummary();
540
- console.error(`[processMessage] State history: ${JSON.stringify(summary, null, 2)}`);
541
-
542
- } finally {
543
- // Cleanup: remove from state store immediately (async to not block)
544
- setImmediate(() => {
545
- sessionStateStore.remove(sessionId);
363
+ debugLog(`[processMessage] Completed: ${outputs.length} outputs received`);
364
+ } catch (error) {
365
+ debugLog(`[processMessage] Error: ${error.message}`);
366
+ debugLog(`[processMessage] Stack: ${error.stack}`);
367
+ const errorMessage = queries.createMessage(conversationId, 'assistant', `Error: ${error.message}`);
368
+ broadcastSync({
369
+ type: 'message_created',
370
+ conversationId,
371
+ message: errorMessage,
372
+ timestamp: Date.now()
546
373
  });
547
-
548
- // Log final state
549
- console.log(`[processMessage] Final state: ${stateManager.getState()}`);
550
374
  }
551
375
  }
552
376
 
@@ -577,29 +401,8 @@ wss.on('connection', (ws, req) => {
577
401
  const data = JSON.parse(msg);
578
402
  if (data.type === 'subscribe') {
579
403
  ws.subscriptions.add(data.sessionId);
580
- // On subscribe, send current state for recovery
581
- const state = StateValidator.getSessionState(data.sessionId);
582
- if (state) {
583
- ws.send(JSON.stringify({
584
- type: 'state_snapshot',
585
- sessionId: data.sessionId,
586
- state,
587
- timestamp: Date.now()
588
- }));
589
- }
590
404
  } else if (data.type === 'unsubscribe') {
591
405
  ws.subscriptions.delete(data.sessionId);
592
- } else if (data.type === 'recovery_request') {
593
- // Client asking to recover from a checkpoint
594
- const state = StateValidator.getSessionState(data.sessionId);
595
- if (state) {
596
- ws.send(JSON.stringify({
597
- type: 'recovery_response',
598
- sessionId: data.sessionId,
599
- state,
600
- timestamp: Date.now()
601
- }));
602
- }
603
406
  }
604
407
  } catch (e) {
605
408
  console.error('WebSocket message parse error:', e.message);
@@ -657,8 +460,6 @@ if (watch) {
657
460
  }
658
461
 
659
462
  process.on('SIGTERM', async () => {
660
- for (const conn of acpPool.values()) await conn.terminate();
661
- acpPool.clear();
662
463
  wss.close(() => server.close(() => process.exit(0)));
663
464
  });
664
465
 
@@ -685,13 +486,6 @@ function onServerReady() {
685
486
  // Then run it every 30 seconds (constant automatic importing)
686
487
  setInterval(performAutoImport, 30000);
687
488
 
688
- // Start conversation sync to watch for changes
689
- try {
690
- const sync = getConversationSync();
691
- sync.start();
692
- } catch (err) {
693
- console.error('[SERVER] Error starting conversation sync:', err.message);
694
- }
695
489
  }
696
490
 
697
491
  function performAutoImport() {