agentgui 1.0.148 → 1.0.150

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/database.js CHANGED
@@ -335,14 +335,14 @@ export const queries = {
335
335
 
336
336
  getSessionsProcessingLongerThan(minutes) {
337
337
  const cutoff = Date.now() - (minutes * 60 * 1000);
338
- const stmt = db.prepare('SELECT * FROM sessions WHERE status = ? AND started_at < ?');
339
- return stmt.all('pending', cutoff);
338
+ const stmt = db.prepare("SELECT * FROM sessions WHERE status IN ('active', 'pending') AND started_at < ?");
339
+ return stmt.all(cutoff);
340
340
  },
341
341
 
342
342
  cleanupOrphanedSessions(days) {
343
343
  const cutoff = Date.now() - (days * 24 * 60 * 60 * 1000);
344
- const stmt = db.prepare('DELETE FROM sessions WHERE status = ? AND started_at < ?');
345
- const result = stmt.run('pending', cutoff);
344
+ const stmt = db.prepare("DELETE FROM sessions WHERE status IN ('active', 'pending') AND started_at < ?");
345
+ const result = stmt.run(cutoff);
346
346
  return result.changes || 0;
347
347
  },
348
348
 
@@ -139,7 +139,24 @@ class AgentRunner {
139
139
  });
140
140
  }
141
141
 
