agentgui 1.0.65 → 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/.prd +54 -0
- package/CLAUDE.md +176 -0
- package/lib/claude-runner.js +71 -0
- package/lib/database-service.ts +388 -0
- package/lib/machines.ts +593 -0
- package/lib/schemas.ts +213 -0
- package/lib/sync-service.ts +340 -0
- package/lib/types.ts +245 -0
- package/package.json +1 -1
- package/server.js +69 -277
- package/static/app.js +210 -1575
- package/static/styles.css +135 -0
- package/conversation-sync.js +0 -196
- package/state-manager.js +0 -360
- package/state-validator.js +0 -150
- package/static/sync-manager.js +0 -273
- package/stream-handler.js +0 -106
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
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
|
|
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 = [
|
|
@@ -205,7 +135,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
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,
|
|
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,159 +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
|
-
|
|
409
|
-
console.log(`[processMessage] Initial state: ${stateManager.getState()}`);
|
|
292
|
+
debugLog(`[processMessage] Starting: conversationId=${conversationId}, agentId=${agentId}`);
|
|
410
293
|
|
|
411
|
-
|
|
412
|
-
stateManager.transition(stateManager.constructor.STATES.ACQUIRING_ACP, {
|
|
413
|
-
reason: 'Connecting to ACP',
|
|
414
|
-
data: {}
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
const cwd = folderContext?.path || '/config';
|
|
294
|
+
const cwd = '/config';
|
|
418
295
|
const actualAgentId = agentId || 'claude-code';
|
|
419
296
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
});
|
|
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
|
+
}
|
|
447
319
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
conn.onUpdate = null;
|
|
320
|
+
// Store full message structure if we have execution data, otherwise fallback to text
|
|
321
|
+
let messageContent = null;
|
|
451
322
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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()
|
|
456
329
|
});
|
|
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));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
messageContent = textParts.join('\n').trim();
|
|
346
|
+
debugLog(`[processMessage] Storing text response: "${messageContent.substring(0, 100)}..."`);
|
|
347
|
+
}
|
|
457
348
|
|
|
458
|
-
|
|
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
|
|
349
|
+
if (messageContent) {
|
|
350
|
+
debugLog(`[processMessage] Creating assistant message`);
|
|
470
351
|
const assistantMessage = queries.createMessage(conversationId, 'assistant', messageContent);
|
|
471
|
-
|
|
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);
|
|
477
|
-
|
|
478
|
-
// Broadcast final consolidated response with full message content
|
|
352
|
+
debugLog(`[processMessage] Created message with id: ${assistantMessage.id}`);
|
|
479
353
|
broadcastSync({
|
|
480
|
-
type: '
|
|
481
|
-
sessionId,
|
|
354
|
+
type: 'message_created',
|
|
482
355
|
conversationId,
|
|
483
|
-
status: 'completed',
|
|
484
356
|
message: assistantMessage,
|
|
485
357
|
timestamp: Date.now()
|
|
486
358
|
});
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
stateManager.transition(stateManager.constructor.STATES.COMPLETED, {
|
|
490
|
-
reason: 'Response successfully generated and saved',
|
|
491
|
-
data: {
|
|
492
|
-
responseLength: responseText.length,
|
|
493
|
-
messageId: assistantMessage.id,
|
|
494
|
-
streamUpdates: streamHandler.getUpdateCount()
|
|
495
|
-
}
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
console.log(`[processMessage] ✅ Session completed with ${streamHandler.getUpdateCount()} stream updates: ${stateManager.getSummary().duration}`);
|
|
499
|
-
|
|
500
|
-
} catch (acpError) {
|
|
501
|
-
console.error(`[processMessage] ACP Error: ${acpError.message}`);
|
|
502
|
-
console.error(`[processMessage] Stack: ${acpError.stack}`);
|
|
503
|
-
|
|
504
|
-
// STATE: → ERROR
|
|
505
|
-
stateManager.transition(stateManager.constructor.STATES.ERROR, {
|
|
506
|
-
reason: `ACP error: ${acpError.message}`,
|
|
507
|
-
data: {
|
|
508
|
-
error: acpError.message,
|
|
509
|
-
stackTrace: acpError.stack
|
|
510
|
-
}
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
// Save error to database
|
|
514
|
-
const errorMsg = `ACP Error: ${acpError.message}`;
|
|
515
|
-
const errorMessage = queries.createMessage(conversationId, 'assistant', errorMsg);
|
|
516
|
-
queries.updateSession(sessionId, { status: 'error', error: acpError.message, completed_at: Date.now() });
|
|
517
|
-
queries.createEvent('session.error', { error: acpError.message, stack: acpError.stack }, conversationId, sessionId);
|
|
518
|
-
broadcastSync({ type: 'session_updated', sessionId, conversationId, status: 'error', error: acpError.message, message: errorMessage, timestamp: Date.now() });
|
|
519
|
-
|
|
520
|
-
// Clean up ACP connection on error
|
|
521
|
-
acpPool.delete(actualAgentId);
|
|
522
|
-
throw acpError;
|
|
359
|
+
} else {
|
|
360
|
+
debugLog(`[processMessage] No response content extracted!`);
|
|
523
361
|
}
|
|
524
362
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
stackTrace: fatalError.stack
|
|
536
|
-
}
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// Log full state history for debugging
|
|
541
|
-
const summary = stateManager.getSummary();
|
|
542
|
-
console.error(`[processMessage] State history: ${JSON.stringify(summary, null, 2)}`);
|
|
543
|
-
|
|
544
|
-
} finally {
|
|
545
|
-
// Cleanup: remove from state store immediately (async to not block)
|
|
546
|
-
setImmediate(() => {
|
|
547
|
-
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()
|
|
548
373
|
});
|
|
549
|
-
|
|
550
|
-
// Log final state
|
|
551
|
-
console.log(`[processMessage] Final state: ${stateManager.getState()}`);
|
|
552
374
|
}
|
|
553
375
|
}
|
|
554
376
|
|
|
@@ -579,29 +401,8 @@ wss.on('connection', (ws, req) => {
|
|
|
579
401
|
const data = JSON.parse(msg);
|
|
580
402
|
if (data.type === 'subscribe') {
|
|
581
403
|
ws.subscriptions.add(data.sessionId);
|
|
582
|
-
// On subscribe, send current state for recovery
|
|
583
|
-
const state = StateValidator.getSessionState(data.sessionId);
|
|
584
|
-
if (state) {
|
|
585
|
-
ws.send(JSON.stringify({
|
|
586
|
-
type: 'state_snapshot',
|
|
587
|
-
sessionId: data.sessionId,
|
|
588
|
-
state,
|
|
589
|
-
timestamp: Date.now()
|
|
590
|
-
}));
|
|
591
|
-
}
|
|
592
404
|
} else if (data.type === 'unsubscribe') {
|
|
593
405
|
ws.subscriptions.delete(data.sessionId);
|
|
594
|
-
} else if (data.type === 'recovery_request') {
|
|
595
|
-
// Client asking to recover from a checkpoint
|
|
596
|
-
const state = StateValidator.getSessionState(data.sessionId);
|
|
597
|
-
if (state) {
|
|
598
|
-
ws.send(JSON.stringify({
|
|
599
|
-
type: 'recovery_response',
|
|
600
|
-
sessionId: data.sessionId,
|
|
601
|
-
state,
|
|
602
|
-
timestamp: Date.now()
|
|
603
|
-
}));
|
|
604
|
-
}
|
|
605
406
|
}
|
|
606
407
|
} catch (e) {
|
|
607
408
|
console.error('WebSocket message parse error:', e.message);
|
|
@@ -659,8 +460,6 @@ if (watch) {
|
|
|
659
460
|
}
|
|
660
461
|
|
|
661
462
|
process.on('SIGTERM', async () => {
|
|
662
|
-
for (const conn of acpPool.values()) await conn.terminate();
|
|
663
|
-
acpPool.clear();
|
|
664
463
|
wss.close(() => server.close(() => process.exit(0)));
|
|
665
464
|
});
|
|
666
465
|
|
|
@@ -687,13 +486,6 @@ function onServerReady() {
|
|
|
687
486
|
// Then run it every 30 seconds (constant automatic importing)
|
|
688
487
|
setInterval(performAutoImport, 30000);
|
|
689
488
|
|
|
690
|
-
// Start conversation sync to watch for changes
|
|
691
|
-
try {
|
|
692
|
-
const sync = getConversationSync();
|
|
693
|
-
sync.start();
|
|
694
|
-
} catch (err) {
|
|
695
|
-
console.error('[SERVER] Error starting conversation sync:', err.message);
|
|
696
|
-
}
|
|
697
489
|
}
|
|
698
490
|
|
|
699
491
|
function performAutoImport() {
|