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 +4 -4
- package/lib/claude-runner.js +36 -26
- package/package.json +1 -1
- package/server.js +78 -39
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(
|
|
339
|
-
return stmt.all(
|
|
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(
|
|
345
|
-
const result = stmt.run(
|
|
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
|
|
package/lib/claude-runner.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
390
|
-
const { execSync } = require('child_process');
|
|
396
|
+
const { spawnSync } = require('child_process');
|
|
391
397
|
return this.list().filter(agent => {
|
|
392
398
|
try {
|
|
393
|
-
|
|
394
|
-
return
|
|
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
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 (
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
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
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
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
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
conversationId,
|
|
1096
|
-
|
|
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
|
}
|