142
- async runACP(prompt, cwd, config = {}) {
142
+ async runACP(prompt, cwd, config = {}, _retryCount = 0) {
143
+ const maxRetries = config.maxRetries ?? 1;
144
+ try {
145
+ return await this._runACPOnce(prompt, cwd, config);
146
+ } catch (err) {
147
+ const isEmptyExit = err.message && err.message.includes('ACP exited with code');
148
+ const isBinaryError = err.code === 'ENOENT' || (err.message && err.message.includes('ENOENT'));
149
+ if ((isEmptyExit || isBinaryError) && _retryCount < maxRetries) {
150
+ const delay = Math.min(1000 * Math.pow(2, _retryCount), 5000);
151
+ console.error(`[${this.id}] ACP attempt ${_retryCount + 1} failed: ${err.message}. Retrying in ${delay}ms...`);
152
+ await new Promise(r => setTimeout(r, delay));
153
+ return this.runACP(prompt, cwd, config, _retryCount + 1);
154
+ }
155
+ throw err;
156
+ }
157
+ }
158
+
159
+ async _runACPOnce(prompt, cwd, config = {}) {
143
160
  return new Promise((resolve, reject) => {
144
161
  const {
145
162
  timeout = 300000,
@@ -147,11 +164,10 @@ class AgentRunner {
147
164
  onError = null
148
165
  } = config;
149
166
 
150
- // Use adapter if required (e.g., for Claude Code via Zed adapter)
151
167
  const cmd = this.requiresAdapter && this.adapterCommand ? this.adapterCommand : this.command;
152
168
  const baseArgs = this.requiresAdapter && this.adapterCommand ? this.adapterArgs : ['acp'];
153
169
  const args = [...baseArgs];
154
-
170
+
155
171
  const proc = spawn(cmd, args, { cwd });
156
172
 
157
173
  if (config.onPid) {
@@ -163,6 +179,7 @@ class AgentRunner {
163
179
  let sessionId = null;
164
180
  let requestId = 0;
165
181
  let initialized = false;
182
+ let stderrText = '';
166
183
 
167
184
  const timeoutHandle = setTimeout(() => {
168
185
  timedOut = true;
@@ -170,12 +187,9 @@ class AgentRunner {
170
187
  reject(new Error(`${this.name} ACP timeout after ${timeout}ms`));
171
188
  }, timeout);
172
189
 
173
- // ACP protocol handler
174
190
  const handleMessage = (message) => {
175
- // Normalize ACP message to common format
176
191
  const normalized = this.protocolHandler(message, { sessionId, initialized });
177
192
  if (!normalized) {
178
- // Check for initialization response
179
193
  if (message.id === 1 && message.result) {
180
194
  initialized = true;
181
195
  }
@@ -217,13 +231,13 @@ class AgentRunner {
217
231
 
218
232
  proc.stderr.on('data', (chunk) => {
219
233
  const errorText = chunk.toString();
234
+ stderrText += errorText;
220
235
  console.error(`[${this.id}] stderr:`, errorText);
221
236
  if (onError) {
222
237
  try { onError(errorText); } catch (e) {}
223
238
  }
224
239
  });
225
240
 
226
- // Send ACP initialize request (protocolVersion must be an integer per ACP spec)
227
241
  const initRequest = {
228
242
  jsonrpc: '2.0',
229
243
  id: ++requestId,
@@ -245,12 +259,10 @@ class AgentRunner {
245
259
 
246
260
  let sessionCreated = false;
247
261
 
248
- // Wait for initialization then create session and send prompt
249
262
  const checkInitAndSend = () => {
250
263
  if (initialized && !sessionCreated) {
251
264
  sessionCreated = true;
252
-
253
- // Step 1: Create a new session
265
+
254
266
  const sessionRequest = {
255
267
  jsonrpc: '2.0',
256
268
  id: ++requestId,
@@ -269,14 +281,11 @@ class AgentRunner {
269
281
  let promptId = null;
270
282
  let completed = false;
271
283
 
272
- // Handle session creation response and send prompt
273
284
  const originalHandler = handleMessage;
274
285
  const enhancedHandler = (message) => {
275
- // Check for session/new response
276
286
  if (message.id && message.result && message.result.sessionId) {
277
287
  sessionId = message.result.sessionId;
278
-
279
- // Step 2: Send the prompt
288
+
280
289
  promptId = ++requestId;
281
290
  const promptRequest = {
282
291
  jsonrpc: '2.0',
@@ -290,8 +299,7 @@ class AgentRunner {
290
299
  proc.stdin.write(JSON.stringify(promptRequest) + '\n');
291
300
  return;
292
301
  }
293
-
294
- // Check for prompt response (end of turn)
302
+
295
303
  if (message.id === promptId && message.result && message.result.stopReason) {
296
304
  completed = true;
297
305
  clearTimeout(timeoutHandle);
@@ -299,11 +307,10 @@ class AgentRunner {
299
307
  resolve({ outputs, sessionId });
300
308
  return;
301
309
  }
302
-
310
+
303
311
  originalHandler(message);
304
312
  };
305
313
 
306
- // Override the message handler
307
314
  buffer = '';
308
315
  proc.stdout.removeAllListeners('data');
309
316
  proc.stdout.on('data', (chunk) => {
@@ -317,12 +324,11 @@ class AgentRunner {
317
324
  if (line.trim()) {
318
325
  try {
319
326
  const message = JSON.parse(line);
320
-
321
- // Check for initialization response
327
+
322
328
  if (message.id === 1 && message.result) {
323
329
  initialized = true;
324
330
  }
325
-
331
+
326
332
  enhancedHandler(message);
327
333
  } catch (e) {
328
334
  console.error(`[${this.id}] JSON parse error:`, line.substring(0, 100));
@@ -340,7 +346,8 @@ class AgentRunner {
340
346
  if (code === 0 || outputs.length > 0) {
341
347
  resolve({ outputs, sessionId });
342
348
  } else {
343
- reject(new Error(`${this.name} ACP exited with code ${code}`));
349
+ const detail = stderrText ? `: ${stderrText.substring(0, 200)}` : '';
350
+ reject(new Error(`${this.name} ACP exited with code ${code}${detail}`));
344
351
  }
345
352
  });
346
353
 
@@ -386,12 +393,15 @@ class AgentRegistry {
386
393
  }
387
394
 
388
395
  listACPAvailable() {
389
- // Check which agents are actually installed
390
- const { execSync } = require('child_process');
396
+ const { spawnSync } = require('child_process');
391
397
  return this.list().filter(agent => {
392
398
  try {
393
- execSync(`which ${agent.command} 2>/dev/null`, { encoding: 'utf-8' });
394
- return true;
399
+ const which = spawnSync('which', [agent.command], { encoding: 'utf-8', timeout: 3000 });
400
+ if (which.status !== 0) return false;
401
+ const binPath = (which.stdout || '').trim();
402
+ if (!binPath) return false;
403
+ const check = spawnSync(binPath, ['--version'], { encoding: 'utf-8', timeout: 10000 });
404
+ return check.status === 0 && (check.stdout || '').trim().length > 0;
395
405
  } catch {
396
406
  return false;
397
407
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.148",
3
+ "version": "1.0.150",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -23,6 +23,9 @@ const SYSTEM_PROMPT = `Write all responses as clean semantic HTML. Use tags like
23
23
 
24
24
  const activeExecutions = new Map();
25
25
  const messageQueues = new Map();
26
+ const STUCK_AGENT_THRESHOLD_MS = 600000;
27
+ const NO_PID_GRACE_PERIOD_MS = 60000;
28
+ const STALE_SESSION_MIN_AGE_MS = 30000;
26
29
 
27
30
  const debugLog = (msg) => {
28
31
  const timestamp = new Date().toISOString();
@@ -648,7 +651,7 @@ function persistChunkWithRetry(sessionId, conversationId, sequence, blockType, b
648
651
 
649
652
  async function processMessageWithStreaming(conversationId, messageId, sessionId, content, agentId) {
650
653
  const startTime = Date.now();
651
- activeExecutions.set(conversationId, { pid: null, startTime, sessionId });
654
+ activeExecutions.set(conversationId, { pid: null, startTime, sessionId, lastActivity: startTime });
652
655
  queries.setIsStreaming(conversationId, true);
653
656
  queries.updateSession(sessionId, { status: 'active' });
654
657
 
@@ -665,6 +668,8 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
665
668
 
666
669
  const onEvent = (parsed) => {
667
670
  eventCount++;
671
+ const entry = activeExecutions.get(conversationId);
672
+ if (entry) entry.lastActivity = Date.now();
668
673
  debugLog(`[stream] Event ${eventCount}: type=${parsed.type}`);
669
674
 
670
675
  if (parsed.type === 'system') {
@@ -1045,25 +1050,27 @@ server.on('error', (err) => {
1045
1050
  function recoverStaleSessions() {
1046
1051
  try {
1047
1052
  const staleSessions = queries.getActiveSessions ? queries.getActiveSessions() : [];
1053
+ const now = Date.now();
1048
1054
  let recoveredCount = 0;
1049
1055
  for (const session of staleSessions) {
1050
- if (!activeExecutions.has(session.conversationId)) {
1051
- queries.updateSession(session.id, {
1052
- status: 'error',
1053
- error: 'Agent died unexpectedly (server restart)',
1054
- completed_at: Date.now()
1055
- });
1056
- queries.setIsStreaming(session.conversationId, false);
1057
- broadcastSync({
1058
- type: 'streaming_error',
1059
- sessionId: session.id,
1060
- conversationId: session.conversationId,
1061
- error: 'Agent died unexpectedly (server restart)',
1062
- recoverable: false,
1063
- timestamp: Date.now()
1064
- });
1065
- recoveredCount++;
1066
- }
1056
+ if (activeExecutions.has(session.conversationId)) continue;
1057
+ const sessionAge = now - session.started_at;
1058
+ if (sessionAge < STALE_SESSION_MIN_AGE_MS) continue;
1059
+ queries.updateSession(session.id, {
1060
+ status: 'error',
1061
+ error: 'Agent died unexpectedly (server restart)',
1062
+ completed_at: now
1063
+ });
1064
+ queries.setIsStreaming(session.conversationId, false);
1065
+ broadcastSync({
1066
+ type: 'streaming_error',
1067
+ sessionId: session.id,
1068
+ conversationId: session.conversationId,
1069
+ error: 'Agent died unexpectedly (server restart)',
1070
+ recoverable: false,
1071
+ timestamp: now
1072
+ });
1073
+ recoveredCount++;
1067
1074
  }
1068
1075
  if (recoveredCount > 0) {
1069
1076
  console.log(`[RECOVERY] Recovered ${recoveredCount} stale active session(s)`);
@@ -1073,31 +1080,63 @@ function recoverStaleSessions() {
1073
1080
  }
1074
1081
  }
1075
1082
 
1083
+ function isProcessAlive(pid) {
1084
+ try {
1085
+ process.kill(pid, 0);
1086
+ return true;
1087
+ } catch (err) {
1088
+ if (err.code === 'EPERM') return true;
1089
+ return false;
1090
+ }
1091
+ }
1092
+
1093
+ function markAgentDead(conversationId, entry, reason) {
1094
+ if (!activeExecutions.has(conversationId)) return;
1095
+ activeExecutions.delete(conversationId);
1096
+ queries.setIsStreaming(conversationId, false);
1097
+ if (entry.sessionId) {
1098
+ queries.updateSession(entry.sessionId, {
1099
+ status: 'error',
1100
+ error: reason,
1101
+ completed_at: Date.now()
1102
+ });
1103
+ }
1104
+ broadcastSync({
1105
+ type: 'streaming_error',
1106
+ sessionId: entry.sessionId,
1107
+ conversationId,
1108
+ error: reason,
1109
+ recoverable: false,
1110
+ timestamp: Date.now()
1111
+ });
1112
+ drainMessageQueue(conversationId);
1113
+ }
1114
+
1076
1115
  function performAgentHealthCheck() {
1116
+ const now = Date.now();
1077
1117
  for (const [conversationId, entry] of activeExecutions) {
1078
- if (!entry || !entry.pid) continue;
1079
- try {
1080
- process.kill(entry.pid, 0);
1081
- } catch (err) {
1082
- debugLog(`[HEALTH] Agent PID ${entry.pid} for conv ${conversationId} is dead`);
1083
- activeExecutions.delete(conversationId);
1084
- queries.setIsStreaming(conversationId, false);
1085
- if (entry.sessionId) {
1086
- queries.updateSession(entry.sessionId, {
1087
- status: 'error',
1088
- error: 'Agent process died unexpectedly',
1089
- completed_at: Date.now()
1118
+ if (!entry) continue;
1119
+
1120
+ if (entry.pid) {
1121
+ if (!isProcessAlive(entry.pid)) {
1122
+ debugLog(`[HEALTH] Agent PID ${entry.pid} for conv ${conversationId} is dead`);
1123
+ markAgentDead(conversationId, entry, 'Agent process died unexpectedly');
1124
+ } else if (now - entry.lastActivity > STUCK_AGENT_THRESHOLD_MS) {
1125
+ debugLog(`[HEALTH] Agent PID ${entry.pid} for conv ${conversationId} has no activity for ${Math.round((now - entry.lastActivity) / 1000)}s`);
1126
+ broadcastSync({
1127
+ type: 'streaming_error',
1128
+ sessionId: entry.sessionId,
1129
+ conversationId,
1130
+ error: 'Agent may be stuck (no activity for 10 minutes)',
1131
+ recoverable: true,
1132
+ timestamp: now
1090
1133
  });
1091
1134
  }
1092
- broadcastSync({
1093
- type: 'streaming_error',
1094
- sessionId: entry.sessionId,
1095
- conversationId,
1096
- error: 'Agent process died unexpectedly',
1097
- recoverable: false,
1098
- timestamp: Date.now()
1099
- });
1100
- drainMessageQueue(conversationId);
1135
+ } else {
1136
+ if (now - entry.startTime > NO_PID_GRACE_PERIOD_MS) {
1137
+ debugLog(`[HEALTH] Agent for conv ${conversationId} never reported PID after ${Math.round((now - entry.startTime) / 1000)}s`);
1138
+ markAgentDead(conversationId, entry, 'Agent failed to start (no PID reported)');
1139
+ }
1101
1140
  }
1102
1141
  }
1103
1142
  }