agentgui 1.0.854 → 1.0.855

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.
@@ -1,10 +1,10 @@
1
1
  import path from 'path';
2
+ import fs from 'fs';
2
3
 
3
4
  export class JsonlParser {
4
- constructor({ broadcastSync, queries, ownedSessionIds }) {
5
+ constructor({ broadcastSync, queries }) {
5
6
  this._bc = broadcastSync;
6
7
  this._q = queries;
7
- this._owned = ownedSessionIds;
8
8
  this._convMap = new Map();
9
9
  this._emitted = new Map();
10
10
  this._seqs = new Map();
@@ -12,6 +12,17 @@ export class JsonlParser {
12
12
  this._sessions = new Map();
13
13
  }
14
14
 
15
+ /**
16
+ * Pre-register a GUI-spawned session so _conv finds the right conversation
17
+ * and _dbSession reuses the existing session ID instead of creating a new one.
18
+ * Must be called as soon as session_id is obtained from Claude stdout, before
19
+ * the 16ms file-watcher debounce fires.
20
+ */
21
+ registerSession(claudeSessionId, convId, dbSessionId) {
22
+ this._convMap.set(claudeSessionId, convId);
23
+ if (dbSessionId) this._sessions.set(claudeSessionId, dbSessionId);
24
+ }
25
+
15
26
  clear() {
16
27
  this._convMap.clear();
17
28
  this._emitted.clear();
@@ -32,22 +43,30 @@ export class JsonlParser {
32
43
  for (const sid of [...this._streaming]) this._endStreaming(this._convMap.get(sid), sid);
33
44
  }
34
45
 
35
- _line(fp, line) {
36
- line = line.trim(); if (!line) return;
37
- let e; try { e = JSON.parse(line); } catch (_) { return; }
38
- if (!e || !e.sessionId) return;
39
- if (this._owned?.has(e.sessionId)) return;
40
- const cid = this._conv(e.sessionId, e, fp);
41
- if (cid) this._route(cid, e.sessionId, e);
42
- }
43
-
44
- _conv(sid, e) {
46
+ _conv(sid, e, fp) {
45
47
  if (this._convMap.has(sid)) return this._convMap.get(sid);
46
48
  const found = this._q.getConversations().find(c => c.claudeSessionId === sid);
47
49
  if (found) { this._convMap.set(sid, found.id); return found.id; }
48
50
  if (e.type === 'queue-operation' || e.type === 'last-prompt') return null;
49
51
  if (e.type === 'user' && e.isMeta) return null;
50
- const cwd = e.cwd || process.cwd();
52
+
53
+ // Resolve workingDirectory: event cwd → sessions-index.json → decoded path
54
+ let cwd = e.cwd || null;
55
+ if (!cwd && fp) {
56
+ const projectDir = path.dirname(fp);
57
+ // Try sessions-index.json first (reliable, no ambiguity)
58
+ try {
59
+ const idx = JSON.parse(fs.readFileSync(path.join(projectDir, 'sessions-index.json'), 'utf-8'));
60
+ if (idx.originalPath) cwd = idx.originalPath;
61
+ } catch (_) {}
62
+ // Fallback: decode encoded directory name (replace leading '-' with '/', rest '-' → '/')
63
+ if (!cwd) {
64
+ const dirName = path.basename(projectDir);
65
+ if (dirName.startsWith('-')) cwd = '/' + dirName.slice(1).replace(/-/g, '/');
66
+ }
67
+ }
68
+ cwd = cwd || process.cwd();
69
+
51
70
  const branch = e.gitBranch || '';
52
71
  const base = path.basename(cwd);
53
72
  const title = branch ? `${branch} @ ${base}` : base;
@@ -3,9 +3,17 @@ import { JsonlWatcher as CCFWatcher } from 'ccfollow';
3
3
  import { JsonlParser } from './jsonl-parser.js';
4
4
 
5
5
  export class JsonlWatcher extends CCFWatcher {
6
- constructor({ broadcastSync, queries, ownedSessionIds }) {
6
+ constructor({ broadcastSync, queries }) {
7
7
  super();
8
- this._parser = new JsonlParser({ broadcastSync, queries, ownedSessionIds });
8
+ this._parser = new JsonlParser({ broadcastSync, queries });
9
+ this._currentFp = null;
10
+ }
11
+
12
+ /** Override _read to capture the current file path for project-folder resolution in _conv */
13
+ _read(fp) {
14
+ this._currentFp = fp;
15
+ super._read(fp);
16
+ this._currentFp = null;
9
17
  }
10
18
 
11
19
  _line(line) {
@@ -14,10 +22,19 @@ export class JsonlWatcher extends CCFWatcher {
14
22
  let e;
15
23
  try { e = JSON.parse(line); } catch (_) { return; }
16
24
  if (!e || !e.sessionId) return;
17
- const cid = this._parser._conv(e.sessionId, e);
25
+ const cid = this._parser._conv(e.sessionId, e, this._currentFp);
18
26
  if (cid) this._parser._route(cid, e.sessionId, e);
19
27
  }
20
28
 
29
+ /**
30
+ * Pre-register a GUI-spawned session so the parser uses the correct existing
31
+ * conversation and session ID rather than creating duplicates.
32
+ * Call this as soon as session_id is obtained from Claude stdout.
33
+ */
34
+ registerSession(claudeSessionId, convId, dbSessionId) {
35
+ this._parser.registerSession(claudeSessionId, convId, dbSessionId);
36
+ }
37
+
21
38
  stop() {
22
39
  super.stop();
23
40
  this._parser.endAllStreaming();
@@ -1,4 +1,4 @@
1
- export function createProcessMessage({ queries, activeExecutions, rateLimitState, execMachine, broadcastSync, runClaudeWithStreaming, cleanupExecution, checkpointManager, discoveredAgents, ownedSessionIds, STARTUP_CWD, buildSystemPrompt, parseRateLimitResetTime, eagerTTS, touchACP, createChunkBatcher, debugLog, logError, scheduleRetry, drainMessageQueue, createEventHandler }) {
1
+ export function createProcessMessage({ queries, activeExecutions, rateLimitState, execMachine, broadcastSync, runClaudeWithStreaming, cleanupExecution, checkpointManager, discoveredAgents, STARTUP_CWD, buildSystemPrompt, parseRateLimitResetTime, eagerTTS, touchACP, getJsonlWatcher, debugLog, logError, scheduleRetry, drainMessageQueue, createEventHandler }) {
2
2
  async function processMessageWithStreaming(conversationId, messageId, sessionId, content, agentId, model, subAgent) {
3
3
  const startTime = Date.now();
4
4
  touchACP(agentId);
@@ -27,12 +27,10 @@ export function createProcessMessage({ queries, activeExecutions, rateLimitState
27
27
  execMachine.send(conversationId, { type: 'START', sessionId });
28
28
  queries.setIsStreaming(conversationId, true);
29
29
  queries.updateSession(sessionId, { status: 'active' });
30
- const batcher = createChunkBatcher(queries, debugLog);
31
30
  const cwd = conv?.workingDirectory || STARTUP_CWD;
32
- const allBlocksRef = { val: [] };
33
- const currentSequenceRef = { val: queries.getMaxSequence(sessionId) ?? -1 };
34
- const batcherRef = { batcher, eventCount: 0, resumeSessionId: conv?.claudeSessionId || null };
35
- const onEvent = createEventHandler({ queries, activeExecutions, broadcastSync, rateLimitState, batcherRef, sessionId, conversationId, messageId, content, agentId, model, subAgent, ownedSessionIds, allBlocksRef, currentSequenceRef, scheduleRetry, eagerTTS, debugLog, parseRateLimitResetTime });
31
+ // stateRef tracks eventCount (for session response metadata) and resumeSessionId
32
+ const stateRef = { eventCount: 0, resumeSessionId: conv?.claudeSessionId || null };
33
+ const onEvent = createEventHandler({ queries, activeExecutions, broadcastSync, rateLimitState, batcherRef: stateRef, sessionId, conversationId, messageId, content, agentId, model, subAgent, getJsonlWatcher, scheduleRetry, eagerTTS, debugLog, parseRateLimitResetTime });
36
34
  try {
37
35
  debugLog(`[stream] Starting: conversationId=${conversationId}, sessionId=${sessionId}`);
38
36
  let resolvedAgentId = agentId || 'claude-code';
@@ -42,7 +40,7 @@ export function createProcessMessage({ queries, activeExecutions, rateLimitState
42
40
  const resolvedSubAgent = subAgent || conv?.subAgent || null;
43
41
  const config = {
44
42
  verbose: true, outputFormat: 'stream-json', timeout: 1800000, print: true,
45
- resumeSessionId: batcherRef.resumeSessionId,
43
+ resumeSessionId: stateRef.resumeSessionId,
46
44
  systemPrompt: buildSystemPrompt(agentId, resolvedModel, resolvedSubAgent),
47
45
  model: resolvedModel || undefined, subAgent: resolvedSubAgent || undefined, onEvent,
48
46
  onPid: (pid) => { const e = activeExecutions.get(conversationId); if (e) e.pid = pid; execMachine.send(conversationId, { type: 'SET_PID', pid }); },
@@ -55,17 +53,16 @@ export function createProcessMessage({ queries, activeExecutions, rateLimitState
55
53
  }
56
54
  activeExecutions.delete(conversationId);
57
55
  execMachine.send(conversationId, { type: 'COMPLETE' });
58
- batcher.drain();
59
- if (claudeSessionId) ownedSessionIds.delete(claudeSessionId);
60
56
  debugLog(`[stream] Claude returned ${outputs.length} outputs, sessionId=${claudeSessionId}`);
61
- queries.updateSession(sessionId, { status: 'complete', response: JSON.stringify({ outputs, eventCount: batcherRef.eventCount }), completed_at: Date.now() });
62
- broadcastSync({ type: 'streaming_complete', sessionId, conversationId, agentId, eventCount: batcherRef.eventCount, seq: currentSequenceRef.val, timestamp: Date.now() });
63
- debugLog(`[stream] Completed: ${outputs.length} outputs, ${batcherRef.eventCount} events`);
57
+ queries.updateSession(sessionId, { status: 'complete', response: JSON.stringify({ outputs, eventCount: stateRef.eventCount }), completed_at: Date.now() });
58
+ // streaming_complete is broadcast by JsonlParser when it sees the turn_duration event.
59
+ // We still broadcast here as a fallback for cases where the file watcher may not have
60
+ // processed the final event yet (e.g., ACP agents that don't write JSONL files).
61
+ broadcastSync({ type: 'streaming_complete', sessionId, conversationId, agentId, eventCount: stateRef.eventCount, timestamp: Date.now() });
62
+ debugLog(`[stream] Completed: ${outputs.length} outputs, ${stateRef.eventCount} events`);
64
63
  } catch (error) {
65
64
  const elapsed = Date.now() - startTime;
66
65
  debugLog(`[stream] Error after ${elapsed}ms: ${error.message}`);
67
- const conv2 = queries.getConversation(conversationId);
68
- if (conv2?.claudeSessionId) ownedSessionIds.delete(conv2.claudeSessionId);
69
66
  if (rateLimitState.get(conversationId)?.isStreamDetected) {
70
67
  debugLog(`[rate-limit] Rate limit already handled in stream for conv ${conversationId}, skipping catch handler`);
71
68
  return;
@@ -79,7 +76,6 @@ export function createProcessMessage({ queries, activeExecutions, rateLimitState
79
76
  const errMsg = queries.createMessage(conversationId, 'assistant', `Error: Authentication failed. ${error.message}. Please update your credentials and try again.`);
80
77
  broadcastSync({ type: 'message_created', conversationId, message: errMsg, timestamp: Date.now() });
81
78
  queries.setIsStreaming(conversationId, false);
82
- batcher.drain();
83
79
  activeExecutions.delete(conversationId);
84
80
  return;
85
81
  }
@@ -98,7 +94,6 @@ export function createProcessMessage({ queries, activeExecutions, rateLimitState
98
94
  const retryAt = Date.now() + cooldownMs;
99
95
  rateLimitState.set(conversationId, { retryAt, cooldownMs, retryCount });
100
96
  broadcastSync({ type: 'rate_limit_hit', sessionId, conversationId, retryAfterMs: cooldownMs, retryAt, retryCount, timestamp: Date.now() });
101
- batcher.drain();
102
97
  debugLog(`[rate-limit] Scheduling retry for conv ${conversationId} in ${cooldownMs}ms (attempt ${retryCount + 1})`);
103
98
  setTimeout(() => {
104
99
  debugLog(`[rate-limit] Timeout fired for conv ${conversationId}, calling scheduleRetry`);
@@ -108,14 +103,13 @@ export function createProcessMessage({ queries, activeExecutions, rateLimitState
108
103
  }, cooldownMs);
109
104
  return;
110
105
  }
111
- const isSessionConflict = error.exitCode === null && batcherRef.eventCount === 0;
106
+ const isSessionConflict = error.exitCode === null && stateRef.eventCount === 0;
112
107
  broadcastSync({ type: 'streaming_error', sessionId, conversationId, error: error.message, isPrematureEnd: error.isPrematureEnd || false, exitCode: error.exitCode, stderrText: error.stderrText, recoverable: elapsed < 60000, isSessionConflict, timestamp: Date.now() });
113
108
  if (!isSessionConflict) {
114
109
  const errMsg = queries.createMessage(conversationId, 'assistant', `Error: ${error.message}`);
115
110
  broadcastSync({ type: 'message_created', conversationId, message: errMsg, timestamp: Date.now() });
116
111
  }
117
112
  } finally {
118
- batcher.drain();
119
113
  if (!rateLimitState.has(conversationId)) {
120
114
  cleanupExecution(conversationId);
121
115
  drainMessageQueue(conversationId);
@@ -1,6 +1,6 @@
1
1
  import { JsonlWatcher } from './jsonl-watcher.js';
2
2
 
3
- export function createOnServerReady({ queries, broadcastSync, warmAssetCache, staticDir, toolManager, discoveredAgents, PORT, BASE_URL, watch, ownedSessionIds, resumeInterruptedStreams, activeExecutions, debugLog, installGMAgentConfigs, startACPTools, getACPStatus, execMachine, toolInstallMachine, getSpeech, ensureModelsDownloaded, performAutoImport, performAgentHealthCheck, pm2Manager, pm2Subscribers, recoverStaleSessions }) {
3
+ export function createOnServerReady({ queries, broadcastSync, warmAssetCache, staticDir, toolManager, discoveredAgents, PORT, BASE_URL, watch, setWatcher, resumeInterruptedStreams, activeExecutions, debugLog, installGMAgentConfigs, startACPTools, getACPStatus, execMachine, toolInstallMachine, getSpeech, ensureModelsDownloaded, performAutoImport, performAgentHealthCheck, pm2Manager, pm2Subscribers, recoverStaleSessions }) {
4
4
  let jsonlWatcher = null;
5
5
 
6
6
  function getJsonlWatcher() { return jsonlWatcher; }
@@ -23,8 +23,9 @@ export function createOnServerReady({ queries, broadcastSync, warmAssetCache, st
23
23
  }, 6 * 60 * 60 * 1000);
24
24
 
25
25
  try {
26
- jsonlWatcher = new JsonlWatcher({ broadcastSync, queries, ownedSessionIds });
26
+ jsonlWatcher = new JsonlWatcher({ broadcastSync, queries });
27
27
  jsonlWatcher.start();
28
+ if (setWatcher) setWatcher(jsonlWatcher);
28
29
  console.log('[JSONL] Watcher started');
29
30
  } catch (err) { console.error('[JSONL] Watcher failed to start:', err.message); }
30
31
 
@@ -1,40 +1,46 @@
1
- export function createEventHandler({ queries, activeExecutions, broadcastSync, rateLimitState, batcherRef, sessionId, conversationId, messageId, content, agentId, model, subAgent, ownedSessionIds, allBlocksRef, currentSequenceRef, scheduleRetry, eagerTTS, debugLog, parseRateLimitResetTime }) {
1
+ /**
2
+ * Minimal Claude stdout event handler.
3
+ *
4
+ * Now that JsonlParser owns all event broadcasting (streaming_start,
5
+ * streaming_progress, streaming_complete) via the JSONL file watcher,
6
+ * this handler only needs to:
7
+ * 1. Extract session_id → call setClaudeSessionId + registerSession so the
8
+ * parser links the file to the correct existing conversation/session.
9
+ * 2. Detect inline rate-limit messages in text/result blocks → scheduleRetry.
10
+ *
11
+ * No batcher, no broadcastSync for individual blocks.
12
+ */
13
+ export function createEventHandler({ queries, activeExecutions, broadcastSync, rateLimitState, batcherRef, sessionId, conversationId, messageId, content, agentId, model, subAgent, getJsonlWatcher, scheduleRetry, eagerTTS, debugLog, parseRateLimitResetTime }) {
2
14
  return function onEvent(parsed) {
3
15
  batcherRef.eventCount++;
4
16
  const entry = activeExecutions.get(conversationId);
5
17
  if (entry) entry.lastActivity = Date.now();
18
+
19
+ // Register session with file watcher as soon as we see the session_id.
20
+ // This pre-maps claudeSessionId → (convId, dbSessionId) in JsonlParser before
21
+ // the 16ms file-watcher debounce fires, preventing duplicate conversation creation.
6
22
  if (parsed.session_id) {
7
- ownedSessionIds.add(parsed.session_id);
8
23
  if (!batcherRef.resumeSessionId || batcherRef.resumeSessionId !== parsed.session_id) {
9
24
  batcherRef.resumeSessionId = parsed.session_id;
10
25
  queries.setClaudeSessionId(conversationId, parsed.session_id, sessionId);
26
+ try { getJsonlWatcher()?.registerSession(parsed.session_id, conversationId, sessionId); } catch (_) {}
11
27
  }
12
28
  }
29
+
13
30
  debugLog(`[stream] Event ${batcherRef.eventCount}: type=${parsed.type}`);
14
31
 
15
- if (parsed.type === 'system') {
16
- if (parsed.subtype === 'task_notification') return;
17
- if (!parsed.model && !parsed.cwd && !parsed.tools) return;
18
- const block = { type: 'system', subtype: parsed.subtype, model: parsed.model, cwd: parsed.cwd, tools: parsed.tools, session_id: parsed.session_id };
19
- currentSequenceRef.val++;
20
- batcherRef.batcher.add(sessionId, conversationId, currentSequenceRef.val, 'system', block);
21
- broadcastSync({ type: 'streaming_progress', sessionId, conversationId, block, blockRole: 'system', blockIndex: allBlocksRef.val.length, seq: currentSequenceRef.val, timestamp: Date.now() });
22
- } else if (parsed.type === 'assistant' && parsed.message?.content) {
32
+ // Rate-limit detection in assistant text blocks
33
+ if (parsed.type === 'assistant' && parsed.message?.content) {
23
34
  for (const block of parsed.message.content) {
24
- allBlocksRef.val.push(block);
25
- currentSequenceRef.val++;
26
- batcherRef.batcher.add(sessionId, conversationId, currentSequenceRef.val, block.type || 'assistant', block);
27
- broadcastSync({ type: 'streaming_progress', sessionId, conversationId, block, blockRole: 'assistant', blockIndex: allBlocksRef.val.length - 1, seq: currentSequenceRef.val, timestamp: Date.now() });
28
35
  if (block.type === 'text' && block.text) {
29
36
  const rateLimitMatch = block.text.match(/you'?ve hit your limit|rate limit exceeded/i);
30
37
  if (rateLimitMatch) {
31
38
  debugLog(`[rate-limit] Detected rate limit message in stream for conv ${conversationId}`);
32
39
  const retryAfterSec = parseRateLimitResetTime(block.text);
33
40
  const entry2 = activeExecutions.get(conversationId);
34
- if (entry2 && entry2.pid) { try { process.kill(entry2.pid); } catch (e) {} }
41
+ if (entry2 && entry2.pid) { try { process.kill(entry2.pid); } catch (_) {} }
35
42
  const existingCount = rateLimitState.get(conversationId)?.retryCount || 0;
36
43
  if (existingCount >= 3) {
37
- batcherRef.batcher.drain();
38
44
  activeExecutions.delete(conversationId);
39
45
  queries.setIsStreaming(conversationId, false);
40
46
  const errMsg = queries.createMessage(conversationId, 'assistant', `Error: Rate limit exceeded after ${existingCount + 1} attempts. Please try again later.`);
@@ -44,7 +50,6 @@ export function createEventHandler({ queries, activeExecutions, broadcastSync, r
44
50
  }
45
51
  rateLimitState.set(conversationId, { retryAt: Date.now() + (retryAfterSec * 1000), cooldownMs: retryAfterSec * 1000, retryCount: existingCount + 1, isStreamDetected: true });
46
52
  broadcastSync({ type: 'rate_limit_hit', sessionId, conversationId, retryAfterMs: retryAfterSec * 1000, retryAt: Date.now() + (retryAfterSec * 1000), retryCount: 1, timestamp: Date.now() });
47
- batcherRef.batcher.drain();
48
53
  activeExecutions.delete(conversationId);
49
54
  queries.setIsStreaming(conversationId, false);
50
55
  setTimeout(() => {
@@ -57,59 +62,38 @@ export function createEventHandler({ queries, activeExecutions, broadcastSync, r
57
62
  eagerTTS(block.text, conversationId, sessionId);
58
63
  }
59
64
  }
60
- } else if (parsed.type === 'user' && parsed.message?.content) {
61
- for (const block of parsed.message.content) {
62
- if (block.type === 'tool_result') {
63
- const toolResultBlock = { type: 'tool_result', tool_use_id: block.tool_use_id, content: typeof block.content === 'string' ? block.content : JSON.stringify(block.content), is_error: block.is_error || false };
64
- currentSequenceRef.val++;
65
- batcherRef.batcher.add(sessionId, conversationId, currentSequenceRef.val, 'tool_result', toolResultBlock);
66
- broadcastSync({ type: 'streaming_progress', sessionId, conversationId, block: toolResultBlock, blockRole: 'tool_result', blockIndex: allBlocksRef.val.length, seq: currentSequenceRef.val, timestamp: Date.now() });
67
- }
68
- }
69
- } else if (parsed.type === 'result') {
70
- const resultBlock = { type: 'result', subtype: parsed.subtype, duration_ms: parsed.duration_ms, total_cost_usd: parsed.total_cost_usd, num_turns: parsed.num_turns, is_error: parsed.is_error || false, result: parsed.result };
71
- currentSequenceRef.val++;
72
- batcherRef.batcher.add(sessionId, conversationId, currentSequenceRef.val, 'result', resultBlock);
73
- broadcastSync({ type: 'streaming_progress', sessionId, conversationId, block: resultBlock, blockRole: 'result', blockIndex: allBlocksRef.val.length, isResult: true, seq: currentSequenceRef.val, timestamp: Date.now() });
74
- if (parsed.result) {
75
- const resultText = typeof parsed.result === 'string' ? parsed.result : JSON.stringify(parsed.result);
76
- const rlMatch = resultText.match(/you'?ve hit your limit|rate limit exceeded/i);
77
- if (rlMatch) {
78
- debugLog(`[rate-limit] Detected rate limit in result for conv ${conversationId}`);
79
- const retryAfterSec = parseRateLimitResetTime(resultText);
80
- const entry3 = activeExecutions.get(conversationId);
81
- if (entry3 && entry3.pid) { try { process.kill(entry3.pid); } catch (e) {} }
82
- const existingCount2 = rateLimitState.get(conversationId)?.retryCount || 0;
83
- if (existingCount2 >= 3) {
84
- batcherRef.batcher.drain();
85
- activeExecutions.delete(conversationId);
86
- queries.setIsStreaming(conversationId, false);
87
- const errMsg2 = queries.createMessage(conversationId, 'assistant', `Error: Rate limit exceeded after ${existingCount2 + 1} attempts. Please try again later.`);
88
- broadcastSync({ type: 'message_created', conversationId, message: errMsg2, timestamp: Date.now() });
89
- broadcastSync({ type: 'streaming_complete', sessionId, conversationId, interrupted: true, timestamp: Date.now() });
90
- return;
91
- }
92
- rateLimitState.set(conversationId, { retryAt: Date.now() + (retryAfterSec * 1000), cooldownMs: retryAfterSec * 1000, retryCount: existingCount2 + 1, isStreamDetected: true });
93
- broadcastSync({ type: 'rate_limit_hit', sessionId, conversationId, retryAfterMs: retryAfterSec * 1000, retryAt: Date.now() + (retryAfterSec * 1000), retryCount: existingCount2 + 1, timestamp: Date.now() });
94
- batcherRef.batcher.drain();
65
+ }
66
+
67
+ // Rate-limit detection in result blocks
68
+ if (parsed.type === 'result' && parsed.result) {
69
+ const resultText = typeof parsed.result === 'string' ? parsed.result : JSON.stringify(parsed.result);
70
+ const rlMatch = resultText.match(/you'?ve hit your limit|rate limit exceeded/i);
71
+ if (rlMatch) {
72
+ debugLog(`[rate-limit] Detected rate limit in result for conv ${conversationId}`);
73
+ const retryAfterSec = parseRateLimitResetTime(resultText);
74
+ const entry3 = activeExecutions.get(conversationId);
75
+ if (entry3 && entry3.pid) { try { process.kill(entry3.pid); } catch (_) {} }
76
+ const existingCount2 = rateLimitState.get(conversationId)?.retryCount || 0;
77
+ if (existingCount2 >= 3) {
95
78
  activeExecutions.delete(conversationId);
96
79
  queries.setIsStreaming(conversationId, false);
97
- setTimeout(() => {
98
- rateLimitState.delete(conversationId);
99
- broadcastSync({ type: 'rate_limit_clear', conversationId, timestamp: Date.now() });
100
- scheduleRetry(conversationId, messageId, content, agentId, model, subAgent);
101
- }, retryAfterSec * 1000);
80
+ const errMsg2 = queries.createMessage(conversationId, 'assistant', `Error: Rate limit exceeded after ${existingCount2 + 1} attempts. Please try again later.`);
81
+ broadcastSync({ type: 'message_created', conversationId, message: errMsg2, timestamp: Date.now() });
82
+ broadcastSync({ type: 'streaming_complete', sessionId, conversationId, interrupted: true, timestamp: Date.now() });
102
83
  return;
103
84
  }
104
- if (resultText) eagerTTS(resultText, conversationId, sessionId);
85
+ rateLimitState.set(conversationId, { retryAt: Date.now() + (retryAfterSec * 1000), cooldownMs: retryAfterSec * 1000, retryCount: existingCount2 + 1, isStreamDetected: true });
86
+ broadcastSync({ type: 'rate_limit_hit', sessionId, conversationId, retryAfterMs: retryAfterSec * 1000, retryAt: Date.now() + (retryAfterSec * 1000), retryCount: existingCount2 + 1, timestamp: Date.now() });
87
+ activeExecutions.delete(conversationId);
88
+ queries.setIsStreaming(conversationId, false);
89
+ setTimeout(() => {
90
+ rateLimitState.delete(conversationId);
91
+ broadcastSync({ type: 'rate_limit_clear', conversationId, timestamp: Date.now() });
92
+ scheduleRetry(conversationId, messageId, content, agentId, model, subAgent);
93
+ }, retryAfterSec * 1000);
94
+ return;
105
95
  }
106
- if (parsed.result && allBlocksRef.val.length === 0) allBlocksRef.val.push({ type: 'text', text: String(parsed.result) });
107
- } else if (parsed.type === 'tool_status') {
108
- broadcastSync({ type: 'streaming_progress', sessionId, conversationId, block: { type: 'tool_status', tool_use_id: parsed.tool_use_id, status: parsed.status }, seq: currentSequenceRef.val, timestamp: Date.now() });
109
- } else if (parsed.type === 'usage') {
110
- broadcastSync({ type: 'streaming_progress', sessionId, conversationId, block: { type: 'usage', usage: parsed.usage }, seq: currentSequenceRef.val, timestamp: Date.now() });
111
- } else if (parsed.type === 'plan') {
112
- broadcastSync({ type: 'streaming_progress', sessionId, conversationId, block: { type: 'plan', entries: parsed.entries }, seq: currentSequenceRef.val, timestamp: Date.now() });
96
+ if (resultText) eagerTTS(resultText, conversationId, sessionId);
113
97
  }
114
98
  };
115
99
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.854",
3
+ "version": "1.0.855",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
package/server.js CHANGED
@@ -26,7 +26,7 @@ const sendWs = (ws, obj) => { if (ws.readyState === 1) ws.send(wsEncode(obj)); }
26
26
  import { startAll as startACPTools, stopAll as stopACPTools, getStatus as getACPStatus, getPort as getACPPort, ensureRunning, queryModels as queryACPModels, touch as touchACP } from './lib/acp-sdk-manager.js';
27
27
  import * as execMachine from './lib/execution-machine.js';
28
28
  import * as toolInstallMachine from './lib/tool-install-machine.js';
29
- import { _assetCache, htmlState, generateETag, warmAssetCache, serveFile as _serveFile, createChunkBatcher } from './lib/asset-server.js';
29
+ import { _assetCache, htmlState, generateETag, warmAssetCache, serveFile as _serveFile } from './lib/asset-server.js';
30
30
  import { installGMAgentConfigs } from './lib/gm-agent-configs.js';
31
31
  import * as toolManager from './lib/tool-manager.js';
32
32
  import { pm2Manager } from './lib/pm2-manager.js';
@@ -53,7 +53,7 @@ const activeExecutions = new Map();
53
53
  const activeScripts = new Map();
54
54
  const messageQueues = new Map();
55
55
  const rateLimitState = new Map();
56
- const ownedSessionIds = new Set();
56
+ let _jsonlWatcher = null;
57
57
  const activeProcessesByRunId = new Map();
58
58
  const checkpointManager = new CheckpointManager(queries);
59
59
  const STUCK_AGENT_THRESHOLD_MS = 1800000;
@@ -121,8 +121,9 @@ const { scheduleRetry, drainMessageQueue } = createMessageQueue({ queries, messa
121
121
  const { processMessageWithStreaming } = createProcessMessage({
122
122
  queries, activeExecutions, rateLimitState, execMachine,
123
123
  broadcastSync, runClaudeWithStreaming, cleanupExecution, checkpointManager,
124
- discoveredAgents, ownedSessionIds, STARTUP_CWD, buildSystemPrompt,
125
- parseRateLimitResetTime, eagerTTS, touchACP, createChunkBatcher,
124
+ discoveredAgents, STARTUP_CWD, buildSystemPrompt,
125
+ parseRateLimitResetTime, eagerTTS, touchACP,
126
+ getJsonlWatcher: () => _jsonlWatcher,
126
127
  debugLog, logError,
127
128
  scheduleRetry, drainMessageQueue, createEventHandler
128
129
  });
@@ -187,7 +188,7 @@ setInterval(performDbRecovery, 300000);
187
188
 
188
189
  const { onServerReady, getJsonlWatcher } = createOnServerReady({
189
190
  queries, broadcastSync, warmAssetCache, staticDir, toolManager, discoveredAgents,
190
- PORT, BASE_URL, watch, ownedSessionIds, resumeInterruptedStreams, activeExecutions,
191
+ PORT, BASE_URL, watch, setWatcher: (w) => { _jsonlWatcher = w; }, resumeInterruptedStreams, activeExecutions,
191
192
  debugLog, installGMAgentConfigs, startACPTools, getACPStatus, execMachine,
192
193
  toolInstallMachine, getSpeech, ensureModelsDownloaded, performAutoImport,
193
194
  performAgentHealthCheck, pm2Manager, pm2Subscribers, recoverStaleSessions