agentgui 1.0.180 → 1.0.182
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 +7 -0
- package/lib/pocket-sidecar.js +38 -7
- package/lib/speech.js +5 -2
- package/package.json +1 -1
- package/server.js +92 -2
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/pocket-sidecar.js
CHANGED
|
@@ -6,7 +6,6 @@ import { fileURLToPath } from 'url';
|
|
|
6
6
|
import http from 'http';
|
|
7
7
|
|
|
8
8
|
const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
9
|
-
const POCKET_BIN = path.join(ROOT, 'data', 'pocket-venv', 'bin', 'pocket-tts');
|
|
10
9
|
const PORT = 8787;
|
|
11
10
|
|
|
12
11
|
const FALLBACK_VOICE = 'alba';
|
|
@@ -15,11 +14,21 @@ const state = {
|
|
|
15
14
|
restartCount: 0, failureCount: 0, lastError: null,
|
|
16
15
|
healthy: false, voicePath: null, starting: false,
|
|
17
16
|
shutdownRequested: false, healthTimer: null, restartTimer: null,
|
|
18
|
-
voiceCloning: false,
|
|
17
|
+
voiceCloning: false, adopted: false,
|
|
19
18
|
};
|
|
20
19
|
globalThis.__pocketSidecar = state;
|
|
21
20
|
|
|
22
|
-
function
|
|
21
|
+
function findBinary() {
|
|
22
|
+
const candidates = [
|
|
23
|
+
path.join(ROOT, 'data', 'pocket-venv', 'bin', 'pocket-tts'),
|
|
24
|
+
'/config/workspace/agentgui/data/pocket-venv/bin/pocket-tts',
|
|
25
|
+
path.join(os.homedir(), '.gmgui', 'pocket-venv', 'bin', 'pocket-tts'),
|
|
26
|
+
];
|
|
27
|
+
for (const p of candidates) if (fs.existsSync(p)) return p;
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isInstalled() { return !!findBinary(); }
|
|
23
32
|
|
|
24
33
|
function findVoiceFile(voiceId) {
|
|
25
34
|
if (!voiceId || voiceId === 'default') return null;
|
|
@@ -54,21 +63,24 @@ function killProcess() {
|
|
|
54
63
|
|
|
55
64
|
function scheduleRestart() {
|
|
56
65
|
if (state.shutdownRequested) return;
|
|
57
|
-
killProcess();
|
|
66
|
+
if (!state.adopted) killProcess();
|
|
58
67
|
const delay = Math.min(1000 * Math.pow(2, state.restartCount), 30000);
|
|
59
68
|
state.restartCount++;
|
|
60
69
|
console.log(`[POCKET-TTS] Restart in ${delay}ms (attempt ${state.restartCount})`);
|
|
61
70
|
state.restartTimer = setTimeout(() => {
|
|
62
71
|
state.restartTimer = null;
|
|
72
|
+
state.adopted = false;
|
|
63
73
|
start(state.voicePath).catch(e => console.error('[POCKET-TTS] Restart failed:', e.message));
|
|
64
74
|
}, delay);
|
|
65
75
|
}
|
|
66
76
|
|
|
67
77
|
function spawnSidecar(voice) {
|
|
78
|
+
const bin = findBinary();
|
|
79
|
+
if (!bin) throw new Error('pocket-tts binary not found');
|
|
68
80
|
const args = ['serve', '--host', '0.0.0.0', '--port', String(PORT)];
|
|
69
81
|
if (voice) args.push('--voice', voice);
|
|
70
|
-
console.log('[POCKET-TTS] Starting:',
|
|
71
|
-
return spawn(
|
|
82
|
+
console.log('[POCKET-TTS] Starting:', bin, args.join(' '));
|
|
83
|
+
return spawn(bin, args, {
|
|
72
84
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
73
85
|
env: { ...process.env, PYTHONUNBUFFERED: '1' },
|
|
74
86
|
});
|
|
@@ -92,10 +104,29 @@ async function waitForReady(proc, timeoutSec) {
|
|
|
92
104
|
return false;
|
|
93
105
|
}
|
|
94
106
|
|
|
107
|
+
async function adoptRunning() {
|
|
108
|
+
if (await healthCheck()) {
|
|
109
|
+
state.status = 'running'; state.healthy = true; state.adopted = true;
|
|
110
|
+
state.restartCount = 0; state.failureCount = 0; state.lastError = null;
|
|
111
|
+
if (!state.healthTimer) state.healthTimer = setInterval(async () => {
|
|
112
|
+
if (state.status !== 'running') return;
|
|
113
|
+
const ok = await healthCheck();
|
|
114
|
+
if (!ok && !state.shutdownRequested) {
|
|
115
|
+
state.failureCount++;
|
|
116
|
+
if (state.failureCount >= 3) { state.adopted = false; scheduleRestart(); }
|
|
117
|
+
} else if (ok) state.failureCount = 0;
|
|
118
|
+
}, 10000);
|
|
119
|
+
console.log('[POCKET-TTS] Adopted existing instance on port', PORT);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
95
125
|
async function start(voicePath) {
|
|
96
|
-
if (!isInstalled()) { state.lastError = 'not installed'; state.status = 'unavailable'; return false; }
|
|
97
126
|
if (state.starting) return false;
|
|
98
127
|
if (state.status === 'running' && state.healthy) return true;
|
|
128
|
+
if (await adoptRunning()) return true;
|
|
129
|
+
if (!isInstalled()) { state.lastError = 'not installed'; state.status = 'unavailable'; return false; }
|
|
99
130
|
state.starting = true; state.shutdownRequested = false;
|
|
100
131
|
const requestedVoice = voicePath || state.voicePath;
|
|
101
132
|
try {
|
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;
|
|
@@ -159,10 +161,10 @@ async function decodeAudioFile(filePath) {
|
|
|
159
161
|
|
|
160
162
|
async function getSTT() {
|
|
161
163
|
if (sttPipeline) return sttPipeline;
|
|
162
|
-
if (sttLoadError) throw sttLoadError;
|
|
164
|
+
if (sttLoadError && (Date.now() - sttLoadErrorTime < STT_RETRY_MS)) throw sttLoadError;
|
|
163
165
|
if (sttLoading) {
|
|
164
166
|
while (sttLoading) await new Promise(r => setTimeout(r, 100));
|
|
165
|
-
if (sttLoadError) throw sttLoadError;
|
|
167
|
+
if (sttLoadError && (Date.now() - sttLoadErrorTime < STT_RETRY_MS)) throw sttLoadError;
|
|
166
168
|
if (!sttPipeline) throw new Error('STT pipeline failed to load');
|
|
167
169
|
return sttPipeline;
|
|
168
170
|
}
|
|
@@ -183,6 +185,7 @@ async function getSTT() {
|
|
|
183
185
|
} catch (err) {
|
|
184
186
|
sttPipeline = null;
|
|
185
187
|
sttLoadError = new Error('STT model load failed: ' + err.message);
|
|
188
|
+
sttLoadErrorTime = Date.now();
|
|
186
189
|
throw sttLoadError;
|
|
187
190
|
} finally {
|
|
188
191
|
sttLoading = false;
|
package/package.json
CHANGED
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 (
|
|
1324
|
-
console.log(`[RECOVERY] Marked ${
|
|
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();
|