agentgui 1.0.152 → 1.0.154

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
@@ -1031,6 +1031,11 @@ export const queries = {
1031
1031
  });
1032
1032
  },
1033
1033
 
1034
+ getConversationChunkCount(conversationId) {
1035
+ const stmt = prep('SELECT COUNT(*) as count FROM chunks WHERE conversationId = ?');
1036
+ return stmt.get(conversationId).count;
1037
+ },
1038
+
1034
1039
  getConversationChunks(conversationId) {
1035
1040
  const stmt = prep(
1036
1041
  `SELECT id, sessionId, conversationId, sequence, type, data, created_at
@@ -1049,6 +1054,26 @@ export const queries = {
1049
1054
  });
1050
1055
  },
1051
1056
 
1057
+ getRecentConversationChunks(conversationId, limit) {
1058
+ const stmt = prep(
1059
+ `SELECT id, sessionId, conversationId, sequence, type, data, created_at
1060
+ FROM chunks WHERE conversationId = ?
1061
+ ORDER BY created_at DESC LIMIT ?`
1062
+ );
1063
+ const rows = stmt.all(conversationId, limit);
1064
+ rows.reverse();
1065
+ return rows.map(row => {
1066
+ try {
1067
+ return {
1068
+ ...row,
1069
+ data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
1070
+ };
1071
+ } catch (e) {
1072
+ return row;
1073
+ }
1074
+ });
1075
+ },
1076
+
1052
1077
  getChunksSince(sessionId, timestamp) {
1053
1078
  const stmt = prep(
1054
1079
  `SELECT id, sessionId, conversationId, sequence, type, data, created_at
@@ -46,7 +46,8 @@ class AgentRunner {
46
46
  const {
47
47
  timeout = 300000,
48
48
  onEvent = null,
49
- onError = null
49
+ onError = null,
50
+ onRateLimit = null
50
51
  } = config;
51
52
 
52
53
  const args = this.buildArgs(prompt, config);
@@ -60,6 +61,8 @@ class AgentRunner {
60
61
  const outputs = [];
61
62
  let timedOut = false;
62
63
  let sessionId = null;
64
+ let rateLimited = false;
65
+ let retryAfterSec = 60;
63
66
 
64
67
  const timeoutHandle = setTimeout(() => {
65
68
  timedOut = true;
@@ -103,6 +106,14 @@ class AgentRunner {
103
106
  proc.stderr.on('data', (chunk) => {
104
107
  const errorText = chunk.toString();
105
108
  console.error(`[${this.id}] stderr:`, errorText);
109
+
110
+ const rateLimitMatch = errorText.match(/rate.?limit|429|too many requests|overloaded|throttl/i);
111
+ if (rateLimitMatch) {
112
+ rateLimited = true;
113
+ const retryMatch = errorText.match(/retry.?after[:\s]+(\d+)/i);
114
+ if (retryMatch) retryAfterSec = parseInt(retryMatch[1], 10) || 60;
115
+ }
116
+
106
117
  if (onError) {
107
118
  try { onError(errorText); } catch (e) {}
108
119
  }
@@ -112,7 +123,17 @@ class AgentRunner {
112
123
  clearTimeout(timeoutHandle);
113
124
  if (timedOut) return;
114
125
 
115
- // Some agents return non-zero but still produce valid output
126
+ if (rateLimited) {
127
+ const err = new Error(`Rate limited - retry after ${retryAfterSec}s`);
128
+ err.rateLimited = true;
129
+ err.retryAfterSec = retryAfterSec;
130
+ if (onRateLimit) {
131
+ try { onRateLimit({ retryAfterSec }); } catch (e) {}
132
+ }
133
+ reject(err);
134
+ return;
135
+ }
136
+
116
137
  const success = code === 0 || (outputs.length > 0 && this.allowNonZeroExit);
117
138
 
118
139
  if (success) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.152",
3
+ "version": "1.0.154",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -24,9 +24,11 @@ const SYSTEM_PROMPT = `Write all responses as clean semantic HTML. Use tags like
24
24
 
25
25
  const activeExecutions = new Map();
26
26
  const messageQueues = new Map();
27
+ const rateLimitState = new Map();
27
28
  const STUCK_AGENT_THRESHOLD_MS = 600000;
28
29
  const NO_PID_GRACE_PERIOD_MS = 60000;
29
30
  const STALE_SESSION_MIN_AGE_MS = 30000;
31
+ const DEFAULT_RATE_LIMIT_COOLDOWN_MS = 60000;
30
32
 
31
33
  const debugLog = (msg) => {
32
34
  const timestamp = new Date().toISOString();
@@ -298,8 +300,8 @@ const server = http.createServer(async (req, res) => {
298
300
  const conv = queries.getConversation(conversationId);
299
301
  if (!conv) { sendJSON(req, res, 404, { error: 'Conversation not found' }); return; }
300
302
 
301
- const prompt = body.content || '';
302
- const agentId = body.agentId || 'claude-code';
303
+ const prompt = body.content || body.message || '';
304
+ const agentId = body.agentId || conv.agentType || conv.agentId || 'claude-code';
303
305
 
304
306
  const userMessage = queries.createMessage(conversationId, 'user', prompt);
305
307
  queries.createEvent('message.created', { role: 'user', messageId: userMessage.id }, conversationId);
@@ -362,14 +364,28 @@ const server = http.createServer(async (req, res) => {
362
364
  const latestSession = queries.getLatestSession(conversationId);
363
365
  const isActivelyStreaming = activeExecutions.has(conversationId) ||
364
366
  (latestSession && latestSession.status === 'active');
365
- const chunks = queries.getConversationChunks(conversationId);
367
+
368
+ const url = new URL(req.url, 'http://localhost');
369
+ const chunkLimit = Math.min(parseInt(url.searchParams.get('chunkLimit') || '500'), 5000);
370
+ const allChunks = url.searchParams.get('allChunks') === '1';
371
+
372
+ const totalChunks = queries.getConversationChunkCount(conversationId);
373
+ let chunks;
374
+ if (allChunks || totalChunks <= chunkLimit) {
375
+ chunks = queries.getConversationChunks(conversationId);
376
+ } else {
377
+ chunks = queries.getRecentConversationChunks(conversationId, chunkLimit);
378
+ }
366
379
  const msgResult = queries.getPaginatedMessages(conversationId, 100, 0);
380
+ const rateLimitInfo = rateLimitState.get(conversationId) || null;
367
381
  sendJSON(req, res, 200, {
368
382
  conversation: conv,
369
383
  isActivelyStreaming,
370
384
  latestSession,
371
385
  chunks,
372
- messages: msgResult.messages
386
+ totalChunks,
387
+ messages: msgResult.messages,
388
+ rateLimitInfo
373
389
  });
374
390
  return;
375
391
  }
@@ -702,6 +718,7 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
702
718
  activeExecutions.set(conversationId, { pid: null, startTime, sessionId, lastActivity: startTime });
703
719
  queries.setIsStreaming(conversationId, true);
704
720
  queries.updateSession(sessionId, { status: 'active' });
721
+ const batcher = createChunkBatcher();
705
722
 
706
723
  try {
707
724
  debugLog(`[stream] Starting: conversationId=${conversationId}, sessionId=${sessionId}`);
@@ -713,7 +730,6 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
713
730
  let allBlocks = [];
714
731
  let eventCount = 0;
715
732
  let currentSequence = queries.getMaxSequence(sessionId) ?? -1;
716
- const batcher = createChunkBatcher();
717
733
 
718
734
  const onEvent = (parsed) => {
719
735
  eventCount++;
@@ -826,12 +842,13 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
826
842
  };
827
843
 
828
844
  const { outputs, sessionId: claudeSessionId } = await runClaudeWithStreaming(content, cwd, agentId || 'claude-code', config);
845
+ activeExecutions.delete(conversationId);
829
846
  batcher.drain();
830
847
  debugLog(`[stream] Claude returned ${outputs.length} outputs, sessionId=${claudeSessionId}`);
831
848
 
832
- if (claudeSessionId && !conv?.claudeSessionId) {
849
+ if (claudeSessionId) {
833
850
  queries.setClaudeSessionId(conversationId, claudeSessionId);
834
- debugLog(`[stream] Stored claudeSessionId=${claudeSessionId}`);
851
+ debugLog(`[stream] Updated claudeSessionId=${claudeSessionId}`);
835
852
  }
836
853
 
837
854
  // Mark session as complete
@@ -854,13 +871,47 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
854
871
  const elapsed = Date.now() - startTime;
855
872
  debugLog(`[stream] Error after ${elapsed}ms: ${error.message}`);
856
873
 
857
- // Mark session as error
874
+ const isRateLimit = error.rateLimited ||
875
+ /rate.?limit|429|too many requests|overloaded|throttl/i.test(error.message);
876
+
858
877
  queries.updateSession(sessionId, {
859
878
  status: 'error',
860
879
  error: error.message,
861
880
  completed_at: Date.now()
862
881
  });
863
882
 
883
+ if (isRateLimit) {
884
+ const cooldownMs = (error.retryAfterSec || 60) * 1000;
885
+ const retryAt = Date.now() + cooldownMs;
886
+ rateLimitState.set(conversationId, { retryAt, cooldownMs });
887
+ debugLog(`[rate-limit] Conv ${conversationId} hit rate limit, retry in ${cooldownMs}ms`);
888
+
889
+ broadcastSync({
890
+ type: 'rate_limit_hit',
891
+ sessionId,
892
+ conversationId,
893
+ retryAfterMs: cooldownMs,
894
+ retryAt,
895
+ timestamp: Date.now()
896
+ });
897
+
898
+ batcher.drain();
899
+ activeExecutions.delete(conversationId);
900
+ queries.setIsStreaming(conversationId, false);
901
+
902
+ setTimeout(() => {
903
+ rateLimitState.delete(conversationId);
904
+ debugLog(`[rate-limit] Conv ${conversationId} cooldown expired, restarting`);
905
+ broadcastSync({
906
+ type: 'rate_limit_clear',
907
+ conversationId,
908
+ timestamp: Date.now()
909
+ });
910
+ scheduleRetry(conversationId, messageId, content, agentId);
911
+ }, cooldownMs);
912
+ return;
913
+ }
914
+
864
915
  broadcastSync({
865
916
  type: 'streaming_error',
866
917
  sessionId,
@@ -881,10 +932,29 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
881
932
  batcher.drain();
882
933
  activeExecutions.delete(conversationId);
883
934
  queries.setIsStreaming(conversationId, false);
884
- drainMessageQueue(conversationId);
935
+ if (!rateLimitState.has(conversationId)) {
936
+ drainMessageQueue(conversationId);
937
+ }
885
938
  }
886
939
  }
887
940
 
941
+ function scheduleRetry(conversationId, messageId, content, agentId) {
942
+ const newSession = queries.createSession(conversationId);
943
+ queries.createEvent('session.created', { messageId, sessionId: newSession.id, retryReason: 'rate_limit' }, conversationId, newSession.id);
944
+
945
+ broadcastSync({
946
+ type: 'streaming_start',
947
+ sessionId: newSession.id,
948
+ conversationId,
949
+ messageId,
950
+ agentId,
951
+ timestamp: Date.now()
952
+ });
953
+
954
+ processMessageWithStreaming(conversationId, messageId, newSession.id, content, agentId)
955
+ .catch(err => debugLog(`[retry] Error: ${err.message}`));
956
+ }
957
+
888
958
  function drainMessageQueue(conversationId) {
889
959
  const queue = messageQueues.get(conversationId);
890
960
  if (!queue || queue.length === 0) return;
@@ -932,7 +1002,7 @@ async function processMessage(conversationId, messageId, content, agentId) {
932
1002
  systemPrompt: SYSTEM_PROMPT
933
1003
  });
934
1004
 
935
- if (claudeSessionId && !conv?.claudeSessionId) {
1005
+ if (claudeSessionId) {
936
1006
  queries.setClaudeSessionId(conversationId, claudeSessionId);
937
1007
  }
938
1008
 
@@ -1060,7 +1130,8 @@ wss.on('connection', (ws, req) => {
1060
1130
  const BROADCAST_TYPES = new Set([
1061
1131
  'message_created', 'conversation_created', 'conversations_updated',
1062
1132
  'conversation_deleted', 'queue_status', 'streaming_start',
1063
- 'streaming_complete', 'streaming_error'
1133
+ 'streaming_complete', 'streaming_error', 'rate_limit_hit',
1134
+ 'rate_limit_clear'
1064
1135
  ]);
1065
1136
 
1066
1137
  const wsBatchQueues = new Map();
package/static/index.html CHANGED
@@ -527,39 +527,6 @@
527
527
  font-size: 0.75rem;
528
528
  }
529
529
 
530
- .streaming-block-tool-use {
531
- margin: 0.25rem 0;
532
- border-left: 3px solid #06b6d4;
533
- background: rgba(6,182,212,0.06);
534
- border-radius: 0 0.375rem 0.375rem 0;
535
- overflow: hidden;
536
- }
537
- .streaming-block-tool-use.folded-tool {
538
- border-left: none;
539
- border-radius: 0.375rem;
540
- margin: 0.125rem 0;
541
- }
542
-
543
- .tool-use-header {
544
- padding: 0.5rem 0.75rem;
545
- font-weight: 600;
546
- font-size: 0.85rem;
547
- display: flex;
548
- align-items: center;
549
- gap: 0.375rem;
550
- }
551
-
552
- .tool-use-icon {
553
- font-size: 0.9rem;
554
- opacity: 0.7;
555
- }
556
-
557
- .tool-use-name {
558
- color: #0891b2;
559
- }
560
-
561
- html.dark .tool-use-name { color: #22d3ee; }
562
-
563
530
  .tool-input-details {
564
531
  }
565
532
 
@@ -1102,6 +1069,12 @@
1102
1069
  opacity: 0.3;
1103
1070
  }
1104
1071
 
1072
+ .conversation-messages { contain: content; }
1073
+ .streaming-blocks { contain: content; }
1074
+ .sidebar-list { contain: strict; content-visibility: auto; }
1075
+ .message { contain: layout style; content-visibility: auto; contain-intrinsic-size: auto 120px; }
1076
+ #output-scroll { will-change: transform; }
1077
+
1105
1078
  .voice-block .voice-result-stats {
1106
1079
  font-size: 0.8rem;
1107
1080
  color: var(--color-text-secondary);
@@ -1585,6 +1558,90 @@
1585
1558
  font-size: 0.7rem;
1586
1559
  }
1587
1560
 
1561
+ /* --- Error variant of folded-tool --- */
1562
+ .folded-tool.folded-tool-error {
1563
+ background: #fef2f2;
1564
+ border-color: #fecaca;
1565
+ }
1566
+ html.dark .folded-tool.folded-tool-error {
1567
+ background: #1c0f0f;
1568
+ border-color: #7f1d1d;
1569
+ }
1570
+ .folded-tool-error > .folded-tool-bar {
1571
+ background: #fee2e2;
1572
+ }
1573
+ html.dark .folded-tool-error > .folded-tool-bar {
1574
+ background: #2c1010;
1575
+ }
1576
+ .folded-tool-error > .folded-tool-bar::before { color: #ef4444; }
1577
+ html.dark .folded-tool-error > .folded-tool-bar::before { color: #f87171; }
1578
+ .folded-tool-error > .folded-tool-bar:hover { background: #fecaca; }
1579
+ html.dark .folded-tool-error > .folded-tool-bar:hover { background: #451a1a; }
1580
+ .folded-tool-error .folded-tool-icon { color: #ef4444; }
1581
+ html.dark .folded-tool-error .folded-tool-icon { color: #f87171; }
1582
+ .folded-tool-error .folded-tool-name { color: #991b1b; }
1583
+ html.dark .folded-tool-error .folded-tool-name { color: #fca5a5; }
1584
+ .folded-tool-error .folded-tool-desc { color: #b91c1c; }
1585
+ html.dark .folded-tool-error .folded-tool-desc { color: #f87171; }
1586
+ .folded-tool-error > .folded-tool-body { border-top-color: #fecaca; }
1587
+ html.dark .folded-tool-error > .folded-tool-body { border-top-color: #7f1d1d; }
1588
+
1589
+ /* --- Collapsible Code Summary --- */
1590
+ .collapsible-code {
1591
+ margin: 0.25rem 0;
1592
+ border-radius: 0.375rem;
1593
+ overflow: hidden;
1594
+ background: #1e293b;
1595
+ border: 1px solid #334155;
1596
+ }
1597
+ .collapsible-code-summary {
1598
+ display: flex;
1599
+ align-items: center;
1600
+ gap: 0.5rem;
1601
+ padding: 0.3rem 0.75rem;
1602
+ cursor: pointer;
1603
+ user-select: none;
1604
+ list-style: none;
1605
+ font-size: 0.75rem;
1606
+ line-height: 1.3;
1607
+ background: #1f2937;
1608
+ color: #9ca3af;
1609
+ font-family: 'Monaco','Menlo','Ubuntu Mono', monospace;
1610
+ transition: background 0.15s;
1611
+ }
1612
+ .collapsible-code-summary::-webkit-details-marker { display: none; }
1613
+ .collapsible-code-summary::marker { display: none; content: ''; }
1614
+ .collapsible-code-summary::before {
1615
+ content: '\25b6';
1616
+ font-size: 0.5rem;
1617
+ margin-right: 0.125rem;
1618
+ display: inline-block;
1619
+ transition: transform 0.15s;
1620
+ color: #6b7280;
1621
+ flex-shrink: 0;
1622
+ }
1623
+ .collapsible-code[open] > .collapsible-code-summary::before { transform: rotate(90deg); }
1624
+ .collapsible-code-summary:hover { background: #374151; }
1625
+ .collapsible-code-summary .copy-code-btn {
1626
+ margin-left: auto;
1627
+ background: none;
1628
+ border: none;
1629
+ color: #6b7280;
1630
+ cursor: pointer;
1631
+ padding: 0.125rem;
1632
+ border-radius: 0.25rem;
1633
+ display: flex;
1634
+ align-items: center;
1635
+ transition: all 0.15s;
1636
+ }
1637
+ .collapsible-code-summary .copy-code-btn:hover { color: #e5e7eb; background: #4b5563; }
1638
+ .collapsible-code-summary .copy-code-btn svg { width: 0.875rem; height: 0.875rem; }
1639
+ .collapsible-code-label {
1640
+ font-size: 0.7rem;
1641
+ text-transform: uppercase;
1642
+ letter-spacing: 0.05em;
1643
+ }
1644
+
1588
1645
  /* --- Tool Result Block --- */
1589
1646
  .block-tool-result {
1590
1647
  margin-bottom: 0.25rem;
@@ -2077,6 +2134,11 @@
2077
2134
  </div>
2078
2135
  </div>
2079
2136
 
2137
+ <script>
2138
+ var _escHtmlMap = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
2139
+ var _escHtmlRe = /[&<>"']/g;
2140
+ window._escHtml = function(t) { return t.replace(_escHtmlRe, function(c) { return _escHtmlMap[c]; }); };
2141
+ </script>
2080
2142
  <script defer src="/gm/js/event-processor.js"></script>
2081
2143
  <script defer src="/gm/js/streaming-renderer.js"></script>
2082
2144
  <script defer src="/gm/js/websocket-manager.js"></script>