agentgui 1.0.616 → 1.0.618
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/TOOLS-FIX-SUMMARY.md +122 -0
- package/TOOLS_COMPLETION_REPORT.md +199 -0
- package/TOOLS_DEBUG_SUMMARY.md +105 -0
- package/database.js +5 -2
- package/lib/speech.js +0 -1
- package/lib/tool-manager.js +178 -119
- package/lib/ws-handlers-conv.js +16 -2
- package/lib/ws-handlers-session.js +32 -39
- package/lib/ws-handlers-util.js +32 -1
- package/package.json +1 -1
- package/scripts/patch-fsbrowse.js +131 -4
- package/server.js +205 -115
- package/static/index.html +13 -22
- package/static/js/client.js +61 -2
- package/static/js/streaming-renderer.js +3 -1
- package/static/js/tools-manager.js +11 -0
- package/test-cli-detection.mjs +37 -0
- package/test-system-validation.js +188 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Patch script to fix Windows path duplication issue in fsbrowse
|
|
4
|
+
* and sync fsbrowse styling with AgentGUI dark mode theme
|
|
4
5
|
* Fixes: Error ENOENT: no such file or directory, scandir 'C:\C:\dev'
|
|
5
6
|
*/
|
|
6
7
|
|
|
@@ -54,10 +55,10 @@ try {
|
|
|
54
55
|
// to avoid duplication like C:\\C:\\dev
|
|
55
56
|
if (baseDriveLetter && sanitizedIsAbsoluteOnDrive && sanitizedDriveLetter === baseDriveLetter) {
|
|
56
57
|
// Remove drive letter and leading slashes to make it relative
|
|
57
|
-
let relativePath = sanitized;
|
|
58
|
-
if (/^[A-Z]:/i.test(relativePath)) {
|
|
59
|
-
relativePath = relativePath.substring(2);
|
|
60
|
-
if (relativePath[0] === '/' || relativePath[0] === String.fromCharCode(92)) relativePath = relativePath.substring(1);
|
|
58
|
+
let relativePath = sanitized;
|
|
59
|
+
if (/^[A-Z]:/i.test(relativePath)) {
|
|
60
|
+
relativePath = relativePath.substring(2);
|
|
61
|
+
if (relativePath[0] === '/' || relativePath[0] === String.fromCharCode(92)) relativePath = relativePath.substring(1);
|
|
61
62
|
}
|
|
62
63
|
fullPath = path.resolve(normalizedBase, relativePath);
|
|
63
64
|
} else {
|
|
@@ -90,3 +91,129 @@ try {
|
|
|
90
91
|
console.error('[PATCH] Error applying fsbrowse patch:', err.message);
|
|
91
92
|
process.exit(1);
|
|
92
93
|
}
|
|
94
|
+
|
|
95
|
+
// Patch fsbrowse CSS for dark mode theme sync
|
|
96
|
+
const fsbrowseCSSPath = path.join(__dirname, '..', 'node_modules', 'fsbrowse', 'public', 'style.css');
|
|
97
|
+
|
|
98
|
+
if (fs.existsSync(fsbrowseCSSPath)) {
|
|
99
|
+
try {
|
|
100
|
+
let cssContent = fs.readFileSync(fsbrowseCSSPath, 'utf8');
|
|
101
|
+
|
|
102
|
+
// Check if dark mode CSS is already patched
|
|
103
|
+
if (cssContent.includes('html.dark {')) {
|
|
104
|
+
console.log('[PATCH] fsbrowse dark mode CSS already patched');
|
|
105
|
+
} else {
|
|
106
|
+
// Inject dark mode CSS rules
|
|
107
|
+
const darkModeCSS = `/* Light mode - explicit */
|
|
108
|
+
html.light {
|
|
109
|
+
--primary: #3b82f6;
|
|
110
|
+
--primary-dark: #2563eb;
|
|
111
|
+
--secondary: #6b7280;
|
|
112
|
+
--border: #e5e7eb;
|
|
113
|
+
--bg: #ffffff;
|
|
114
|
+
--bg-alt: #f9fafb;
|
|
115
|
+
--text: #111827;
|
|
116
|
+
--text-light: #6b7280;
|
|
117
|
+
--danger: #ef4444;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* Dark mode - explicit, matches AgentGUI grey dark theme */
|
|
121
|
+
html.dark {
|
|
122
|
+
--primary: #737373;
|
|
123
|
+
--primary-dark: #525252;
|
|
124
|
+
--secondary: #a3a3a3;
|
|
125
|
+
--border: #333333;
|
|
126
|
+
--bg: #1a1a1a;
|
|
127
|
+
--bg-alt: #242424;
|
|
128
|
+
--text: #e5e5e5;
|
|
129
|
+
--text-light: #a3a3a3;
|
|
130
|
+
--danger: #ef4444;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* Fallback: media query for dark mode preference */
|
|
134
|
+
@media (prefers-color-scheme: dark) {
|
|
135
|
+
:root:not(.light) {
|
|
136
|
+
--primary: #60a5fa;
|
|
137
|
+
--primary-dark: #3b82f6;
|
|
138
|
+
--secondary: #9ca3af;
|
|
139
|
+
--border: #374151;
|
|
140
|
+
--bg: #111827;
|
|
141
|
+
--bg-alt: #1f2937;
|
|
142
|
+
--text: #f3f4f6;
|
|
143
|
+
--text-light: #9ca3af;
|
|
144
|
+
--danger: #f87171;
|
|
145
|
+
}
|
|
146
|
+
}`;
|
|
147
|
+
|
|
148
|
+
// Find the closing brace of :root and insert after it
|
|
149
|
+
cssContent = cssContent.replace(
|
|
150
|
+
/:root \{[\s\S]*?\}\s*@media/,
|
|
151
|
+
match => match.replace('@media', darkModeCSS + '\n@media')
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
fs.writeFileSync(fsbrowseCSSPath, cssContent, 'utf8');
|
|
155
|
+
console.log('[PATCH] fsbrowse dark mode CSS patched successfully');
|
|
156
|
+
}
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.warn('[PATCH] Could not patch fsbrowse CSS:', err.message);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Patch fsbrowse app.js for theme sync
|
|
163
|
+
const fsbrowseAppJSPath = path.join(__dirname, '..', 'node_modules', 'fsbrowse', 'public', 'app.js');
|
|
164
|
+
|
|
165
|
+
if (fs.existsSync(fsbrowseAppJSPath)) {
|
|
166
|
+
try {
|
|
167
|
+
let appContent = fs.readFileSync(fsbrowseAppJSPath, 'utf8');
|
|
168
|
+
|
|
169
|
+
// Check if theme sync is already patched
|
|
170
|
+
if (appContent.includes('setupThemeSync')) {
|
|
171
|
+
console.log('[PATCH] fsbrowse theme sync already patched');
|
|
172
|
+
} else {
|
|
173
|
+
// Inject setupThemeSync call and method
|
|
174
|
+
const themeSyncMethod = `
|
|
175
|
+
setupThemeSync() {
|
|
176
|
+
// Sync theme from parent window/localStorage if available
|
|
177
|
+
const syncTheme = () => {
|
|
178
|
+
const theme = localStorage.getItem('gmgui-theme') ||
|
|
179
|
+
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
|
180
|
+
document.documentElement.className = theme;
|
|
181
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
syncTheme();
|
|
185
|
+
|
|
186
|
+
// Watch for storage changes from other tabs/windows
|
|
187
|
+
window.addEventListener('storage', e => {
|
|
188
|
+
if (e.key === 'gmgui-theme') syncTheme();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Watch for media query changes
|
|
192
|
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', syncTheme);
|
|
193
|
+
},`;
|
|
194
|
+
|
|
195
|
+
// Add setupThemeSync call to init()
|
|
196
|
+
appContent = appContent.replace(
|
|
197
|
+
'async init() {',
|
|
198
|
+
'async init() {\n this.setupThemeSync();'
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// Add setupThemeSync method after init()
|
|
202
|
+
appContent = appContent.replace(
|
|
203
|
+
'async init() {\n this.setupThemeSync();\n this.setupDragDrop();',
|
|
204
|
+
'async init() {\n this.setupThemeSync();\n this.setupDragDrop();'
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// Insert the method after the api() method
|
|
208
|
+
appContent = appContent.replace(
|
|
209
|
+
'api(path) {\n return `${this.basePath}${path}`;\n },',
|
|
210
|
+
'api(path) {\n return `${this.basePath}${path}`;\n },' + themeSyncMethod
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
fs.writeFileSync(fsbrowseAppJSPath, appContent, 'utf8');
|
|
214
|
+
console.log('[PATCH] fsbrowse theme sync patched successfully');
|
|
215
|
+
}
|
|
216
|
+
} catch (err) {
|
|
217
|
+
console.warn('[PATCH] Could not patch fsbrowse app.js:', err.message);
|
|
218
|
+
}
|
|
219
|
+
}
|
package/server.js
CHANGED
|
@@ -372,37 +372,63 @@ expressApp.post(BASE_URL + '/api/upload/:conversationId', (req, res) => {
|
|
|
372
372
|
}
|
|
373
373
|
});
|
|
374
374
|
|
|
375
|
+
// Cache fsbrowse routers per conversation to ensure API calls work
|
|
376
|
+
const fsbrowseRouters = new Map();
|
|
377
|
+
|
|
375
378
|
// fsbrowse file browser - mounted per conversation workingDirectory
|
|
376
379
|
// Route: /gm/files/:conversationId/*
|
|
377
380
|
expressApp.use(BASE_URL + '/files/:conversationId', (req, res, next) => {
|
|
378
|
-
const
|
|
381
|
+
const convId = req.params.conversationId;
|
|
382
|
+
const conv = queries.getConversation(convId);
|
|
379
383
|
if (!conv || !conv.workingDirectory) {
|
|
380
384
|
return res.status(404).json({ error: 'Conversation not found or no working directory' });
|
|
381
385
|
}
|
|
386
|
+
|
|
382
387
|
// Normalize the working directory path to avoid Windows path duplication issues
|
|
383
388
|
const normalizedWorkingDir = path.resolve(conv.workingDirectory);
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
389
|
+
|
|
390
|
+
// Get or create cached fsbrowse router for this conversation
|
|
391
|
+
let router = fsbrowseRouters.get(convId);
|
|
392
|
+
if (!router) {
|
|
393
|
+
router = fsbrowse({ baseDir: normalizedWorkingDir, name: 'Files' });
|
|
394
|
+
fsbrowseRouters.set(convId, router);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Set baseUrl before calling the router
|
|
398
|
+
req.baseUrl = BASE_URL + '/files/' + convId;
|
|
399
|
+
req.url = req.url.replace(new RegExp(`^${BASE_URL}/files/${convId}`), '');
|
|
400
|
+
|
|
388
401
|
router(req, res, next);
|
|
389
402
|
});
|
|
390
403
|
|
|
391
404
|
function findCommand(cmd) {
|
|
392
405
|
const isWindows = os.platform() === 'win32';
|
|
393
406
|
const localBin = path.join(path.dirname(fileURLToPath(import.meta.url)), 'node_modules', '.bin', isWindows ? cmd + '.cmd' : cmd);
|
|
394
|
-
if (fs.existsSync(localBin))
|
|
407
|
+
if (fs.existsSync(localBin)) {
|
|
408
|
+
console.log(`[agent-discovery] Found ${cmd} in local node_modules`);
|
|
409
|
+
return localBin;
|
|
410
|
+
}
|
|
395
411
|
try {
|
|
412
|
+
// Increase timeout to 10 seconds to handle slower systems and running agents
|
|
413
|
+
const timeoutMs = 10000;
|
|
396
414
|
if (isWindows) {
|
|
397
|
-
const result = execSync(`where ${cmd}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
|
|
398
|
-
|
|
415
|
+
const result = execSync(`where ${cmd}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'], timeout: timeoutMs }).trim();
|
|
416
|
+
if (result) {
|
|
417
|
+
console.log(`[agent-discovery] Found ${cmd} in PATH`);
|
|
418
|
+
return result.split('\n')[0].trim();
|
|
419
|
+
}
|
|
399
420
|
} else {
|
|
400
|
-
const result = execSync(`which ${cmd}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
|
|
401
|
-
|
|
421
|
+
const result = execSync(`which ${cmd}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'], timeout: timeoutMs }).trim();
|
|
422
|
+
if (result) {
|
|
423
|
+
console.log(`[agent-discovery] Found ${cmd} in PATH`);
|
|
424
|
+
return result;
|
|
425
|
+
}
|
|
402
426
|
}
|
|
403
|
-
} catch (
|
|
427
|
+
} catch (err) {
|
|
428
|
+
console.log(`[agent-discovery] ${cmd} not found or timed out`);
|
|
404
429
|
return null;
|
|
405
430
|
}
|
|
431
|
+
return null;
|
|
406
432
|
}
|
|
407
433
|
|
|
408
434
|
async function queryACPServerAgents(baseUrl) {
|
|
@@ -479,7 +505,11 @@ function discoverAgents() {
|
|
|
479
505
|
if (result) {
|
|
480
506
|
agents.push({ id: bin.id, name: bin.name, icon: bin.icon, path: result, protocol: bin.protocol });
|
|
481
507
|
} else if (bin.npxPackage) {
|
|
508
|
+
// For npx-launchable packages (including claude-code as fallback)
|
|
482
509
|
agents.push({ id: bin.id, name: bin.name, icon: bin.icon, path: null, protocol: bin.protocol, npxPackage: bin.npxPackage, npxLaunchable: true });
|
|
510
|
+
} else if (bin.id === 'claude-code') {
|
|
511
|
+
// Ensure Claude Code is always available as an npx-launchable agent
|
|
512
|
+
agents.push({ id: bin.id, name: bin.name, icon: bin.icon, path: null, protocol: bin.protocol, npxPackage: '@anthropic-ai/claude-code', npxLaunchable: true });
|
|
483
513
|
}
|
|
484
514
|
}
|
|
485
515
|
return agents;
|
|
@@ -497,9 +527,23 @@ async function discoverExternalACPServers() {
|
|
|
497
527
|
return externalAgents;
|
|
498
528
|
}
|
|
499
529
|
|
|
500
|
-
|
|
530
|
+
let discoveredAgents = [];
|
|
501
531
|
initializeDescriptors(discoveredAgents);
|
|
502
532
|
|
|
533
|
+
// Agent discovery happens asynchronously in background to not block startup
|
|
534
|
+
async function initializeAgentDiscovery() {
|
|
535
|
+
try {
|
|
536
|
+
discoveredAgents = discoverAgents();
|
|
537
|
+
initializeDescriptors(discoveredAgents);
|
|
538
|
+
console.log('[AGENTS] Discovered:', discoveredAgents.map(a => ({ id: a.id, found: !!a.path })));
|
|
539
|
+
} catch (err) {
|
|
540
|
+
console.error('[AGENTS] Discovery error:', err.message);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Start immediately but don't wait for it
|
|
545
|
+
initializeAgentDiscovery().catch(() => {});
|
|
546
|
+
|
|
503
547
|
const modelCache = new Map();
|
|
504
548
|
|
|
505
549
|
async function getModelsForAgent(agentId) {
|
|
@@ -1031,11 +1075,23 @@ const server = http.createServer(async (req, res) => {
|
|
|
1031
1075
|
|
|
1032
1076
|
if (req.url === '/') { res.writeHead(302, { Location: BASE_URL + '/' }); res.end(); return; }
|
|
1033
1077
|
|
|
1034
|
-
|
|
1078
|
+
// Handle requests with or without BASE_URL prefix (for reverse proxy compatibility)
|
|
1079
|
+
let routePath = req.url;
|
|
1080
|
+
if (req.url.startsWith(BASE_URL + '/')) {
|
|
1081
|
+
routePath = req.url.slice(BASE_URL.length);
|
|
1082
|
+
} else if (req.url === BASE_URL) {
|
|
1083
|
+
routePath = '/';
|
|
1084
|
+
} else if (req.url.startsWith('/api/') || req.url.startsWith('/js/') || req.url.startsWith('/css/') ||
|
|
1085
|
+
req.url.startsWith('/vendor/') || req.url.startsWith('/sync') || req.url === '/' ||
|
|
1086
|
+
req.url.startsWith('/conversations/')) {
|
|
1087
|
+
// Allow requests without BASE_URL prefix for static files and known routes
|
|
1088
|
+
// This supports reverse proxies that strip the BASE_URL prefix
|
|
1089
|
+
routePath = req.url;
|
|
1090
|
+
} else {
|
|
1035
1091
|
res.writeHead(404); res.end('Not found'); return;
|
|
1036
1092
|
}
|
|
1037
1093
|
|
|
1038
|
-
|
|
1094
|
+
routePath = routePath || '/';
|
|
1039
1095
|
|
|
1040
1096
|
try {
|
|
1041
1097
|
// Remove query parameters from routePath for matching
|
|
@@ -1833,21 +1889,44 @@ const server = http.createServer(async (req, res) => {
|
|
|
1833
1889
|
|
|
1834
1890
|
if (pathOnly === '/api/tools' && req.method === 'GET') {
|
|
1835
1891
|
console.log('[TOOLS-API] Handling GET /api/tools');
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1892
|
+
try {
|
|
1893
|
+
// Return immediately with cached data (non-blocking) - skip network version checks
|
|
1894
|
+
const tools = await Promise.race([
|
|
1895
|
+
toolManager.getAllToolsAsync(true), // skipPublishedVersion=true for fast response
|
|
1896
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000))
|
|
1897
|
+
]);
|
|
1898
|
+
const result = tools.map((t) => ({
|
|
1899
|
+
id: t.id,
|
|
1900
|
+
name: t.name,
|
|
1901
|
+
pkg: t.pkg,
|
|
1902
|
+
category: t.category || 'plugin',
|
|
1903
|
+
installed: t.installed,
|
|
1904
|
+
status: t.installed ? (t.isUpToDate ? 'installed' : 'needs_update') : 'not_installed',
|
|
1905
|
+
isUpToDate: t.isUpToDate,
|
|
1906
|
+
upgradeNeeded: t.upgradeNeeded,
|
|
1907
|
+
hasUpdate: t.upgradeNeeded && t.installed,
|
|
1908
|
+
installedVersion: t.installedVersion,
|
|
1909
|
+
publishedVersion: t.publishedVersion
|
|
1910
|
+
}));
|
|
1911
|
+
sendJSON(req, res, 200, { tools: result });
|
|
1912
|
+
} catch (err) {
|
|
1913
|
+
console.log('[TOOLS-API] Error getting tools, returning cached status:', err.message);
|
|
1914
|
+
// Return synchronously cached tool status - this provides immediate response with last-known status
|
|
1915
|
+
const tools = toolManager.getAllToolsSync().map((t) => ({
|
|
1916
|
+
id: t.id,
|
|
1917
|
+
name: t.name,
|
|
1918
|
+
pkg: t.pkg,
|
|
1919
|
+
category: t.category || 'plugin',
|
|
1920
|
+
installed: t.installed || false,
|
|
1921
|
+
status: (t.installed) ? (t.isUpToDate ? 'installed' : 'needs_update') : 'not_installed',
|
|
1922
|
+
isUpToDate: t.isUpToDate || false,
|
|
1923
|
+
upgradeNeeded: t.upgradeNeeded || false,
|
|
1924
|
+
hasUpdate: (t.upgradeNeeded && t.installed) || false,
|
|
1925
|
+
installedVersion: t.installedVersion || null,
|
|
1926
|
+
publishedVersion: t.publishedVersion || null
|
|
1927
|
+
}));
|
|
1928
|
+
sendJSON(req, res, 200, { tools });
|
|
1929
|
+
}
|
|
1851
1930
|
return;
|
|
1852
1931
|
}
|
|
1853
1932
|
|
|
@@ -3425,6 +3504,7 @@ function serveFile(filePath, res, req) {
|
|
|
3425
3504
|
const baseTag = `<script>window.__BASE_URL='${BASE_URL}';</script>`;
|
|
3426
3505
|
content = content.replace('<head>', `<head>\n <base href="${BASE_URL}/">\n ` + baseTag);
|
|
3427
3506
|
content = content.replace(/(href|src)="vendor\//g, `$1="${BASE_URL}/vendor/`);
|
|
3507
|
+
content = content.replace(/(src)="\/gm\/js\//g, `$1="${BASE_URL}/js/`);
|
|
3428
3508
|
if (watch) {
|
|
3429
3509
|
content += `\n<script>(function(){const ws=new WebSocket((location.protocol==='https:'?'wss://':'ws://')+location.host+'${BASE_URL}/hot-reload');ws.onmessage=e=>{if(JSON.parse(e.data).type==='reload')location.reload()};})();</script>`;
|
|
3430
3510
|
}
|
|
@@ -4155,6 +4235,7 @@ const BROADCAST_TYPES = new Set([
|
|
|
4155
4235
|
'streaming_start', 'streaming_progress', 'streaming_complete', 'streaming_error',
|
|
4156
4236
|
'tool_install_started', 'tool_install_progress', 'tool_install_complete', 'tool_install_failed',
|
|
4157
4237
|
'tool_update_progress', 'tool_update_complete', 'tool_update_failed',
|
|
4238
|
+
'tool_status_update',
|
|
4158
4239
|
'tools_update_started', 'tools_update_complete', 'tools_refresh_complete',
|
|
4159
4240
|
'pm2_monit_update', 'pm2_monitoring_started', 'pm2_monitoring_stopped',
|
|
4160
4241
|
'pm2_list_response', 'pm2_start_response', 'pm2_stop_response',
|
|
@@ -4219,7 +4300,7 @@ registerUtilHandlers(wsRouter, {
|
|
|
4219
4300
|
broadcastSync, getSpeech, getProviderConfigs, saveProviderConfig,
|
|
4220
4301
|
startGeminiOAuth, exchangeGeminiOAuthCode,
|
|
4221
4302
|
geminiOAuthState: () => geminiOAuthState,
|
|
4222
|
-
STARTUP_CWD, activeScripts, voiceCacheManager, toolManager
|
|
4303
|
+
STARTUP_CWD, activeScripts, voiceCacheManager, toolManager, discoveredAgents
|
|
4223
4304
|
});
|
|
4224
4305
|
|
|
4225
4306
|
wsRouter.onLegacy((data, ws) => {
|
|
@@ -4508,13 +4589,12 @@ server.on('error', (err) => {
|
|
|
4508
4589
|
function recoverStaleSessions() {
|
|
4509
4590
|
try {
|
|
4510
4591
|
const now = Date.now();
|
|
4592
|
+
const RESUME_WINDOW_MS = 600000; // 10 minutes
|
|
4511
4593
|
|
|
4512
4594
|
const resumable = new Set();
|
|
4513
|
-
const resumableConvs = queries.getResumableConversations ? queries.getResumableConversations() : [];
|
|
4595
|
+
const resumableConvs = queries.getResumableConversations ? queries.getResumableConversations(RESUME_WINDOW_MS) : [];
|
|
4514
4596
|
for (const conv of resumableConvs) {
|
|
4515
|
-
|
|
4516
|
-
resumable.add(conv.id);
|
|
4517
|
-
}
|
|
4597
|
+
resumable.add(conv.id); // All agent types are resumable
|
|
4518
4598
|
}
|
|
4519
4599
|
|
|
4520
4600
|
const staleSessions = queries.getActiveSessions ? queries.getActiveSessions() : [];
|
|
@@ -4554,23 +4634,29 @@ function recoverStaleSessions() {
|
|
|
4554
4634
|
|
|
4555
4635
|
async function resumeInterruptedStreams() {
|
|
4556
4636
|
try {
|
|
4637
|
+
const RESUME_WINDOW_MS = 600000; // Only resume sessions active within the last 10 minutes
|
|
4638
|
+
const cutoff = Date.now() - RESUME_WINDOW_MS;
|
|
4639
|
+
|
|
4557
4640
|
// Get conversations marked as streaming in database (isStreaming=1)
|
|
4558
4641
|
// Fall back to getResumableConversations if isStreaming is not being used
|
|
4559
4642
|
let toResume = [];
|
|
4560
4643
|
|
|
4561
4644
|
// Primary: Check database isStreaming flag for conversations still marked as active
|
|
4562
|
-
// Exclude conversations whose last session completed
|
|
4645
|
+
// Exclude conversations whose last session completed or started more than 10 min ago
|
|
4563
4646
|
const streamingConvs = queries.getConversations().filter(c => {
|
|
4564
4647
|
if (c.isStreaming !== 1) return false;
|
|
4565
4648
|
const lastSession = queries.getLatestSession(c.id);
|
|
4566
|
-
|
|
4649
|
+
if (!lastSession) return false;
|
|
4650
|
+
if (lastSession.status === 'complete') return false;
|
|
4651
|
+
// Only resume if session started within the last 10 minutes
|
|
4652
|
+
return lastSession.started_at > cutoff;
|
|
4567
4653
|
});
|
|
4568
4654
|
|
|
4569
4655
|
if (streamingConvs.length > 0) {
|
|
4570
4656
|
toResume = streamingConvs;
|
|
4571
4657
|
} else {
|
|
4572
|
-
// Fallback: Use session-based resumable conversations
|
|
4573
|
-
toResume = queries.getResumableConversations ? queries.getResumableConversations() : [];
|
|
4658
|
+
// Fallback: Use session-based resumable conversations (already filtered by 10 min)
|
|
4659
|
+
toResume = queries.getResumableConversations ? queries.getResumableConversations(RESUME_WINDOW_MS) : [];
|
|
4574
4660
|
}
|
|
4575
4661
|
|
|
4576
4662
|
if (toResume.length === 0) return;
|
|
@@ -4580,87 +4666,13 @@ async function resumeInterruptedStreams() {
|
|
|
4580
4666
|
for (let i = 0; i < toResume.length; i++) {
|
|
4581
4667
|
const conv = toResume[i];
|
|
4582
4668
|
try {
|
|
4583
|
-
// Find previous incomplete sessions to load checkpoint from
|
|
4584
4669
|
const previousSessions = [...queries.getSessionsByStatus(conv.id, 'active'), ...queries.getSessionsByStatus(conv.id, 'pending')];
|
|
4585
4670
|
const previousSessionId = previousSessions.length > 0 ? previousSessions[0].id : null;
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
const checkpoint = previousSessionId ? checkpointManager.loadCheckpoint(previousSessionId) : null;
|
|
4589
|
-
|
|
4590
|
-
for (const s of previousSessions) {
|
|
4591
|
-
queries.updateSession(s.id, { status: 'interrupted', error: 'Server restarted, resuming', completed_at: Date.now() });
|
|
4592
|
-
}
|
|
4593
|
-
|
|
4594
|
-
const lastMsg = queries.getLastUserMessage(conv.id);
|
|
4595
|
-
const prompt = lastMsg?.content || 'continue';
|
|
4596
|
-
const promptText = typeof prompt === 'string' ? prompt : JSON.stringify(prompt);
|
|
4597
|
-
|
|
4598
|
-
const session = queries.createSession(conv.id);
|
|
4599
|
-
queries.createEvent('session.created', {
|
|
4600
|
-
sessionId: session.id,
|
|
4601
|
-
resumeReason: 'server_restart',
|
|
4602
|
-
claudeSessionId: conv.claudeSessionId,
|
|
4603
|
-
checkpointFrom: previousSessionId
|
|
4604
|
-
}, conv.id, session.id);
|
|
4605
|
-
|
|
4606
|
-
activeExecutions.set(conv.id, {
|
|
4607
|
-
pid: null,
|
|
4608
|
-
startTime: Date.now(),
|
|
4609
|
-
sessionId: session.id,
|
|
4610
|
-
lastActivity: Date.now()
|
|
4611
|
-
});
|
|
4612
|
-
|
|
4613
|
-
broadcastSync({
|
|
4614
|
-
type: 'streaming_start',
|
|
4615
|
-
sessionId: session.id,
|
|
4616
|
-
conversationId: conv.id,
|
|
4617
|
-
agentId: conv.agentType,
|
|
4618
|
-
resumed: true,
|
|
4619
|
-
checkpointAvailable: !!checkpoint,
|
|
4620
|
-
timestamp: Date.now()
|
|
4621
|
-
});
|
|
4622
|
-
|
|
4623
|
-
// Store checkpoint to inject when client subscribes (not now, since no clients connected yet)
|
|
4624
|
-
if (checkpoint) {
|
|
4625
|
-
checkpointManager.storeCheckpointForDelay(conv.id, checkpoint);
|
|
4626
|
-
checkpointManager.markSessionResumed(previousSessionId);
|
|
4627
|
-
console.log(`[RESUME] Checkpoint stored for ${conv.id}, will inject on next client subscribe`);
|
|
4628
|
-
}
|
|
4629
|
-
|
|
4630
|
-
const messageId = lastMsg?.id || null;
|
|
4631
|
-
console.log(`[RESUME] Resuming conv ${conv.id} (claude session: ${conv.claudeSessionId}) with checkpoint=${!!checkpoint}`);
|
|
4632
|
-
|
|
4633
|
-
try {
|
|
4634
|
-
await processMessageWithStreaming(conv.id, messageId, session.id, promptText, conv.agentType, conv.model, conv.subAgent);
|
|
4635
|
-
} catch (err) {
|
|
4636
|
-
console.error(`[RESUME] Error resuming conv ${conv.id}: ${err.message}`);
|
|
4637
|
-
queries.setIsStreaming(conv.id, false);
|
|
4638
|
-
const activeSessions = queries.getSessionsByStatus(conv.id, 'active');
|
|
4639
|
-
const pendingSessions = queries.getSessionsByStatus(conv.id, 'pending');
|
|
4640
|
-
for (const s of [...activeSessions, ...pendingSessions]) {
|
|
4641
|
-
queries.updateSession(s.id, {
|
|
4642
|
-
status: 'error',
|
|
4643
|
-
error: 'Resume failed: ' + err.message,
|
|
4644
|
-
completed_at: Date.now()
|
|
4645
|
-
});
|
|
4646
|
-
}
|
|
4647
|
-
}
|
|
4648
|
-
|
|
4649
|
-
if (i < toResume.length - 1) {
|
|
4650
|
-
await new Promise(r => setTimeout(r, 200));
|
|
4651
|
-
}
|
|
4671
|
+
await resumeConversation(conv.id, previousSessionId, 'Server restarted, resuming');
|
|
4672
|
+
if (i < toResume.length - 1) await new Promise(r => setTimeout(r, 200));
|
|
4652
4673
|
} catch (err) {
|
|
4653
4674
|
console.error(`[RESUME] Failed to resume conv ${conv.id}: ${err.message}`);
|
|
4654
4675
|
queries.setIsStreaming(conv.id, false);
|
|
4655
|
-
const activeSessions = queries.getSessionsByStatus(conv.id, 'active');
|
|
4656
|
-
const pendingSessions = queries.getSessionsByStatus(conv.id, 'pending');
|
|
4657
|
-
for (const s of [...activeSessions, ...pendingSessions]) {
|
|
4658
|
-
queries.updateSession(s.id, {
|
|
4659
|
-
status: 'error',
|
|
4660
|
-
error: 'Resume failed: ' + err.message,
|
|
4661
|
-
completed_at: Date.now()
|
|
4662
|
-
});
|
|
4663
|
-
}
|
|
4664
4676
|
}
|
|
4665
4677
|
}
|
|
4666
4678
|
} catch (err) {
|
|
@@ -4681,14 +4693,29 @@ function isProcessAlive(pid) {
|
|
|
4681
4693
|
function markAgentDead(conversationId, entry, reason) {
|
|
4682
4694
|
if (!activeExecutions.has(conversationId)) return;
|
|
4683
4695
|
activeExecutions.delete(conversationId);
|
|
4696
|
+
|
|
4697
|
+
const RESUME_WINDOW_MS = 600000; // 10 minutes
|
|
4698
|
+
const sessionAge = entry.startTime ? Date.now() - entry.startTime : Infinity;
|
|
4699
|
+
const shouldRestart = sessionAge < RESUME_WINDOW_MS;
|
|
4700
|
+
|
|
4684
4701
|
queries.setIsStreaming(conversationId, false);
|
|
4685
4702
|
if (entry.sessionId) {
|
|
4686
4703
|
queries.updateSession(entry.sessionId, {
|
|
4687
|
-
status: 'error',
|
|
4704
|
+
status: shouldRestart ? 'interrupted' : 'error',
|
|
4688
4705
|
error: reason,
|
|
4689
4706
|
completed_at: Date.now()
|
|
4690
4707
|
});
|
|
4691
4708
|
}
|
|
4709
|
+
|
|
4710
|
+
if (shouldRestart) {
|
|
4711
|
+
// Session was recent — restart it automatically
|
|
4712
|
+
resumeConversation(conversationId, entry.sessionId, reason).catch(err => {
|
|
4713
|
+
console.error(`[RESUME] Auto-restart failed for conv ${conversationId}: ${err.message}`);
|
|
4714
|
+
queries.setIsStreaming(conversationId, false);
|
|
4715
|
+
});
|
|
4716
|
+
return;
|
|
4717
|
+
}
|
|
4718
|
+
|
|
4692
4719
|
broadcastSync({
|
|
4693
4720
|
type: 'streaming_error',
|
|
4694
4721
|
sessionId: entry.sessionId,
|
|
@@ -4700,6 +4727,61 @@ function markAgentDead(conversationId, entry, reason) {
|
|
|
4700
4727
|
drainMessageQueue(conversationId);
|
|
4701
4728
|
}
|
|
4702
4729
|
|
|
4730
|
+
// Resume a single conversation after interruption. Used both by markAgentDead and resumeInterruptedStreams.
|
|
4731
|
+
async function resumeConversation(conversationId, previousSessionId, reason) {
|
|
4732
|
+
const conv = queries.getConversation(conversationId);
|
|
4733
|
+
if (!conv) throw new Error('Conversation not found');
|
|
4734
|
+
|
|
4735
|
+
const checkpoint = previousSessionId ? checkpointManager.loadCheckpoint(previousSessionId) : null;
|
|
4736
|
+
|
|
4737
|
+
if (previousSessionId) {
|
|
4738
|
+
// Only mark interrupted if not already done
|
|
4739
|
+
const prev = queries.getSession ? queries.getSession(previousSessionId) : null;
|
|
4740
|
+
if (prev && prev.status !== 'interrupted') {
|
|
4741
|
+
queries.updateSession(previousSessionId, { status: 'interrupted', error: reason || 'Restarting', completed_at: Date.now() });
|
|
4742
|
+
}
|
|
4743
|
+
if (checkpoint) {
|
|
4744
|
+
checkpointManager.markSessionResumed(previousSessionId);
|
|
4745
|
+
}
|
|
4746
|
+
}
|
|
4747
|
+
|
|
4748
|
+
const lastMsg = queries.getLastUserMessage(conversationId);
|
|
4749
|
+
const promptText = typeof lastMsg?.content === 'string' ? lastMsg.content : JSON.stringify(lastMsg?.content || 'continue');
|
|
4750
|
+
|
|
4751
|
+
const session = queries.createSession(conversationId);
|
|
4752
|
+
queries.createEvent('session.created', {
|
|
4753
|
+
sessionId: session.id,
|
|
4754
|
+
resumeReason: 'interrupted',
|
|
4755
|
+
claudeSessionId: conv.claudeSessionId,
|
|
4756
|
+
checkpointFrom: previousSessionId || null
|
|
4757
|
+
}, conversationId, session.id);
|
|
4758
|
+
|
|
4759
|
+
activeExecutions.set(conversationId, {
|
|
4760
|
+
pid: null,
|
|
4761
|
+
startTime: Date.now(),
|
|
4762
|
+
sessionId: session.id,
|
|
4763
|
+
lastActivity: Date.now()
|
|
4764
|
+
});
|
|
4765
|
+
|
|
4766
|
+
broadcastSync({
|
|
4767
|
+
type: 'streaming_start',
|
|
4768
|
+
sessionId: session.id,
|
|
4769
|
+
conversationId,
|
|
4770
|
+
agentId: conv.agentType,
|
|
4771
|
+
resumed: true,
|
|
4772
|
+
checkpointAvailable: !!checkpoint,
|
|
4773
|
+
timestamp: Date.now()
|
|
4774
|
+
});
|
|
4775
|
+
|
|
4776
|
+
if (checkpoint) {
|
|
4777
|
+
checkpointManager.storeCheckpointForDelay(conversationId, checkpoint);
|
|
4778
|
+
console.log(`[RESUME] Checkpoint stored for conv ${conversationId}`);
|
|
4779
|
+
}
|
|
4780
|
+
|
|
4781
|
+
console.log(`[RESUME] Restarting conv ${conversationId} (reason: ${reason})`);
|
|
4782
|
+
await processMessageWithStreaming(conversationId, lastMsg?.id || null, session.id, promptText, conv.agentType, conv.model, conv.subAgent);
|
|
4783
|
+
}
|
|
4784
|
+
|
|
4703
4785
|
function performAgentHealthCheck() {
|
|
4704
4786
|
const now = Date.now();
|
|
4705
4787
|
for (const [conversationId, entry] of activeExecutions) {
|
|
@@ -4725,6 +4807,9 @@ function performAgentHealthCheck() {
|
|
|
4725
4807
|
}
|
|
4726
4808
|
|
|
4727
4809
|
function onServerReady() {
|
|
4810
|
+
// Clear tool status cache on startup to ensure fresh detection
|
|
4811
|
+
toolManager.clearStatusCache();
|
|
4812
|
+
|
|
4728
4813
|
console.log(`GMGUI running on http://localhost:${PORT}${BASE_URL}/`);
|
|
4729
4814
|
console.log(`Agents: ${discoveredAgents.map(a => a.name).join(', ') || 'none'}`);
|
|
4730
4815
|
console.log(`Hot reload: ${watch ? 'on' : 'off'}`);
|
|
@@ -4768,6 +4853,11 @@ function onServerReady() {
|
|
|
4768
4853
|
} else if (evt.type === 'tool_install_failed' || evt.type === 'tool_update_failed') {
|
|
4769
4854
|
queries.updateToolStatus(evt.toolId, { status: 'failed', error_message: evt.data?.error });
|
|
4770
4855
|
queries.addToolInstallHistory(evt.toolId, evt.type.includes('update') ? 'update' : 'install', 'failed', evt.data?.error);
|
|
4856
|
+
} else if (evt.type === 'tool_status_update') {
|
|
4857
|
+
const d = evt.data || {};
|
|
4858
|
+
if (d.installed) {
|
|
4859
|
+
queries.updateToolStatus(evt.toolId, { status: 'installed', version: d.installedVersion || null, installed_at: Date.now() });
|
|
4860
|
+
}
|
|
4771
4861
|
}
|
|
4772
4862
|
}).catch(err => console.error('[TOOLS] Auto-provision error:', err.message));
|
|
4773
4863
|
|