agentgui 1.0.153 → 1.0.155
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 +11 -0
- package/package.json +1 -1
- package/server.js +56 -10
- package/static/js/client.js +78 -5
package/database.js
CHANGED
|
@@ -430,6 +430,17 @@ export const queries = {
|
|
|
430
430
|
});
|
|
431
431
|
},
|
|
432
432
|
|
|
433
|
+
getLastUserMessage(conversationId) {
|
|
434
|
+
const stmt = prep(
|
|
435
|
+
"SELECT * FROM messages WHERE conversationId = ? AND role = 'user' ORDER BY created_at DESC LIMIT 1"
|
|
436
|
+
);
|
|
437
|
+
const msg = stmt.get(conversationId);
|
|
438
|
+
if (msg && typeof msg.content === 'string') {
|
|
439
|
+
try { msg.content = JSON.parse(msg.content); } catch (_) {}
|
|
440
|
+
}
|
|
441
|
+
return msg || null;
|
|
442
|
+
},
|
|
443
|
+
|
|
433
444
|
getPaginatedMessages(conversationId, limit = 50, offset = 0) {
|
|
434
445
|
const countStmt = prep('SELECT COUNT(*) as count FROM messages WHERE conversationId = ?');
|
|
435
446
|
const total = countStmt.get(conversationId).count;
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -1228,29 +1228,75 @@ function recoverStaleSessions() {
|
|
|
1228
1228
|
try {
|
|
1229
1229
|
const staleSessions = queries.getActiveSessions ? queries.getActiveSessions() : [];
|
|
1230
1230
|
const now = Date.now();
|
|
1231
|
-
let
|
|
1231
|
+
let resumedCount = 0;
|
|
1232
|
+
let failedCount = 0;
|
|
1233
|
+
|
|
1232
1234
|
for (const session of staleSessions) {
|
|
1233
1235
|
if (activeExecutions.has(session.conversationId)) continue;
|
|
1234
1236
|
const sessionAge = now - session.started_at;
|
|
1235
1237
|
if (sessionAge < STALE_SESSION_MIN_AGE_MS) continue;
|
|
1238
|
+
|
|
1236
1239
|
queries.updateSession(session.id, {
|
|
1237
1240
|
status: 'error',
|
|
1238
|
-
error: '
|
|
1241
|
+
error: 'Server restarted - resuming',
|
|
1239
1242
|
completed_at: now
|
|
1240
1243
|
});
|
|
1241
|
-
|
|
1244
|
+
|
|
1245
|
+
const conv = queries.getConversation(session.conversationId);
|
|
1246
|
+
if (!conv) {
|
|
1247
|
+
queries.setIsStreaming(session.conversationId, false);
|
|
1248
|
+
failedCount++;
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
const lastMsg = queries.getLastUserMessage(session.conversationId);
|
|
1253
|
+
if (!lastMsg || !conv.claudeSessionId) {
|
|
1254
|
+
queries.setIsStreaming(session.conversationId, false);
|
|
1255
|
+
debugLog(`[RECOVERY] Conv ${session.conversationId}: no user message or no claudeSessionId, cannot resume`);
|
|
1256
|
+
broadcastSync({
|
|
1257
|
+
type: 'streaming_error',
|
|
1258
|
+
sessionId: session.id,
|
|
1259
|
+
conversationId: session.conversationId,
|
|
1260
|
+
error: 'Server restarted - could not resume (missing context)',
|
|
1261
|
+
recoverable: false,
|
|
1262
|
+
timestamp: now
|
|
1263
|
+
});
|
|
1264
|
+
failedCount++;
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
const content = typeof lastMsg.content === 'string' ? lastMsg.content : JSON.stringify(lastMsg.content);
|
|
1269
|
+
const agentId = conv.agentType || conv.agentId || 'claude-code';
|
|
1270
|
+
|
|
1271
|
+
debugLog(`[RECOVERY] Resuming conv ${session.conversationId} with claudeSessionId=${conv.claudeSessionId}`);
|
|
1272
|
+
|
|
1273
|
+
const newSession = queries.createSession(session.conversationId);
|
|
1274
|
+
queries.createEvent('session.created', {
|
|
1275
|
+
messageId: lastMsg.id,
|
|
1276
|
+
sessionId: newSession.id,
|
|
1277
|
+
retryReason: 'server_restart'
|
|
1278
|
+
}, session.conversationId, newSession.id);
|
|
1279
|
+
|
|
1242
1280
|
broadcastSync({
|
|
1243
|
-
type: '
|
|
1244
|
-
sessionId:
|
|
1281
|
+
type: 'streaming_start',
|
|
1282
|
+
sessionId: newSession.id,
|
|
1245
1283
|
conversationId: session.conversationId,
|
|
1246
|
-
|
|
1247
|
-
|
|
1284
|
+
messageId: lastMsg.id,
|
|
1285
|
+
agentId,
|
|
1248
1286
|
timestamp: now
|
|
1249
1287
|
});
|
|
1250
|
-
|
|
1288
|
+
|
|
1289
|
+
processMessageWithStreaming(session.conversationId, lastMsg.id, newSession.id, content, agentId)
|
|
1290
|
+
.catch(err => debugLog(`[RECOVERY] Resume error for ${session.conversationId}: ${err.message}`));
|
|
1291
|
+
|
|
1292
|
+
resumedCount++;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
if (resumedCount > 0) {
|
|
1296
|
+
console.log(`[RECOVERY] Resumed ${resumedCount} conversation(s) from previous run`);
|
|
1251
1297
|
}
|
|
1252
|
-
if (
|
|
1253
|
-
console.log(`[RECOVERY]
|
|
1298
|
+
if (failedCount > 0) {
|
|
1299
|
+
console.log(`[RECOVERY] Failed to resume ${failedCount} conversation(s)`);
|
|
1254
1300
|
}
|
|
1255
1301
|
} catch (err) {
|
|
1256
1302
|
console.error('[RECOVERY] Stale session recovery error:', err.message);
|
package/static/js/client.js
CHANGED
|
@@ -609,8 +609,8 @@ class AgentGUIClient {
|
|
|
609
609
|
console.log('Streaming completed:', data);
|
|
610
610
|
|
|
611
611
|
const conversationId = data.conversationId || this.state.currentSession?.conversationId;
|
|
612
|
+
if (conversationId) this.invalidateCache(conversationId);
|
|
612
613
|
|
|
613
|
-
// If this event is for a conversation we are NOT currently viewing, just track state
|
|
614
614
|
if (conversationId && this.state.currentConversation?.id !== conversationId) {
|
|
615
615
|
console.log('Streaming completed for non-active conversation:', conversationId);
|
|
616
616
|
this.emit('streaming:complete', data);
|
|
@@ -1243,11 +1243,34 @@ class AgentGUIClient {
|
|
|
1243
1243
|
}
|
|
1244
1244
|
}
|
|
1245
1245
|
|
|
1246
|
+
cacheCurrentConversation() {
|
|
1247
|
+
const convId = this.state.currentConversation?.id;
|
|
1248
|
+
if (!convId) return;
|
|
1249
|
+
const outputEl = document.getElementById('output');
|
|
1250
|
+
if (!outputEl || !outputEl.firstChild) return;
|
|
1251
|
+
if (this.state.isStreaming) return;
|
|
1252
|
+
|
|
1253
|
+
this.saveScrollPosition(convId);
|
|
1254
|
+
const clone = outputEl.cloneNode(true);
|
|
1255
|
+
this.conversationCache.set(convId, {
|
|
1256
|
+
dom: clone,
|
|
1257
|
+
conversation: this.state.currentConversation,
|
|
1258
|
+
timestamp: Date.now()
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
if (this.conversationCache.size > this.MAX_CACHE_SIZE) {
|
|
1262
|
+
const oldest = this.conversationCache.keys().next().value;
|
|
1263
|
+
this.conversationCache.delete(oldest);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
invalidateCache(conversationId) {
|
|
1268
|
+
this.conversationCache.delete(conversationId);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1246
1271
|
async loadConversationMessages(conversationId) {
|
|
1247
1272
|
try {
|
|
1248
|
-
|
|
1249
|
-
this.saveScrollPosition(this.state.currentConversation.id);
|
|
1250
|
-
}
|
|
1273
|
+
this.cacheCurrentConversation();
|
|
1251
1274
|
this.stopChunkPolling();
|
|
1252
1275
|
if (this.state.isStreaming && this.state.currentConversation?.id !== conversationId) {
|
|
1253
1276
|
this.state.isStreaming = false;
|
|
@@ -1259,9 +1282,27 @@ class AgentGUIClient {
|
|
|
1259
1282
|
this.wsManager.sendMessage({ type: 'subscribe', conversationId });
|
|
1260
1283
|
}
|
|
1261
1284
|
|
|
1285
|
+
const cached = this.conversationCache.get(conversationId);
|
|
1286
|
+
if (cached && (Date.now() - cached.timestamp) < 120000) {
|
|
1287
|
+
const outputEl = document.getElementById('output');
|
|
1288
|
+
if (outputEl) {
|
|
1289
|
+
outputEl.innerHTML = '';
|
|
1290
|
+
while (cached.dom.firstChild) {
|
|
1291
|
+
outputEl.appendChild(cached.dom.firstChild);
|
|
1292
|
+
}
|
|
1293
|
+
this.state.currentConversation = cached.conversation;
|
|
1294
|
+
this.conversationCache.delete(conversationId);
|
|
1295
|
+
this.restoreScrollPosition(conversationId);
|
|
1296
|
+
this.enableControls();
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
this.conversationCache.delete(conversationId);
|
|
1302
|
+
|
|
1262
1303
|
const resp = await fetch(window.__BASE_URL + `/api/conversations/${conversationId}/full`);
|
|
1263
1304
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
1264
|
-
const { conversation, isActivelyStreaming, latestSession, chunks: rawChunks, messages: allMessages } = await resp.json();
|
|
1305
|
+
const { conversation, isActivelyStreaming, latestSession, chunks: rawChunks, totalChunks, messages: allMessages } = await resp.json();
|
|
1265
1306
|
|
|
1266
1307
|
this.state.currentConversation = conversation;
|
|
1267
1308
|
|
|
@@ -1270,6 +1311,7 @@ class AgentGUIClient {
|
|
|
1270
1311
|
block: typeof chunk.data === 'string' ? JSON.parse(chunk.data) : chunk.data
|
|
1271
1312
|
}));
|
|
1272
1313
|
const userMessages = (allMessages || []).filter(m => m.role === 'user');
|
|
1314
|
+
const hasMoreChunks = totalChunks && chunks.length < totalChunks;
|
|
1273
1315
|
|
|
1274
1316
|
const shouldResumeStreaming = isActivelyStreaming && latestSession &&
|
|
1275
1317
|
(latestSession.status === 'active' || latestSession.status === 'pending');
|
|
@@ -1286,6 +1328,29 @@ class AgentGUIClient {
|
|
|
1286
1328
|
`;
|
|
1287
1329
|
|
|
1288
1330
|
const messagesEl = outputEl.querySelector('.conversation-messages');
|
|
1331
|
+
|
|
1332
|
+
if (hasMoreChunks) {
|
|
1333
|
+
const loadMoreBtn = document.createElement('button');
|
|
1334
|
+
loadMoreBtn.className = 'btn btn-secondary';
|
|
1335
|
+
loadMoreBtn.style.cssText = 'width:100%;margin-bottom:1rem;padding:0.5rem;font-size:0.8rem;';
|
|
1336
|
+
loadMoreBtn.textContent = `Load earlier messages (${totalChunks - chunks.length} more chunks)`;
|
|
1337
|
+
loadMoreBtn.addEventListener('click', async () => {
|
|
1338
|
+
loadMoreBtn.disabled = true;
|
|
1339
|
+
loadMoreBtn.textContent = 'Loading...';
|
|
1340
|
+
try {
|
|
1341
|
+
const fullResp = await fetch(window.__BASE_URL + `/api/conversations/${conversationId}/full?allChunks=1`);
|
|
1342
|
+
if (fullResp.ok) {
|
|
1343
|
+
this.invalidateCache(conversationId);
|
|
1344
|
+
await this.loadConversationMessages(conversationId);
|
|
1345
|
+
}
|
|
1346
|
+
} catch (e) {
|
|
1347
|
+
loadMoreBtn.textContent = 'Failed to load. Try again.';
|
|
1348
|
+
loadMoreBtn.disabled = false;
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
messagesEl.appendChild(loadMoreBtn);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1289
1354
|
if (chunks.length > 0) {
|
|
1290
1355
|
const sessionOrder = [];
|
|
1291
1356
|
const sessionChunks = {};
|
|
@@ -1299,6 +1364,14 @@ class AgentGUIClient {
|
|
|
1299
1364
|
|
|
1300
1365
|
const frag = document.createDocumentFragment();
|
|
1301
1366
|
let userMsgIdx = 0;
|
|
1367
|
+
|
|
1368
|
+
if (hasMoreChunks && sessionOrder.length > 0) {
|
|
1369
|
+
const firstChunkTime = chunks[0].created_at;
|
|
1370
|
+
while (userMsgIdx < userMessages.length && userMessages[userMsgIdx].created_at < firstChunkTime) {
|
|
1371
|
+
userMsgIdx++;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1302
1375
|
sessionOrder.forEach((sessionId) => {
|
|
1303
1376
|
const sessionChunkList = sessionChunks[sessionId];
|
|
1304
1377
|
const sessionStart = sessionChunkList[0].created_at;
|