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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.153",
3
+ "version": "1.0.155",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
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 recoveredCount = 0;
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: 'Agent died unexpectedly (server restart)',
1241
+ error: 'Server restarted - resuming',
1239
1242
  completed_at: now
1240
1243
  });
1241
- queries.setIsStreaming(session.conversationId, false);
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: 'streaming_error',
1244
- sessionId: session.id,
1281
+ type: 'streaming_start',
1282
+ sessionId: newSession.id,
1245
1283
  conversationId: session.conversationId,
1246
- error: 'Agent died unexpectedly (server restart)',
1247
- recoverable: false,
1284
+ messageId: lastMsg.id,
1285
+ agentId,
1248
1286
  timestamp: now
1249
1287
  });
1250
- recoveredCount++;
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 (recoveredCount > 0) {
1253
- console.log(`[RECOVERY] Recovered ${recoveredCount} stale active session(s)`);
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);
@@ -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
- if (this.state.currentConversation?.id) {
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;