agentgui 1.0.844 → 1.0.846
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/CHANGELOG.md +9 -0
- package/lib/http-handler.js +133 -0
- package/lib/server-startup.js +116 -0
- package/lib/server-startup2.js +83 -0
- package/lib/ws-legacy-handlers.js +154 -0
- package/lib/ws-setup.js +77 -0
- package/package.json +1 -1
- package/server.js +58 -799
package/server.js
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import os from 'os';
|
|
5
4
|
import { fileURLToPath } from 'url';
|
|
6
|
-
import { WebSocketServer } from 'ws';
|
|
7
|
-
import { execSync, spawn } from 'child_process';
|
|
8
5
|
import { LRUCache } from 'lru-cache';
|
|
9
|
-
import { createRequire } from 'module';
|
|
10
|
-
import crypto from 'crypto';
|
|
11
6
|
const PKG_VERSION = JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version;
|
|
12
7
|
import { createExpressApp } from './lib/routes-upload.js';
|
|
13
8
|
import { queries } from './database.js';
|
|
@@ -35,6 +30,10 @@ import { WSOptimizer } from './lib/ws-optimizer.js';
|
|
|
35
30
|
import { WsRouter } from './lib/ws-protocol.js';
|
|
36
31
|
import { encode as wsEncode } from './lib/codec.js';
|
|
37
32
|
import { parseBody, acceptsEncoding, compressAndSend, sendJSON } from './lib/http-utils.js';
|
|
33
|
+
import { createWsSetup } from './lib/ws-setup.js';
|
|
34
|
+
import { createHttpHandler } from './lib/http-handler.js';
|
|
35
|
+
import { createOnServerReady } from './lib/server-startup.js';
|
|
36
|
+
import { createAutoImport, createDbRecovery, createPluginLoader } from './lib/server-startup2.js';
|
|
38
37
|
const sendWs = (ws, obj) => { if (ws.readyState === 1) ws.send(wsEncode(obj)); };
|
|
39
38
|
import { register as registerConvHandlers } from './lib/ws-handlers-conv.js';
|
|
40
39
|
import { register as registerConvHandlers2 } from './lib/ws-handlers-conv2.js';
|
|
@@ -54,7 +53,6 @@ import { installGMAgentConfigs } from './lib/gm-agent-configs.js';
|
|
|
54
53
|
import * as toolManager from './lib/tool-manager.js';
|
|
55
54
|
import { pm2Manager } from './lib/pm2-manager.js';
|
|
56
55
|
import CheckpointManager from './lib/checkpoint-manager.js';
|
|
57
|
-
import { JsonlWatcher } from './lib/jsonl-watcher.js';
|
|
58
56
|
import { createBroadcast } from './lib/broadcast.js';
|
|
59
57
|
import { createRecovery } from './lib/recovery.js';
|
|
60
58
|
import { parseRateLimitResetTime } from './lib/process-message-rate-limit.js';
|
|
@@ -75,7 +73,6 @@ process.on('unhandledRejection', (reason, promise) => {
|
|
|
75
73
|
if (reason instanceof Error) console.error(reason.stack);
|
|
76
74
|
});
|
|
77
75
|
|
|
78
|
-
// Signal handlers registered after server initialization (see bottom of file)
|
|
79
76
|
process.on('SIGHUP', () => { console.log('[SIGNAL] SIGHUP received (ignored - uncrashable)'); });
|
|
80
77
|
process.on('beforeExit', (code) => { console.log('[PROCESS] beforeExit with code:', code); });
|
|
81
78
|
process.on('exit', (code) => { console.log('[PROCESS] exit with code:', code); });
|
|
@@ -126,271 +123,34 @@ const getModelsForAgent = makeGetModelsForAgent({ modelCache, discoveredAgents,
|
|
|
126
123
|
const _rateLimitMap = new LRUCache({ max: 1000, ttl: 60000 });
|
|
127
124
|
const RATE_LIMIT_MAX = parseInt(process.env.RATE_LIMIT_MAX || '300', 10);
|
|
128
125
|
|
|
129
|
-
const server = http.createServer(async (req, res) => {
|
|
130
|
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
131
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
132
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
133
|
-
if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; }
|
|
134
|
-
if (req.headers.upgrade && req.headers.upgrade.toLowerCase() === 'websocket') return;
|
|
135
|
-
|
|
136
|
-
const clientIp = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.socket.remoteAddress;
|
|
137
|
-
const hits = (_rateLimitMap.get(clientIp) || 0) + 1;
|
|
138
|
-
_rateLimitMap.set(clientIp, hits);
|
|
139
|
-
res.setHeader('X-RateLimit-Limit', RATE_LIMIT_MAX);
|
|
140
|
-
res.setHeader('X-RateLimit-Remaining', Math.max(0, RATE_LIMIT_MAX - hits));
|
|
141
|
-
if (hits > RATE_LIMIT_MAX) {
|
|
142
|
-
res.writeHead(429, { 'Retry-After': '60' });
|
|
143
|
-
res.end('Too Many Requests');
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const _pwd = process.env.PASSWORD;
|
|
148
|
-
if (_pwd) {
|
|
149
|
-
const _auth = req.headers['authorization'] || '';
|
|
150
|
-
let _ok = false;
|
|
151
|
-
if (_auth.startsWith('Basic ')) {
|
|
152
|
-
try {
|
|
153
|
-
const _decoded = Buffer.from(_auth.slice(6), 'base64').toString('utf8');
|
|
154
|
-
const _ci = _decoded.indexOf(':');
|
|
155
|
-
if (_ci !== -1) { const _p = _decoded.slice(_ci + 1); try { _ok = _p.length === _pwd.length && crypto.timingSafeEqual(Buffer.from(_p), Buffer.from(_pwd)); } catch { _ok = false; } }
|
|
156
|
-
} catch (_) {}
|
|
157
|
-
}
|
|
158
|
-
if (!_ok) {
|
|
159
|
-
res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="agentgui"' });
|
|
160
|
-
res.end('Unauthorized');
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const pathOnly = req.url.split('?')[0];
|
|
166
|
-
|
|
167
|
-
// Route file upload and fsbrowse requests through Express sub-app
|
|
168
|
-
if (pathOnly.startsWith(BASE_URL + '/api/upload/') || pathOnly.startsWith(BASE_URL + '/files/')) {
|
|
169
|
-
return expressApp(req, res);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (req.url === '/favicon.ico' || req.url === BASE_URL + '/favicon.ico') {
|
|
173
|
-
const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100" rx="20" fill="#3b82f6"/><text x="50" y="68" font-size="50" font-family="sans-serif" font-weight="bold" fill="white" text-anchor="middle">G</text></svg>';
|
|
174
|
-
res.writeHead(200, { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'public, max-age=86400' });
|
|
175
|
-
res.end(svg);
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (req.url === '/') { res.writeHead(302, { Location: BASE_URL + '/' }); res.end(); return; }
|
|
180
|
-
|
|
181
|
-
// Handle requests with or without BASE_URL prefix (for reverse proxy compatibility)
|
|
182
|
-
let routePath = req.url;
|
|
183
|
-
if (req.url.startsWith(BASE_URL + '/')) {
|
|
184
|
-
routePath = req.url.slice(BASE_URL.length);
|
|
185
|
-
} else if (req.url === BASE_URL) {
|
|
186
|
-
routePath = '/';
|
|
187
|
-
} else if (req.url.startsWith('/api/') || req.url.startsWith('/js/') || req.url.startsWith('/css/') ||
|
|
188
|
-
req.url.startsWith('/vendor/') || req.url.startsWith('/sync') || req.url === '/' ||
|
|
189
|
-
req.url.startsWith('/conversations/')) {
|
|
190
|
-
// Allow requests without BASE_URL prefix for static files and known routes
|
|
191
|
-
// This supports reverse proxies that strip the BASE_URL prefix
|
|
192
|
-
routePath = req.url;
|
|
193
|
-
} else {
|
|
194
|
-
res.writeHead(404); res.end('Not found'); return;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
routePath = routePath || '/';
|
|
198
|
-
|
|
199
|
-
try {
|
|
200
|
-
// Remove query parameters from routePath for matching
|
|
201
|
-
const pathOnly = routePath.split('?')[0];
|
|
202
|
-
|
|
203
|
-
if (pathOnly === '/oauth2callback' && req.method === 'GET') {
|
|
204
|
-
await handleGeminiOAuthCallback(req, res, PORT);
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (pathOnly === '/codex-oauth2callback' && req.method === 'GET') {
|
|
209
|
-
await handleCodexOAuthCallback(req, res, PORT);
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (pathOnly === '/api/health' && req.method === 'GET') {
|
|
214
|
-
let dbStatus = { ok: true };
|
|
215
|
-
try { queries._db.prepare('SELECT 1').get(); } catch (e) { dbStatus = { ok: false, error: e.message }; }
|
|
216
|
-
const queueSizes = {};
|
|
217
|
-
for (const [k, v] of messageQueues) queueSizes[k] = v.length;
|
|
218
|
-
sendJSON(req, res, 200, {
|
|
219
|
-
status: 'ok',
|
|
220
|
-
version: PKG_VERSION,
|
|
221
|
-
uptime: process.uptime(),
|
|
222
|
-
agents: discoveredAgents.length,
|
|
223
|
-
activeExecutions: activeExecutions.size,
|
|
224
|
-
wsClients: wss.clients.size,
|
|
225
|
-
memory: process.memoryUsage(),
|
|
226
|
-
acp: getACPStatus(),
|
|
227
|
-
db: dbStatus,
|
|
228
|
-
queueSizes
|
|
229
|
-
});
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const convHandler = _convRoutes._match(req.method, pathOnly);
|
|
234
|
-
if (convHandler) { await convHandler(req, res); return; }
|
|
235
|
-
|
|
236
|
-
const messagesHandler = _messagesRoutes._match(req.method, pathOnly);
|
|
237
|
-
if (messagesHandler) { await messagesHandler(req, res); return; }
|
|
238
|
-
|
|
239
|
-
const sessionsHandler = _sessionsRoutes._match(req.method, pathOnly);
|
|
240
|
-
if (sessionsHandler) { await sessionsHandler(req, res); return; }
|
|
241
|
-
|
|
242
|
-
const scriptsHandler = _scriptsRoutes._match(req.method, pathOnly);
|
|
243
|
-
if (scriptsHandler) { await scriptsHandler(req, res); return; }
|
|
244
|
-
|
|
245
|
-
const runsHandlerA = _runsRoutes._match(req.method, pathOnly);
|
|
246
|
-
if (runsHandlerA) { await runsHandlerA(req, res); return; }
|
|
247
|
-
|
|
248
|
-
const agentHandler = _agentRoutes._match(req.method, pathOnly);
|
|
249
|
-
if (agentHandler) { await agentHandler(req, res); return; }
|
|
250
|
-
|
|
251
|
-
const oauthHandler = _oauthRoutes._match(req.method, pathOnly);
|
|
252
|
-
if (oauthHandler) { await oauthHandler(req, res); return; }
|
|
253
|
-
|
|
254
|
-
const agentActionsHandler = _agentActionsRoutes._match(req.method, pathOnly);
|
|
255
|
-
if (agentActionsHandler) { await agentActionsHandler(req, res); return; }
|
|
256
|
-
|
|
257
|
-
const authConfigHandler = _authConfigRoutes._match(req.method, pathOnly);
|
|
258
|
-
if (authConfigHandler) { await authConfigHandler(req, res); return; }
|
|
259
|
-
|
|
260
|
-
const speechHandler = _speechRoutes._match(req.method, pathOnly);
|
|
261
|
-
if (speechHandler) { await speechHandler(req, res, pathOnly); return; }
|
|
262
|
-
|
|
263
|
-
const utilHandler = _utilRoutes._match(req.method, pathOnly);
|
|
264
|
-
if (utilHandler) { await utilHandler(req, res); return; }
|
|
265
|
-
|
|
266
|
-
const threadHandler = _threadRoutes._match(req.method, pathOnly);
|
|
267
|
-
if (threadHandler) { await threadHandler(req, res); return; }
|
|
268
|
-
|
|
269
|
-
if (routePath.startsWith('/api/image/')) {
|
|
270
|
-
const imagePath = routePath.slice('/api/image/'.length);
|
|
271
|
-
const decodedPath = decodeURIComponent(imagePath);
|
|
272
|
-
const expandedPath = decodedPath.startsWith('~') ?
|
|
273
|
-
decodedPath.replace('~', os.homedir()) : decodedPath;
|
|
274
|
-
const normalizedPath = path.normalize(expandedPath);
|
|
275
|
-
const isWindows = os.platform() === 'win32';
|
|
276
|
-
const isAbsolute = isWindows ? /^[A-Za-z]:[\\\/]/.test(normalizedPath) : normalizedPath.startsWith('/');
|
|
277
|
-
if (!isAbsolute || normalizedPath.includes('..')) {
|
|
278
|
-
res.writeHead(403); res.end('Forbidden'); return;
|
|
279
|
-
}
|
|
280
|
-
try {
|
|
281
|
-
if (!fs.existsSync(normalizedPath)) { res.writeHead(404); res.end('Not found'); return; }
|
|
282
|
-
const ext = path.extname(normalizedPath).toLowerCase();
|
|
283
|
-
const mimeTypes = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml' };
|
|
284
|
-
const contentType = mimeTypes[ext] || 'application/octet-stream';
|
|
285
|
-
const fileContent = fs.readFileSync(normalizedPath);
|
|
286
|
-
res.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': 'no-cache' });
|
|
287
|
-
res.end(fileContent);
|
|
288
|
-
} catch (err) {
|
|
289
|
-
sendJSON(req, res, 400, { error: err.message });
|
|
290
|
-
}
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Handle conversation detail routes - serve index.html for client-side routing
|
|
295
|
-
if (pathOnly.match(/^\/conversations\/[^\/]+$/)) {
|
|
296
|
-
const indexPath = path.join(staticDir, 'index.html');
|
|
297
|
-
serveFile(indexPath, res, req);
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
let filePath = routePath === '/' ? '/index.html' : routePath;
|
|
302
|
-
filePath = path.join(staticDir, filePath);
|
|
303
|
-
const normalizedPath = path.normalize(filePath);
|
|
304
|
-
if (!normalizedPath.startsWith(staticDir)) { res.writeHead(403); res.end('Forbidden'); return; }
|
|
305
|
-
|
|
306
|
-
fs.stat(filePath, (err, stats) => {
|
|
307
|
-
if (err) { res.writeHead(404); res.end('Not found'); return; }
|
|
308
|
-
if (stats.isDirectory()) {
|
|
309
|
-
filePath = path.join(filePath, 'index.html');
|
|
310
|
-
fs.stat(filePath, (err2) => {
|
|
311
|
-
if (err2) { res.writeHead(404); res.end('Not found'); return; }
|
|
312
|
-
serveFile(filePath, res, req);
|
|
313
|
-
});
|
|
314
|
-
} else {
|
|
315
|
-
serveFile(filePath, res, req);
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
} catch (e) {
|
|
319
|
-
console.error('Server error:', e.message);
|
|
320
|
-
sendJSON(req, res, 500, { error: e.message });
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
|
|
324
126
|
const _assetDeps = { compressAndSend, acceptsEncoding, watch, BASE_URL, PKG_VERSION };
|
|
325
127
|
function serveFile(filePath, res, req) { return _serveFile(filePath, res, req, _assetDeps); }
|
|
326
128
|
|
|
327
|
-
|
|
328
|
-
|
|
129
|
+
const _routes = {};
|
|
130
|
+
const server = http.createServer(createHttpHandler({
|
|
131
|
+
BASE_URL, expressApp, queries, sendJSON, serveFile, staticDir, messageQueues,
|
|
132
|
+
get wss() { return wss; }, activeExecutions, getACPStatus, discoveredAgents,
|
|
133
|
+
PKG_VERSION, RATE_LIMIT_MAX, rateLimitMap: _rateLimitMap,
|
|
134
|
+
get convRoutes() { return _routes.conv; },
|
|
135
|
+
get messagesRoutes() { return _routes.messages; },
|
|
136
|
+
get sessionsRoutes() { return _routes.sessions; },
|
|
137
|
+
get scriptsRoutes() { return _routes.scripts; },
|
|
138
|
+
get runsRoutes() { return _routes.runs; },
|
|
139
|
+
get agentRoutes() { return _routes.agents; },
|
|
140
|
+
get oauthRoutes() { return _routes.oauth; },
|
|
141
|
+
get agentActionsRoutes() { return _routes.agentActions; },
|
|
142
|
+
get authConfigRoutes() { return _routes.authConfig; },
|
|
143
|
+
get speechRoutes() { return _routes.speech; },
|
|
144
|
+
get utilRoutes() { return _routes.util; },
|
|
145
|
+
get threadRoutes() { return _routes.threads; },
|
|
146
|
+
handleGeminiOAuthCallback, handleCodexOAuthCallback, PORT
|
|
147
|
+
}));
|
|
329
148
|
|
|
330
|
-
|
|
331
|
-
const wss = new WebSocketServer({
|
|
332
|
-
server,
|
|
333
|
-
perMessageDeflate: false // Disabled: msgpack binary doesn't compress well, and
|
|
334
|
-
// synchronous zlib on every frame blocks the event loop.
|
|
335
|
-
// HTTP-layer gzip already handles static assets; WS
|
|
336
|
-
// streaming events are small and latency-sensitive.
|
|
337
|
-
});
|
|
338
|
-
wss.on('error', (err) => {
|
|
339
|
-
console.error('[WSS] WebSocket server error (contained):', err.message);
|
|
340
|
-
});
|
|
341
|
-
const hotReloadClients = [];
|
|
149
|
+
let broadcastSeq = 0;
|
|
342
150
|
const syncClients = new Set();
|
|
343
151
|
const subscriptionIndex = new Map();
|
|
344
152
|
const pm2Subscribers = new Set();
|
|
345
153
|
|
|
346
|
-
wss.on('connection', (ws, req) => {
|
|
347
|
-
const _pwd = process.env.PASSWORD;
|
|
348
|
-
if (_pwd) {
|
|
349
|
-
const url = new URL(req.url, 'http://localhost');
|
|
350
|
-
const token = url.searchParams.get('token');
|
|
351
|
-
if (token !== _pwd) { ws.close(4001, 'Unauthorized'); return; }
|
|
352
|
-
}
|
|
353
|
-
const wsPath = req.url.split('?')[0];
|
|
354
|
-
const wsRoute = wsPath.startsWith(BASE_URL) ? wsPath.slice(BASE_URL.length) : wsPath;
|
|
355
|
-
if (wsRoute === '/hot-reload') {
|
|
356
|
-
hotReloadClients.push(ws);
|
|
357
|
-
ws.on('close', () => { const i = hotReloadClients.indexOf(ws); if (i > -1) hotReloadClients.splice(i, 1); });
|
|
358
|
-
} else if (wsRoute === '/sync') {
|
|
359
|
-
syncClients.add(ws);
|
|
360
|
-
ws.isAlive = true;
|
|
361
|
-
ws.subscriptions = new Set();
|
|
362
|
-
ws.clientId = `client-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
363
|
-
|
|
364
|
-
sendWs(ws, ({
|
|
365
|
-
type: 'sync_connected',
|
|
366
|
-
clientId: ws.clientId,
|
|
367
|
-
timestamp: Date.now()
|
|
368
|
-
}));
|
|
369
|
-
|
|
370
|
-
ws.on('error', (err) => {
|
|
371
|
-
console.error('[WS] Client error (contained):', ws.clientId, err.message);
|
|
372
|
-
});
|
|
373
|
-
ws.on('message', (msg) => {
|
|
374
|
-
try { wsRouter.onMessage(ws, msg); } catch (e) { console.error('[WS] Message handler error (contained):', e.message); }
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
ws.on('pong', () => { ws.isAlive = true; });
|
|
378
|
-
ws.on('close', () => {
|
|
379
|
-
if (ws.terminalProc) { try { ws.terminalProc.kill(); } catch(e) {} ws.terminalProc = null; }
|
|
380
|
-
syncClients.delete(ws);
|
|
381
|
-
wsOptimizer.removeClient(ws);
|
|
382
|
-
for (const sub of ws.subscriptions) {
|
|
383
|
-
const idx = subscriptionIndex.get(sub);
|
|
384
|
-
if (idx) { idx.delete(ws); if (idx.size === 0) subscriptionIndex.delete(sub); }
|
|
385
|
-
}
|
|
386
|
-
if (ws.pm2Subscribed) {
|
|
387
|
-
pm2Subscribers.delete(ws);
|
|
388
|
-
}
|
|
389
|
-
debugLog(`[WebSocket] Client ${ws.clientId} disconnected`);
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
});
|
|
393
|
-
|
|
394
154
|
const BROADCAST_TYPES = new Set([
|
|
395
155
|
'message_created', 'conversation_created', 'conversation_updated',
|
|
396
156
|
'conversations_updated', 'conversation_deleted', 'all_conversations_deleted', 'queue_status', 'queue_updated', 'queue_item_dequeued',
|
|
@@ -420,7 +180,6 @@ const broadcastSync = createBroadcast({
|
|
|
420
180
|
|
|
421
181
|
const cleanupExecution = makeCleanupExecution({ execMachine, activeExecutions, queries, broadcastSync, debugLog });
|
|
422
182
|
|
|
423
|
-
// Wire up process-message factories now that broadcastSync and all deps are available
|
|
424
183
|
const _mqDeps = {
|
|
425
184
|
queries, messageQueues, activeExecutions, rateLimitState, execMachine,
|
|
426
185
|
broadcastSync, cleanupExecution, debugLog,
|
|
@@ -437,34 +196,33 @@ const { processMessageWithStreaming } = createProcessMessage({
|
|
|
437
196
|
scheduleRetry, drainMessageQueue, createEventHandler
|
|
438
197
|
});
|
|
439
198
|
|
|
440
|
-
// WebSocket protocol router
|
|
441
199
|
const wsRouter = new WsRouter();
|
|
442
200
|
|
|
443
201
|
initSpeechManager({ broadcastSync, syncClients, queries });
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
202
|
+
_routes.speech = registerSpeechRoutes({ sendJSON, parseBody, broadcastSync, debugLog });
|
|
203
|
+
_routes.oauth = registerOAuthRoutes({ sendJSON, parseBody, PORT, BASE_URL, rootDir });
|
|
204
|
+
_routes.util = registerUtilRoutes({ sendJSON, parseBody, queries, STARTUP_CWD, PKG_VERSION });
|
|
447
205
|
const _toolRoutes = registerToolRoutes({ sendJSON, parseBody, queries, broadcastSync, logError, toolManager });
|
|
448
|
-
|
|
206
|
+
_routes.threads = registerThreadRoutes({ sendJSON, parseBody, queries });
|
|
449
207
|
const _debugRoutes = registerDebugRoutes({ sendJSON, queries, activeExecutions, messageQueues, syncClients, wsOptimizer, _errLogPath: errLogPath });
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
208
|
+
_routes.conv = registerConvRoutes({ sendJSON, parseBody, queries, activeExecutions, broadcastSync });
|
|
209
|
+
_routes.agents = registerAgentRoutes({ sendJSON, parseBody, queries, discoveredAgents, getACPStatus, modelCache, getModelsForAgent, debugLog });
|
|
210
|
+
_routes.messages = registerMessagesRoutes({ queries, sendJSON, parseBody, broadcastSync, processMessageWithStreaming, activeExecutions, messageQueues, debugLog, logError });
|
|
211
|
+
_routes.sessions = registerSessionsRoutes({ queries, sendJSON, activeExecutions, rateLimitState, debugLog });
|
|
212
|
+
_routes.runs = registerRunsRoutes({ sendJSON, parseBody, queries, broadcastSync, processMessageWithStreaming, activeExecutions, activeProcessesByRunId, discoveredAgents, STARTUP_CWD });
|
|
213
|
+
_routes.scripts = registerScriptsRoutes({ sendJSON, parseBody, queries, broadcastSync, activeScripts, activeExecutions, processMessageWithStreaming, STARTUP_CWD });
|
|
214
|
+
_routes.agentActions = registerAgentActionsRoutes({ sendJSON, queries, broadcastSync, discoveredAgents, activeScripts, startGeminiOAuth, startCodexOAuth, getGeminiOAuthState, getCodexOAuthState, modelCache, PORT, BASE_URL, rootDir });
|
|
215
|
+
_routes.authConfig = registerAuthConfigRoutes({ sendJSON, parseBody, getProviderConfigs, saveProviderConfig });
|
|
458
216
|
|
|
459
217
|
registerConvHandlers(wsRouter, {
|
|
460
218
|
queries, activeExecutions, rateLimitState,
|
|
461
219
|
broadcastSync, processMessageWithStreaming, cleanupExecution,
|
|
462
|
-
getJsonlWatcher
|
|
220
|
+
getJsonlWatcher
|
|
463
221
|
});
|
|
464
222
|
registerConvHandlers2(wsRouter, {
|
|
465
223
|
queries, activeExecutions, rateLimitState,
|
|
466
224
|
broadcastSync, processMessageWithStreaming, cleanupExecution,
|
|
467
|
-
getJsonlWatcher
|
|
225
|
+
getJsonlWatcher
|
|
468
226
|
});
|
|
469
227
|
|
|
470
228
|
registerMsgHandlers(wsRouter, {
|
|
@@ -510,281 +268,16 @@ registerOAuthHandlers(wsRouter, {
|
|
|
510
268
|
codexOAuthState: getCodexOAuthState,
|
|
511
269
|
});
|
|
512
270
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
if (data.conversationId) {
|
|
522
|
-
const key = `conv-${data.conversationId}`;
|
|
523
|
-
ws.subscriptions.add(key);
|
|
524
|
-
if (!subscriptionIndex.has(key)) subscriptionIndex.set(key, new Set());
|
|
525
|
-
subscriptionIndex.get(key).add(ws);
|
|
526
|
-
}
|
|
527
|
-
const subTarget = data.sessionId || data.conversationId;
|
|
528
|
-
debugLog(`[WebSocket] Client ${ws.clientId} subscribed to ${subTarget}`);
|
|
529
|
-
sendWs(ws, ({
|
|
530
|
-
type: 'subscription_confirmed',
|
|
531
|
-
sessionId: data.sessionId,
|
|
532
|
-
conversationId: data.conversationId,
|
|
533
|
-
timestamp: Date.now()
|
|
534
|
-
}));
|
|
535
|
-
|
|
536
|
-
// Notify client if this conversation has an active streaming execution
|
|
537
|
-
// Machine is authoritative for streaming state check on subscribe
|
|
538
|
-
if (data.conversationId && execMachine.isActive(data.conversationId)) {
|
|
539
|
-
const ctx = execMachine.getContext(data.conversationId);
|
|
540
|
-
const execution = activeExecutions.get(data.conversationId);
|
|
541
|
-
const sessionId = ctx?.sessionId || execution?.sessionId;
|
|
542
|
-
const conv = queries.getConversation(data.conversationId);
|
|
543
|
-
const queueLength = execMachine.getQueue(data.conversationId).length || messageQueues.get(data.conversationId)?.length || 0;
|
|
544
|
-
sendWs(ws, ({
|
|
545
|
-
type: 'streaming_start',
|
|
546
|
-
sessionId,
|
|
547
|
-
conversationId: data.conversationId,
|
|
548
|
-
agentId: conv?.agentType || conv?.agentId || 'claude-code',
|
|
549
|
-
queueLength,
|
|
550
|
-
resumed: true,
|
|
551
|
-
seq: ++broadcastSeq,
|
|
552
|
-
timestamp: Date.now()
|
|
553
|
-
}));
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// Inject pending checkpoint events if this is a conversation subscription
|
|
557
|
-
if (data.conversationId && checkpointManager.hasPendingCheckpoint(data.conversationId)) {
|
|
558
|
-
const checkpoint = checkpointManager.getPendingCheckpoint(data.conversationId);
|
|
559
|
-
if (checkpoint) {
|
|
560
|
-
debugLog(`[checkpoint] Injecting ${checkpoint.events.length} events to client for ${data.conversationId}`);
|
|
561
|
-
|
|
562
|
-
const latestSession = queries.getLatestSession(data.conversationId);
|
|
563
|
-
if (latestSession) {
|
|
564
|
-
sendWs(ws, ({
|
|
565
|
-
type: 'streaming_resumed',
|
|
566
|
-
sessionId: latestSession.id,
|
|
567
|
-
conversationId: data.conversationId,
|
|
568
|
-
resumeFrom: checkpoint.sessionId,
|
|
569
|
-
eventCount: checkpoint.events.length,
|
|
570
|
-
chunkCount: checkpoint.chunks.length,
|
|
571
|
-
timestamp: Date.now()
|
|
572
|
-
}));
|
|
573
|
-
|
|
574
|
-
checkpointManager.injectCheckpointEvents(latestSession.id, checkpoint, (evt) => {
|
|
575
|
-
sendWs(ws, ({
|
|
576
|
-
...evt,
|
|
577
|
-
sessionId: latestSession.id,
|
|
578
|
-
conversationId: data.conversationId
|
|
579
|
-
}));
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
} else if (data.type === 'unsubscribe') {
|
|
585
|
-
if (data.sessionId) {
|
|
586
|
-
ws.subscriptions.delete(data.sessionId);
|
|
587
|
-
const idx = subscriptionIndex.get(data.sessionId);
|
|
588
|
-
if (idx) { idx.delete(ws); if (idx.size === 0) subscriptionIndex.delete(data.sessionId); }
|
|
589
|
-
}
|
|
590
|
-
if (data.conversationId) {
|
|
591
|
-
const key = `conv-${data.conversationId}`;
|
|
592
|
-
ws.subscriptions.delete(key);
|
|
593
|
-
const idx = subscriptionIndex.get(key);
|
|
594
|
-
if (idx) { idx.delete(ws); if (idx.size === 0) subscriptionIndex.delete(key); }
|
|
595
|
-
}
|
|
596
|
-
debugLog(`[WebSocket] Client ${ws.clientId} unsubscribed from ${data.sessionId || data.conversationId}`);
|
|
597
|
-
} else if (data.type === 'get_subscriptions') {
|
|
598
|
-
sendWs(ws, ({
|
|
599
|
-
type: 'subscriptions',
|
|
600
|
-
subscriptions: Array.from(ws.subscriptions),
|
|
601
|
-
timestamp: Date.now()
|
|
602
|
-
}));
|
|
603
|
-
} else if (data.type === 'set_voice') {
|
|
604
|
-
ws.ttsVoiceId = data.voiceId || 'default';
|
|
605
|
-
} else if (data.type === 'latency_report') {
|
|
606
|
-
ws.latencyTier = data.quality || 'good';
|
|
607
|
-
ws.latencyAvg = data.avg || 0;
|
|
608
|
-
ws.latencyTrend = data.trend || 'stable';
|
|
609
|
-
} else if (data.type === 'ping') {
|
|
610
|
-
sendWs(ws, ({
|
|
611
|
-
type: 'pong',
|
|
612
|
-
requestId: data.requestId,
|
|
613
|
-
timestamp: Date.now()
|
|
614
|
-
}));
|
|
615
|
-
} else if (data.type === 'terminal_start') {
|
|
616
|
-
if (ws.terminalProc) {
|
|
617
|
-
try { ws.terminalProc.kill(); } catch(e) {}
|
|
618
|
-
}
|
|
619
|
-
try {
|
|
620
|
-
const _req = createRequire(import.meta.url);
|
|
621
|
-
const pty = _req('node-pty');
|
|
622
|
-
const shell = process.env.SHELL || '/bin/bash';
|
|
623
|
-
const cwd = data.cwd || process.env.STARTUP_CWD || process.env.HOME || '/';
|
|
624
|
-
const proc = pty.spawn(shell, [], {
|
|
625
|
-
name: 'xterm-256color',
|
|
626
|
-
cols: data.cols || 80,
|
|
627
|
-
rows: data.rows || 24,
|
|
628
|
-
cwd: cwd,
|
|
629
|
-
env: { ...process.env, TERM: 'xterm-256color', COLORTERM: 'truecolor' }
|
|
630
|
-
});
|
|
631
|
-
ws.terminalProc = proc;
|
|
632
|
-
ws.terminalPty = true;
|
|
633
|
-
proc.on('data', (chunk) => {
|
|
634
|
-
if (ws.readyState === 1) sendWs(ws, ({ type: 'terminal_output', data: Buffer.from(chunk).toString('base64'), encoding: 'base64' }));
|
|
635
|
-
});
|
|
636
|
-
proc.on('exit', (code) => {
|
|
637
|
-
if (ws.readyState === 1) sendWs(ws, { type: 'terminal_exit', code });
|
|
638
|
-
ws.terminalProc = null;
|
|
639
|
-
});
|
|
640
|
-
proc.on('error', (err) => {
|
|
641
|
-
console.error('[TERMINAL] PTY error (contained):', err.message);
|
|
642
|
-
if (ws.readyState === 1) sendWs(ws, { type: 'terminal_exit', code: 1, error: err.message });
|
|
643
|
-
ws.terminalProc = null;
|
|
644
|
-
});
|
|
645
|
-
sendWs(ws, ({ type: 'terminal_started', timestamp: Date.now() }));
|
|
646
|
-
} catch (e) {
|
|
647
|
-
console.error('[TERMINAL] Failed to spawn PTY, falling back to pipes:', e.message);
|
|
648
|
-
const shell = process.env.SHELL || '/bin/bash';
|
|
649
|
-
const cwd = data.cwd || process.env.STARTUP_CWD || process.env.HOME || '/';
|
|
650
|
-
const proc = spawn(shell, ['-i'], { cwd, env: { ...process.env, TERM: 'xterm-256color', COLORTERM: 'truecolor' }, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
651
|
-
ws.terminalProc = proc;
|
|
652
|
-
ws.terminalPty = false;
|
|
653
|
-
proc.stdout.on('data', (chunk) => {
|
|
654
|
-
if (ws.readyState === 1) sendWs(ws, ({ type: 'terminal_output', data: chunk.toString('base64'), encoding: 'base64' }));
|
|
655
|
-
});
|
|
656
|
-
proc.stderr.on('data', (chunk) => {
|
|
657
|
-
if (ws.readyState === 1) sendWs(ws, ({ type: 'terminal_output', data: chunk.toString('base64'), encoding: 'base64' }));
|
|
658
|
-
});
|
|
659
|
-
proc.on('exit', (code) => {
|
|
660
|
-
if (ws.readyState === 1) sendWs(ws, { type: 'terminal_exit', code });
|
|
661
|
-
ws.terminalProc = null;
|
|
662
|
-
});
|
|
663
|
-
proc.on('error', (err) => {
|
|
664
|
-
console.error('[TERMINAL] Spawn error (contained):', err.message);
|
|
665
|
-
if (ws.readyState === 1) sendWs(ws, { type: 'terminal_exit', code: 1, error: err.message });
|
|
666
|
-
ws.terminalProc = null;
|
|
667
|
-
});
|
|
668
|
-
proc.stdin.on('error', () => {});
|
|
669
|
-
proc.stdout.on('error', () => {});
|
|
670
|
-
proc.stderr.on('error', () => {});
|
|
671
|
-
sendWs(ws, ({ type: 'terminal_started', timestamp: Date.now() }));
|
|
672
|
-
}
|
|
673
|
-
} else if (data.type === 'terminal_input') {
|
|
674
|
-
if (ws.terminalProc) {
|
|
675
|
-
try {
|
|
676
|
-
const input = Buffer.from(data.data, 'base64');
|
|
677
|
-
if (ws.terminalPty) {
|
|
678
|
-
ws.terminalProc.write(input);
|
|
679
|
-
} else if (ws.terminalProc.stdin && ws.terminalProc.stdin.writable) {
|
|
680
|
-
ws.terminalProc.stdin.write(input);
|
|
681
|
-
}
|
|
682
|
-
} catch (e) {}
|
|
683
|
-
}
|
|
684
|
-
} else if (data.type === 'terminal_resize') {
|
|
685
|
-
if (ws.terminalProc && ws.terminalPty) {
|
|
686
|
-
try {
|
|
687
|
-
const { cols, rows } = data;
|
|
688
|
-
if (cols && rows && typeof ws.terminalProc.resize === 'function') {
|
|
689
|
-
ws.terminalProc.resize(cols, rows);
|
|
690
|
-
}
|
|
691
|
-
} catch (e) {}
|
|
692
|
-
}
|
|
693
|
-
} else if (data.type === 'terminal_stop') {
|
|
694
|
-
if (ws.terminalProc) {
|
|
695
|
-
try { ws.terminalProc.kill(); } catch(e) {}
|
|
696
|
-
ws.terminalProc = null;
|
|
697
|
-
}
|
|
698
|
-
} else if (data.type === 'pm2_list') {
|
|
699
|
-
if (!pm2Manager.connected) {
|
|
700
|
-
if (ws.readyState === 1) sendWs(ws, ({ type: 'pm2_unavailable', reason: 'PM2 not connected', timestamp: Date.now() }));
|
|
701
|
-
} else {
|
|
702
|
-
pm2Manager.listProcesses().then(processes => {
|
|
703
|
-
if (ws.readyState === 1) {
|
|
704
|
-
const hasActive = processes.some(p => ['online','launching','stopping','waiting restart'].includes(p.status));
|
|
705
|
-
sendWs(ws, { type: 'pm2_list_response', processes, hasActive });
|
|
706
|
-
}
|
|
707
|
-
}).catch(() => {
|
|
708
|
-
if (ws.readyState === 1) sendWs(ws, ({ type: 'pm2_unavailable', reason: 'list failed', timestamp: Date.now() }));
|
|
709
|
-
});
|
|
710
|
-
}
|
|
711
|
-
} else if (data.type === 'pm2_start_monitoring') {
|
|
712
|
-
pm2Subscribers.add(ws);
|
|
713
|
-
ws.pm2Subscribed = true;
|
|
714
|
-
if (!pm2Manager.connected) {
|
|
715
|
-
if (ws.readyState === 1) sendWs(ws, ({ type: 'pm2_unavailable', reason: 'PM2 not connected', timestamp: Date.now() }));
|
|
716
|
-
} else {
|
|
717
|
-
sendWs(ws, { type: 'pm2_monitoring_started' });
|
|
718
|
-
}
|
|
719
|
-
} else if (data.type === 'pm2_stop_monitoring') {
|
|
720
|
-
pm2Subscribers.delete(ws);
|
|
721
|
-
ws.pm2Subscribed = false;
|
|
722
|
-
sendWs(ws, { type: 'pm2_monitoring_stopped' });
|
|
723
|
-
} else if (data.type === 'pm2_start') {
|
|
724
|
-
pm2Manager.startProcess(data.name).then(result => {
|
|
725
|
-
sendWs(ws, { type: 'pm2_start_response', name: data.name, ...result });
|
|
726
|
-
});
|
|
727
|
-
} else if (data.type === 'pm2_stop') {
|
|
728
|
-
pm2Manager.stopProcess(data.name).then(result => {
|
|
729
|
-
sendWs(ws, { type: 'pm2_stop_response', name: data.name, ...result });
|
|
730
|
-
});
|
|
731
|
-
} else if (data.type === 'pm2_restart') {
|
|
732
|
-
pm2Manager.restartProcess(data.name).then(result => {
|
|
733
|
-
sendWs(ws, { type: 'pm2_restart_response', name: data.name, ...result });
|
|
734
|
-
});
|
|
735
|
-
} else if (data.type === 'pm2_delete') {
|
|
736
|
-
pm2Manager.deleteProcess(data.name).then(result => {
|
|
737
|
-
sendWs(ws, { type: 'pm2_delete_response', name: data.name, ...result });
|
|
738
|
-
});
|
|
739
|
-
} else if (data.type === 'pm2_logs') {
|
|
740
|
-
pm2Manager.getLogs(data.name, { lines: data.lines || 100 }).then(result => {
|
|
741
|
-
sendWs(ws, { type: 'pm2_logs_response', name: data.name, ...result });
|
|
742
|
-
});
|
|
743
|
-
} else if (data.type === 'pm2_flush_logs') {
|
|
744
|
-
pm2Manager.flushLogs(data.name).then(result => {
|
|
745
|
-
sendWs(ws, { type: 'pm2_flush_logs_response', name: data.name, ...result });
|
|
746
|
-
});
|
|
747
|
-
} else if (data.type === 'pm2_ping') {
|
|
748
|
-
pm2Manager.ping().then(result => {
|
|
749
|
-
sendWs(ws, { type: 'pm2_ping_response', ...result });
|
|
750
|
-
});
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
} catch (err) { console.error('[WS-LEGACY] Handler error (contained):', err.message); }
|
|
271
|
+
const { wss, hotReloadClients } = createWsSetup(server, {
|
|
272
|
+
BASE_URL, watch, staticDir, _assetCache, htmlState, sendWs, wsRouter, debugLog,
|
|
273
|
+
subscriptionIndex, syncClients, pm2Subscribers, wsOptimizer,
|
|
274
|
+
legacyDeps: {
|
|
275
|
+
subscriptionIndex, execMachine, activeExecutions, messageQueues,
|
|
276
|
+
checkpointManager, queries, pm2Manager, pm2Subscribers,
|
|
277
|
+
getSeq: () => ++broadcastSeq, sendWs, debugLog
|
|
278
|
+
}
|
|
754
279
|
});
|
|
755
280
|
|
|
756
|
-
// Heartbeat interval to detect stale connections
|
|
757
|
-
const heartbeatInterval = setInterval(() => {
|
|
758
|
-
syncClients.forEach(ws => {
|
|
759
|
-
if (!ws.isAlive) {
|
|
760
|
-
syncClients.delete(ws);
|
|
761
|
-
wsOptimizer.removeClient(ws);
|
|
762
|
-
return ws.terminate();
|
|
763
|
-
}
|
|
764
|
-
ws.isAlive = false;
|
|
765
|
-
ws.ping();
|
|
766
|
-
});
|
|
767
|
-
}, 30000);
|
|
768
|
-
|
|
769
|
-
if (watch) {
|
|
770
|
-
const watchedFiles = new Map();
|
|
771
|
-
try {
|
|
772
|
-
fs.readdirSync(staticDir).forEach(file => {
|
|
773
|
-
const fp = path.join(staticDir, file);
|
|
774
|
-
if (watchedFiles.has(fp)) return;
|
|
775
|
-
fs.watchFile(fp, { interval: 100 }, (curr, prev) => {
|
|
776
|
-
if (curr.mtime > prev.mtime) {
|
|
777
|
-
_assetCache.clear();
|
|
778
|
-
htmlState.cache = null;
|
|
779
|
-
htmlState.etag = null;
|
|
780
|
-
hotReloadClients.forEach(c => { if (c.readyState === 1) c.send(JSON.stringify({ type: 'reload' })); });
|
|
781
|
-
}
|
|
782
|
-
});
|
|
783
|
-
watchedFiles.set(fp, true);
|
|
784
|
-
});
|
|
785
|
-
} catch (e) { console.error('Watch error:', e.message); }
|
|
786
|
-
}
|
|
787
|
-
|
|
788
281
|
const { killActiveExecutions, recoverStaleSessions, resumeInterruptedStreams, isProcessAlive, markAgentDead, resumeConversation, performAgentHealthCheck } = createRecovery({
|
|
789
282
|
activeExecutions,
|
|
790
283
|
processMessageWithStreaming,
|
|
@@ -799,7 +292,7 @@ const { killActiveExecutions, recoverStaleSessions, resumeInterruptedStreams, is
|
|
|
799
292
|
process.on('SIGTERM', () => {
|
|
800
293
|
console.log('[SIGNAL] SIGTERM received - graceful shutdown');
|
|
801
294
|
killActiveExecutions();
|
|
802
|
-
if (
|
|
295
|
+
const _jw = getJsonlWatcher(); if (_jw) try { _jw.stop(); } catch (_) {}
|
|
803
296
|
try { pm2Manager.disconnect(); } catch (_) {}
|
|
804
297
|
stopACPTools().catch(() => {}).finally(() => {
|
|
805
298
|
try { wss.close(() => server.close(() => process.exit(0))); } catch (_) { process.exit(0); }
|
|
@@ -823,253 +316,19 @@ server.on('error', (err) => {
|
|
|
823
316
|
}
|
|
824
317
|
});
|
|
825
318
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
// Clear tool status cache on startup to ensure fresh detection
|
|
830
|
-
toolManager.clearStatusCache();
|
|
831
|
-
|
|
832
|
-
console.log(`GMGUI running on http://localhost:${PORT}${BASE_URL}/`);
|
|
833
|
-
console.log(`Agents: ${discoveredAgents.map(a => a.name).join(', ') || 'none'}`);
|
|
834
|
-
console.log(`Hot reload: ${watch ? 'on' : 'off'}`);
|
|
835
|
-
|
|
836
|
-
const deletedCount = queries.cleanupEmptyConversations();
|
|
837
|
-
if (deletedCount > 0) {
|
|
838
|
-
console.log(`Cleaned up ${deletedCount} empty conversation(s) on startup`);
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
recoverStaleSessions();
|
|
842
|
-
warmAssetCache(staticDir);
|
|
843
|
-
|
|
844
|
-
// Run DB cleanup on startup and every 6 hours
|
|
845
|
-
try { queries.cleanup(); console.log('[cleanup] Initial DB cleanup complete'); } catch (e) { console.error('[cleanup] Error:', e.message); }
|
|
846
|
-
setInterval(() => {
|
|
847
|
-
try { queries.cleanup(); console.log('[cleanup] Scheduled DB cleanup complete'); } catch (e) { console.error('[cleanup] Error:', e.message); }
|
|
848
|
-
}, 6 * 60 * 60 * 1000);
|
|
849
|
-
|
|
850
|
-
try {
|
|
851
|
-
jsonlWatcher = new JsonlWatcher({ broadcastSync, queries, ownedSessionIds });
|
|
852
|
-
jsonlWatcher.start();
|
|
853
|
-
console.log('[JSONL] Watcher started');
|
|
854
|
-
} catch (err) {
|
|
855
|
-
console.error('[JSONL] Watcher failed to start:', err.message);
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
resumeInterruptedStreams().catch(err => console.error('[RESUME] Startup error:', err.message));
|
|
859
|
-
|
|
860
|
-
setInterval(() => {
|
|
861
|
-
try {
|
|
862
|
-
const streaming = queries.getStreamingConversations();
|
|
863
|
-
let cleared = 0;
|
|
864
|
-
for (const c of streaming) {
|
|
865
|
-
if (!activeExecutions.has(c.id)) {
|
|
866
|
-
queries.setIsStreaming(c.id, false);
|
|
867
|
-
cleared++;
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
if (cleared > 0) debugLog(`[HEALTH] Cleared ${cleared} stale streaming flag(s)`);
|
|
871
|
-
} catch (e) { debugLog(`[HEALTH] Error: ${e.message}`); }
|
|
872
|
-
}, 5 * 60 * 1000);
|
|
873
|
-
|
|
874
|
-
installGMAgentConfigs().catch(err => console.error('[GM-CONFIG] Startup error:', err.message));
|
|
875
|
-
|
|
876
|
-
startACPTools().then(() => {
|
|
877
|
-
console.log('[ACP] On-demand startup enabled (ACP tools start when first used)');
|
|
878
|
-
setTimeout(() => {
|
|
879
|
-
const acpStatus = getACPStatus();
|
|
880
|
-
for (const s of acpStatus) {
|
|
881
|
-
if (s.healthy) {
|
|
882
|
-
const agent = discoveredAgents.find(a => a.id === s.id);
|
|
883
|
-
if (agent) { agent.acpPort = s.port; }
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
if (acpStatus.length > 0) {
|
|
887
|
-
console.log(`[ACP] Tools ready: ${acpStatus.filter(s => s.healthy).map(s => s.id + ':' + s.port).join(', ') || 'none healthy yet'}`);
|
|
888
|
-
}
|
|
889
|
-
}, 6000);
|
|
890
|
-
}).catch(err => console.error('[ACP] Startup error:', err.message));
|
|
319
|
+
const { performAutoImport } = createAutoImport({ queries, broadcastSync });
|
|
320
|
+
const { performDbRecovery } = createDbRecovery({ queries, debugLog });
|
|
321
|
+
const { loadPluginExtensions } = createPluginLoader({ pluginsDir: path.join(__dirname, 'lib', 'plugins'), expressApp, BASE_URL });
|
|
891
322
|
|
|
892
|
-
|
|
893
|
-
queries.initializeToolInstallations(toolIds.map(id => ({ id })));
|
|
894
|
-
console.log('[TOOLS] Starting background provisioning...');
|
|
323
|
+
setInterval(performDbRecovery, 300000);
|
|
895
324
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
} else if (evt.type === 'tool_install_failed' || evt.type === 'tool_update_failed') {
|
|
904
|
-
queries.updateToolStatus(evt.toolId, { status: 'failed', error_message: evt.data?.error });
|
|
905
|
-
queries.addToolInstallHistory(evt.toolId, evt.type.includes('update') ? 'update' : 'install', 'failed', evt.data?.error);
|
|
906
|
-
} else if (evt.type === 'tool_status_update') {
|
|
907
|
-
const d = evt.data || {};
|
|
908
|
-
if (d.installed) {
|
|
909
|
-
queries.updateToolStatus(evt.toolId, { status: 'installed', version: d.installedVersion || null, installed_at: Date.now() });
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
};
|
|
913
|
-
|
|
914
|
-
// Initial provisioning (blocks until complete)
|
|
915
|
-
toolManager.autoProvision(toolBroadcaster)
|
|
916
|
-
.catch(err => console.error('[TOOLS] Auto-provision error:', err.message))
|
|
917
|
-
.then(() => {
|
|
918
|
-
const acpActors = ['opencode', 'kilo', 'codex'];
|
|
919
|
-
const execActorCount = execMachine.stopAll ? 0 : 0;
|
|
920
|
-
console.log(`[MACHINES] tool-install: ${toolInstallMachine.getMachineActors().size} actors, acp-server: ${acpActors.length} configured`);
|
|
921
|
-
console.log('[TOOLS] Starting periodic update checker...');
|
|
922
|
-
toolManager.startPeriodicUpdateCheck(toolBroadcaster);
|
|
923
|
-
});
|
|
924
|
-
|
|
925
|
-
ensureModelsDownloaded().then(async ok => {
|
|
926
|
-
if (ok) console.log('[MODELS] Speech models ready');
|
|
927
|
-
else console.log('[MODELS] Speech model download failed');
|
|
928
|
-
try {
|
|
929
|
-
const { getVoices } = await getSpeech();
|
|
930
|
-
const voices = getVoices();
|
|
931
|
-
broadcastSync({ type: 'voice_list', voices });
|
|
932
|
-
} catch (err) {
|
|
933
|
-
debugLog('[VOICE] Failed to broadcast voices: ' + err.message);
|
|
934
|
-
broadcastSync({ type: 'voice_list', voices: [] });
|
|
935
|
-
}
|
|
936
|
-
}).catch(async err => {
|
|
937
|
-
console.error('[MODELS] Download error:', err.message);
|
|
938
|
-
try {
|
|
939
|
-
const { getVoices } = await getSpeech();
|
|
940
|
-
const voices = getVoices();
|
|
941
|
-
broadcastSync({ type: 'voice_list', voices });
|
|
942
|
-
} catch (err2) {
|
|
943
|
-
debugLog('[VOICE] Failed to broadcast voices: ' + err2.message);
|
|
944
|
-
broadcastSync({ type: 'voice_list', voices: [] });
|
|
945
|
-
}
|
|
946
|
-
});
|
|
947
|
-
|
|
948
|
-
getSpeech().then(s => s.preloadTTS()).catch(e => debugLog('[TTS] Preload failed: ' + e.message));
|
|
949
|
-
|
|
950
|
-
performAutoImport();
|
|
951
|
-
|
|
952
|
-
setInterval(performAutoImport, 30000);
|
|
953
|
-
|
|
954
|
-
setInterval(performAgentHealthCheck, 30000);
|
|
955
|
-
|
|
956
|
-
// Initialize PM2 monitoring - only when PM2 daemon is available
|
|
957
|
-
const broadcastPM2 = (update) => {
|
|
958
|
-
const msg = JSON.stringify(update);
|
|
959
|
-
for (const client of pm2Subscribers) {
|
|
960
|
-
if (client.readyState === 1) { try { client.send(msg); } catch (_) {} }
|
|
961
|
-
}
|
|
962
|
-
};
|
|
963
|
-
|
|
964
|
-
const startPM2Monitoring = async () => {
|
|
965
|
-
try {
|
|
966
|
-
await pm2Manager.connect();
|
|
967
|
-
await pm2Manager.startMonitoring(broadcastPM2);
|
|
968
|
-
console.log('[PM2] Monitoring started');
|
|
969
|
-
} catch (err) {
|
|
970
|
-
console.log('[PM2] Not available:', err.message);
|
|
971
|
-
broadcastPM2({ type: 'pm2_unavailable', reason: err.message, timestamp: Date.now() });
|
|
972
|
-
}
|
|
973
|
-
};
|
|
974
|
-
|
|
975
|
-
setTimeout(startPM2Monitoring, 2000);
|
|
976
|
-
|
|
977
|
-
setInterval(async () => {
|
|
978
|
-
if (!pm2Manager.connected && !pm2Manager.monitoring) {
|
|
979
|
-
try {
|
|
980
|
-
const healed = await pm2Manager.heal();
|
|
981
|
-
if (healed.success) await pm2Manager.startMonitoring(broadcastPM2);
|
|
982
|
-
} catch (_) {}
|
|
983
|
-
}
|
|
984
|
-
}, 30000);
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
const importMtimeCache = new Map();
|
|
988
|
-
|
|
989
|
-
function hasIndexFilesChanged() {
|
|
990
|
-
const projectsDir = path.join(os.homedir(), '.claude', 'projects');
|
|
991
|
-
if (!fs.existsSync(projectsDir)) return false;
|
|
992
|
-
let changed = false;
|
|
993
|
-
try {
|
|
994
|
-
const dirs = fs.readdirSync(projectsDir);
|
|
995
|
-
for (const d of dirs) {
|
|
996
|
-
const indexPath = path.join(projectsDir, d, 'sessions-index.json');
|
|
997
|
-
try {
|
|
998
|
-
const stat = fs.statSync(indexPath);
|
|
999
|
-
const cached = importMtimeCache.get(indexPath);
|
|
1000
|
-
if (!cached || cached < stat.mtimeMs) {
|
|
1001
|
-
importMtimeCache.set(indexPath, stat.mtimeMs);
|
|
1002
|
-
changed = true;
|
|
1003
|
-
}
|
|
1004
|
-
} catch (_) {}
|
|
1005
|
-
}
|
|
1006
|
-
} catch (_) {}
|
|
1007
|
-
return changed;
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
function performAutoImport() {
|
|
1011
|
-
try {
|
|
1012
|
-
if (!hasIndexFilesChanged()) return;
|
|
1013
|
-
const imported = queries.importClaudeCodeConversations();
|
|
1014
|
-
if (imported.length > 0) {
|
|
1015
|
-
const importedCount = imported.filter(i => i.status === 'imported').length;
|
|
1016
|
-
if (importedCount > 0) {
|
|
1017
|
-
console.log(`[AUTO-IMPORT] Imported ${importedCount} new Claude Code conversations`);
|
|
1018
|
-
broadcastSync({ type: 'conversations_updated', count: importedCount });
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
} catch (err) {
|
|
1022
|
-
console.error('[AUTO-IMPORT] Error:', err.message);
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
function performRecovery() {
|
|
1027
|
-
try {
|
|
1028
|
-
// Cleanup orphaned sessions (older than 7 days)
|
|
1029
|
-
const cleanedUp = queries.cleanupOrphanedSessions(7);
|
|
1030
|
-
if (cleanedUp > 0) {
|
|
1031
|
-
debugLog(`[RECOVERY] Cleaned up ${cleanedUp} orphaned sessions`);
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
// Mark sessions incomplete if they've been processing too long (>2 hours)
|
|
1035
|
-
const longRunning = queries.getSessionsProcessingLongerThan(120);
|
|
1036
|
-
if (longRunning.length > 0) {
|
|
1037
|
-
for (const session of longRunning) {
|
|
1038
|
-
queries.markSessionIncomplete(session.id, 'Timeout: processing exceeded 2 hours');
|
|
1039
|
-
}
|
|
1040
|
-
debugLog(`[RECOVERY] Marked ${longRunning.length} long-running sessions as incomplete`);
|
|
1041
|
-
}
|
|
1042
|
-
} catch (err) {
|
|
1043
|
-
console.error('[RECOVERY] Error:', err.message);
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
// Run recovery every 5 minutes
|
|
1048
|
-
setInterval(performRecovery, 300000);
|
|
1049
|
-
|
|
1050
|
-
// Load plugins as extensions (additive, not replacing core routes)
|
|
1051
|
-
import PluginLoader from './lib/plugin-loader.js';
|
|
1052
|
-
const pluginLoader = new PluginLoader(path.join(__dirname, 'lib', 'plugins'));
|
|
1053
|
-
async function loadPluginExtensions() {
|
|
1054
|
-
try {
|
|
1055
|
-
await pluginLoader.loadAllPlugins({ router: expressApp, baseUrl: BASE_URL, logger: console, env: process.env });
|
|
1056
|
-
const names = Array.from(pluginLoader.registry.keys());
|
|
1057
|
-
if (names.length > 0) {
|
|
1058
|
-
for (const name of names) {
|
|
1059
|
-
const state = pluginLoader.get(name);
|
|
1060
|
-
if (!state || !state.routes) continue;
|
|
1061
|
-
for (const route of state.routes) {
|
|
1062
|
-
const fullPath = BASE_URL + route.path;
|
|
1063
|
-
const method = (route.method || 'GET').toLowerCase();
|
|
1064
|
-
if (expressApp[method]) expressApp[method](fullPath, route.handler);
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
console.log(`[PLUGINS] Loaded extensions: ${names.join(', ')}`);
|
|
1068
|
-
}
|
|
1069
|
-
} catch (err) {
|
|
1070
|
-
console.error('[PLUGINS] Extension loading failed (non-fatal):', err.message);
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
325
|
+
const { onServerReady, getJsonlWatcher } = createOnServerReady({
|
|
326
|
+
queries, broadcastSync, warmAssetCache, staticDir, toolManager, discoveredAgents,
|
|
327
|
+
PORT, BASE_URL, watch, ownedSessionIds, resumeInterruptedStreams, activeExecutions,
|
|
328
|
+
debugLog, installGMAgentConfigs, startACPTools, getACPStatus, execMachine,
|
|
329
|
+
toolInstallMachine, getSpeech, ensureModelsDownloaded, performAutoImport,
|
|
330
|
+
performAgentHealthCheck, pm2Manager, pm2Subscribers, recoverStaleSessions
|
|
331
|
+
});
|
|
1073
332
|
|
|
1074
333
|
server.listen(PORT, () => {
|
|
1075
334
|
onServerReady();
|