agentgui 1.0.181 → 1.0.183

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
@@ -354,6 +354,13 @@ export const queries = {
354
354
  return stmt.all();
355
355
  },
356
356
 
357
+ getResumableConversations() {
358
+ const stmt = prep(
359
+ "SELECT id, title, claudeSessionId, agentType, workingDirectory FROM conversations WHERE isStreaming = 1 AND claudeSessionId IS NOT NULL AND claudeSessionId != ''"
360
+ );
361
+ return stmt.all();
362
+ },
363
+
357
364
  clearAllStreamingFlags() {
358
365
  const stmt = prep('UPDATE conversations SET isStreaming = 0 WHERE isStreaming = 1');
359
366
  return stmt.run().changes;
package/lib/speech.js CHANGED
@@ -105,6 +105,8 @@ let transformersModule = null;
105
105
  let sttPipeline = null;
106
106
  let sttLoading = false;
107
107
  let sttLoadError = null;
108
+ let sttLoadErrorTime = 0;
109
+ const STT_RETRY_MS = 30000;
108
110
  const SAMPLE_RATE_STT = 16000;
109
111
 
110
112
  const TTS_CACHE_MAX_BYTES = 10 * 1024 * 1024;
@@ -118,12 +120,16 @@ async function loadTransformers() {
118
120
  return transformersModule;
119
121
  }
120
122
 
123
+ const PERSISTENT_CACHE = path.join(os.homedir(), '.gmgui', 'models');
124
+
121
125
  function whisperModelPath() {
122
126
  try {
123
127
  const webtalkDir = path.dirname(require.resolve('webtalk'));
124
128
  const p = path.join(webtalkDir, 'models', 'onnx-community', 'whisper-base');
125
129
  if (fs.existsSync(p)) return p;
126
130
  } catch (_) {}
131
+ const cached = path.join(PERSISTENT_CACHE, 'onnx-community', 'whisper-base');
132
+ if (fs.existsSync(cached)) return cached;
127
133
  return 'onnx-community/whisper-base';
128
134
  }
129
135
 
@@ -159,10 +165,10 @@ async function decodeAudioFile(filePath) {
159
165
 
160
166
  async function getSTT() {
161
167
  if (sttPipeline) return sttPipeline;
162
- if (sttLoadError) throw sttLoadError;
168
+ if (sttLoadError && (Date.now() - sttLoadErrorTime < STT_RETRY_MS)) throw sttLoadError;
163
169
  if (sttLoading) {
164
170
  while (sttLoading) await new Promise(r => setTimeout(r, 100));
165
- if (sttLoadError) throw sttLoadError;
171
+ if (sttLoadError && (Date.now() - sttLoadErrorTime < STT_RETRY_MS)) throw sttLoadError;
166
172
  if (!sttPipeline) throw new Error('STT pipeline failed to load');
167
173
  return sttPipeline;
168
174
  }
@@ -173,9 +179,11 @@ async function getSTT() {
173
179
  const isLocal = !modelPath.includes('/') || fs.existsSync(modelPath);
174
180
  env.allowLocalModels = true;
175
181
  env.allowRemoteModels = !isLocal;
182
+ env.cacheDir = PERSISTENT_CACHE;
176
183
  if (isLocal) env.localModelPath = '';
177
184
  sttPipeline = await pipeline('automatic-speech-recognition', modelPath, {
178
185
  device: 'cpu',
186
+ cache_dir: PERSISTENT_CACHE,
179
187
  local_files_only: isLocal,
180
188
  });
181
189
  sttLoadError = null;
@@ -183,6 +191,7 @@ async function getSTT() {
183
191
  } catch (err) {
184
192
  sttPipeline = null;
185
193
  sttLoadError = new Error('STT model load failed: ' + err.message);
194
+ sttLoadErrorTime = Date.now();
186
195
  throw sttLoadError;
187
196
  } finally {
188
197
  sttLoading = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.181",
3
+ "version": "1.0.183",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -1311,34 +1311,121 @@ function recoverStaleSessions() {
1311
1311
  try {
1312
1312
  const now = Date.now();
1313
1313
 
1314
+ const resumable = new Set();
1315
+ const resumableConvs = queries.getResumableConversations ? queries.getResumableConversations() : [];
1316
+ for (const conv of resumableConvs) {
1317
+ if (conv.agentType === 'claude-code') {
1318
+ resumable.add(conv.id);
1319
+ }
1320
+ }
1321
+
1314
1322
  const staleSessions = queries.getActiveSessions ? queries.getActiveSessions() : [];
1323
+ let markedCount = 0;
1315
1324
  for (const session of staleSessions) {
1316
1325
  if (activeExecutions.has(session.conversationId)) continue;
1326
+ if (resumable.has(session.conversationId)) continue;
1317
1327
  queries.updateSession(session.id, {
1318
1328
  status: 'error',
1319
1329
  error: 'Server restarted',
1320
1330
  completed_at: now
1321
1331
  });
1332
+ markedCount++;
1322
1333
  }
1323
- if (staleSessions.length > 0) {
1324
- console.log(`[RECOVERY] Marked ${staleSessions.length} stale session(s) as error`);
1334
+ if (markedCount > 0) {
1335
+ console.log(`[RECOVERY] Marked ${markedCount} stale session(s) as error`);
1325
1336
  }
1326
1337
 
1327
1338
  const streamingConvs = queries.getStreamingConversations ? queries.getStreamingConversations() : [];
1328
1339
  let clearedCount = 0;
1329
1340
  for (const conv of streamingConvs) {
1330
1341
  if (activeExecutions.has(conv.id)) continue;
1342
+ if (resumable.has(conv.id)) continue;
1331
1343
  queries.setIsStreaming(conv.id, false);
1332
1344
  clearedCount++;
1333
1345
  }
1334
1346
  if (clearedCount > 0) {
1335
1347
  console.log(`[RECOVERY] Cleared isStreaming flag on ${clearedCount} stale conversation(s)`);
1336
1348
  }
1349
+ if (resumable.size > 0) {
1350
+ console.log(`[RECOVERY] Found ${resumable.size} resumable conversation(s)`);
1351
+ }
1337
1352
  } catch (err) {
1338
1353
  console.error('[RECOVERY] Stale session recovery error:', err.message);
1339
1354
  }
1340
1355
  }
1341
1356
 
1357
+ async function resumeInterruptedStreams() {
1358
+ try {
1359
+ const resumableConvs = queries.getResumableConversations ? queries.getResumableConversations() : [];
1360
+ const toResume = resumableConvs.filter(c => c.agentType === 'claude-code');
1361
+
1362
+ if (toResume.length === 0) return;
1363
+
1364
+ console.log(`[RESUME] Resuming ${toResume.length} interrupted conversation(s)`);
1365
+
1366
+ for (let i = 0; i < toResume.length; i++) {
1367
+ const conv = toResume[i];
1368
+ try {
1369
+ const staleSessions = [...queries.getSessionsByStatus(conv.id, 'active'), ...queries.getSessionsByStatus(conv.id, 'pending')];
1370
+ for (const s of staleSessions) {
1371
+ queries.updateSession(s.id, { status: 'interrupted', error: 'Server restarted, resuming', completed_at: Date.now() });
1372
+ }
1373
+
1374
+ const lastMsg = queries.getLastUserMessage(conv.id);
1375
+ const prompt = lastMsg?.content || 'continue';
1376
+ const promptText = typeof prompt === 'string' ? prompt : JSON.stringify(prompt);
1377
+
1378
+ const session = queries.createSession(conv.id);
1379
+ queries.createEvent('session.created', {
1380
+ sessionId: session.id,
1381
+ resumeReason: 'server_restart',
1382
+ claudeSessionId: conv.claudeSessionId
1383
+ }, conv.id, session.id);
1384
+
1385
+ activeExecutions.set(conv.id, {
1386
+ pid: null,
1387
+ startTime: Date.now(),
1388
+ sessionId: session.id,
1389
+ lastActivity: Date.now()
1390
+ });
1391
+
1392
+ broadcastSync({
1393
+ type: 'streaming_start',
1394
+ sessionId: session.id,
1395
+ conversationId: conv.id,
1396
+ agentId: conv.agentType,
1397
+ resumed: true,
1398
+ timestamp: Date.now()
1399
+ });
1400
+
1401
+ const messageId = lastMsg?.id || null;
1402
+ console.log(`[RESUME] Resuming conv ${conv.id} (claude session: ${conv.claudeSessionId})`);
1403
+
1404
+ processMessageWithStreaming(conv.id, messageId, session.id, promptText, conv.agentType)
1405
+ .catch(err => debugLog(`[RESUME] Error resuming conv ${conv.id}: ${err.message}`));
1406
+
1407
+ if (i < toResume.length - 1) {
1408
+ await new Promise(r => setTimeout(r, 200));
1409
+ }
1410
+ } catch (err) {
1411
+ console.error(`[RESUME] Failed to resume conv ${conv.id}: ${err.message}`);
1412
+ queries.setIsStreaming(conv.id, false);
1413
+ const activeSessions = queries.getSessionsByStatus(conv.id, 'active');
1414
+ const pendingSessions = queries.getSessionsByStatus(conv.id, 'pending');
1415
+ for (const s of [...activeSessions, ...pendingSessions]) {
1416
+ queries.updateSession(s.id, {
1417
+ status: 'error',
1418
+ error: 'Resume failed: ' + err.message,
1419
+ completed_at: Date.now()
1420
+ });
1421
+ }
1422
+ }
1423
+ }
1424
+ } catch (err) {
1425
+ console.error('[RESUME] Error during stream resumption:', err.message);
1426
+ }
1427
+ }
1428
+
1342
1429
  function isProcessAlive(pid) {
1343
1430
  try {
1344
1431
  process.kill(pid, 0);
@@ -1414,6 +1501,9 @@ function onServerReady() {
1414
1501
  // Recover stale active sessions from previous run
1415
1502
  recoverStaleSessions();
1416
1503
 
1504
+ // Resume interrupted streams after recovery
1505
+ resumeInterruptedStreams().catch(err => console.error('[RESUME] Startup error:', err.message));
1506
+
1417
1507
  getSpeech().then(s => s.preloadTTS()).catch(e => debugLog('[TTS] Preload failed: ' + e.message));
1418
1508
 
1419
1509
  performAutoImport();