agentgui 1.0.857 → 1.0.859

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.
@@ -76,6 +76,10 @@ export function addToolQueries(q, db, prep, generateId) {
76
76
 
77
77
  q.addToolInstallHistory = function(toolId, action, status, error) {
78
78
  const now = Date.now();
79
+ // Ensure the parent tool_installations row exists before inserting history
80
+ // (handles tools added after the DB was first initialized)
81
+ prep(`INSERT OR IGNORE INTO tool_installations (id, tool_id, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?)`)
82
+ .run(generateId('tinst'), toolId, 'not_installed', now, now);
79
83
  const stmt = prep(`
80
84
  INSERT INTO tool_install_history
81
85
  (id, tool_id, action, started_at, completed_at, status, error_message, created_at)
@@ -3,7 +3,7 @@ import path from 'path';
3
3
  import os from 'os';
4
4
  import crypto from 'crypto';
5
5
 
6
- export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, serveFile, staticDir, messageQueues, wss, activeExecutions, getACPStatus, discoveredAgents, PKG_VERSION, RATE_LIMIT_MAX, rateLimitMap, routes, handleGeminiOAuthCallback, handleCodexOAuthCallback, PORT }) {
6
+ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, serveFile, staticDir, messageQueues, getWss, activeExecutions, getACPStatus, discoveredAgents, PKG_VERSION, RATE_LIMIT_MAX, rateLimitMap, routes, handleGeminiOAuthCallback, handleCodexOAuthCallback, PORT }) {
7
7
  return async function httpHandler(req, res) {
8
8
  res.setHeader('Access-Control-Allow-Origin', '*');
9
9
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
@@ -64,7 +64,7 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
64
64
  try { queries._db.prepare('SELECT 1').get(); } catch (e) { dbStatus = { ok: false, error: e.message }; }
65
65
  const queueSizes = {};
66
66
  for (const [k, v] of messageQueues) queueSizes[k] = v.length;
67
- sendJSON(req, res, 200, { status: 'ok', version: PKG_VERSION, uptime: process.uptime(), agents: discoveredAgents.length, activeExecutions: activeExecutions.size, wsClients: wss.clients.size, memory: process.memoryUsage(), acp: getACPStatus(), db: dbStatus, queueSizes });
67
+ sendJSON(req, res, 200, { status: 'ok', version: PKG_VERSION, uptime: process.uptime(), agents: discoveredAgents.length, activeExecutions: activeExecutions.size, wsClients: getWss()?.clients?.size ?? 0, memory: process.memoryUsage(), acp: getACPStatus(), db: dbStatus, queueSizes });
68
68
  return;
69
69
  }
70
70
 
@@ -1,19 +1,9 @@
1
1
  import path from 'path';
2
- <<<<<<< HEAD
3
-
4
- export class JsonlParser {
5
- constructor({ broadcastSync, queries, ownedSessionIds }) {
6
- this._bc = broadcastSync;
7
- this._q = queries;
8
- this._owned = ownedSessionIds;
9
- =======
10
- import fs from 'fs';
11
2
 
12
3
  export class JsonlParser {
13
4
  constructor({ broadcastSync, queries }) {
14
5
  this._bc = broadcastSync;
15
6
  this._q = queries;
16
- >>>>>>> 6bfde951cbeb65ec72b73da9c23b9c8c0ba0bbc1
17
7
  this._convMap = new Map();
18
8
  this._emitted = new Map();
19
9
  this._seqs = new Map();
@@ -21,8 +11,6 @@ export class JsonlParser {
21
11
  this._sessions = new Map();
22
12
  }
23
13
 
24
- <<<<<<< HEAD
25
- =======
26
14
  /**
27
15
  * Pre-register a GUI-spawned session so _conv finds the right conversation
28
16
  * and _dbSession reuses the existing session ID instead of creating a new one.
@@ -34,7 +22,6 @@ export class JsonlParser {
34
22
  if (dbSessionId) this._sessions.set(claudeSessionId, dbSessionId);
35
23
  }
36
24
 
37
- >>>>>>> 6bfde951cbeb65ec72b73da9c23b9c8c0ba0bbc1
38
25
  clear() {
39
26
  this._convMap.clear();
40
27
  this._emitted.clear();
@@ -55,47 +42,23 @@ export class JsonlParser {
55
42
  for (const sid of [...this._streaming]) this._endStreaming(this._convMap.get(sid), sid);
56
43
  }
57
44
 
58
- <<<<<<< HEAD
59
- _line(fp, line) {
60
- line = line.trim(); if (!line) return;
61
- let e; try { e = JSON.parse(line); } catch (_) { return; }
62
- if (!e || !e.sessionId) return;
63
- if (this._owned?.has(e.sessionId)) return;
64
- const cid = this._conv(e.sessionId, e, fp);
65
- if (cid) this._route(cid, e.sessionId, e);
66
- }
67
-
68
- _conv(sid, e) {
69
- =======
70
45
  _conv(sid, e, fp) {
71
- >>>>>>> 6bfde951cbeb65ec72b73da9c23b9c8c0ba0bbc1
72
46
  if (this._convMap.has(sid)) return this._convMap.get(sid);
73
47
  const found = this._q.getConversations().find(c => c.claudeSessionId === sid);
74
48
  if (found) { this._convMap.set(sid, found.id); return found.id; }
75
49
  if (e.type === 'queue-operation' || e.type === 'last-prompt') return null;
76
50
  if (e.type === 'user' && e.isMeta) return null;
77
- <<<<<<< HEAD
78
- const cwd = e.cwd || process.cwd();
79
- =======
80
51
 
81
- // Resolve workingDirectory: event cwd → sessions-index.json decoded path
52
+ // Resolve workingDirectory: event cwd → decode encoded directory name
82
53
  let cwd = e.cwd || null;
83
54
  if (!cwd && fp) {
84
55
  const projectDir = path.dirname(fp);
85
- // Try sessions-index.json first (reliable, no ambiguity)
86
- try {
87
- const idx = JSON.parse(fs.readFileSync(path.join(projectDir, 'sessions-index.json'), 'utf-8'));
88
- if (idx.originalPath) cwd = idx.originalPath;
89
- } catch (_) {}
90
- // Fallback: decode encoded directory name (replace leading '-' with '/', rest '-' → '/')
91
- if (!cwd) {
92
- const dirName = path.basename(projectDir);
93
- if (dirName.startsWith('-')) cwd = '/' + dirName.slice(1).replace(/-/g, '/');
94
- }
56
+ // Decode encoded directory name (replace leading '-' with '/', rest '-' → '/')
57
+ const dirName = path.basename(projectDir);
58
+ if (dirName.startsWith('-')) cwd = '/' + dirName.slice(1).replace(/-/g, '/');
95
59
  }
96
60
  cwd = cwd || process.cwd();
97
61
 
98
- >>>>>>> 6bfde951cbeb65ec72b73da9c23b9c8c0ba0bbc1
99
62
  const branch = e.gitBranch || '';
100
63
  const base = path.basename(cwd);
101
64
  const title = branch ? `${branch} @ ${base}` : base;
@@ -3,11 +3,6 @@ import { JsonlWatcher as CCFWatcher } from 'ccfollow';
3
3
  import { JsonlParser } from './jsonl-parser.js';
4
4
 
5
5
  export class JsonlWatcher extends CCFWatcher {
6
- <<<<<<< HEAD
7
- constructor({ broadcastSync, queries, ownedSessionIds }) {
8
- super();
9
- this._parser = new JsonlParser({ broadcastSync, queries, ownedSessionIds });
10
- =======
11
6
  constructor({ broadcastSync, queries }) {
12
7
  super();
13
8
  this._parser = new JsonlParser({ broadcastSync, queries });
@@ -19,7 +14,6 @@ export class JsonlWatcher extends CCFWatcher {
19
14
  this._currentFp = fp;
20
15
  super._read(fp);
21
16
  this._currentFp = null;
22
- >>>>>>> 6bfde951cbeb65ec72b73da9c23b9c8c0ba0bbc1
23
17
  }
24
18
 
25
19
  _line(line) {
@@ -28,12 +22,6 @@ export class JsonlWatcher extends CCFWatcher {
28
22
  let e;
29
23
  try { e = JSON.parse(line); } catch (_) { return; }
30
24
  if (!e || !e.sessionId) return;
31
- <<<<<<< HEAD
32
- const cid = this._parser._conv(e.sessionId, e);
33
- if (cid) this._parser._route(cid, e.sessionId, e);
34
- }
35
-
36
- =======
37
25
  const cid = this._parser._conv(e.sessionId, e, this._currentFp);
38
26
  if (cid) this._parser._route(cid, e.sessionId, e);
39
27
  }
@@ -47,7 +35,6 @@ export class JsonlWatcher extends CCFWatcher {
47
35
  this._parser.registerSession(claudeSessionId, convId, dbSessionId);
48
36
  }
49
37
 
50
- >>>>>>> 6bfde951cbeb65ec72b73da9c23b9c8c0ba0bbc1
51
38
  stop() {
52
39
  super.stop();
53
40
  this._parser.endAllStreaming();
@@ -1,8 +1,4 @@
1
- <<<<<<< HEAD
2
- 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 }) {
3
- =======
4
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 }) {
5
- >>>>>>> 6bfde951cbeb65ec72b73da9c23b9c8c0ba0bbc1
6
2
  async function processMessageWithStreaming(conversationId, messageId, sessionId, content, agentId, model, subAgent) {
7
3
  const startTime = Date.now();
8
4
  touchACP(agentId);
@@ -31,33 +27,24 @@ export function createProcessMessage({ queries, activeExecutions, rateLimitState
31
27
  execMachine.send(conversationId, { type: 'START', sessionId });
32
28
  queries.setIsStreaming(conversationId, true);
33
29
  queries.updateSession(sessionId, { status: 'active' });
34
- <<<<<<< HEAD
35
- const batcher = createChunkBatcher(queries, debugLog);
36
- const cwd = conv?.workingDirectory || STARTUP_CWD;
37
- const allBlocksRef = { val: [] };
38
- const currentSequenceRef = { val: queries.getMaxSequence(sessionId) ?? -1 };
39
- const batcherRef = { batcher, eventCount: 0, resumeSessionId: conv?.claudeSessionId || null };
40
- const onEvent = createEventHandler({ queries, activeExecutions, broadcastSync, rateLimitState, batcherRef, sessionId, conversationId, messageId, content, agentId, model, subAgent, ownedSessionIds, allBlocksRef, currentSequenceRef, scheduleRetry, eagerTTS, debugLog, parseRateLimitResetTime });
41
- =======
42
30
  const cwd = conv?.workingDirectory || STARTUP_CWD;
31
+ // Resolve agent before building stateRef so isJsonlBacked can be set correctly.
32
+ // claude-code (protocol: direct) writes JSONL → JsonlParser owns event broadcasting.
33
+ // All other agents (protocol: acp) rely solely on onEvent for streaming.
34
+ let resolvedAgentId = agentId || 'claude-code';
35
+ const wrapperAgent = discoveredAgents.find(a => a.id === resolvedAgentId && a.protocol === 'cli-wrapper' && a.acpId);
36
+ if (wrapperAgent) resolvedAgentId = wrapperAgent.acpId;
37
+ const isJsonlBacked = resolvedAgentId === 'claude-code';
43
38
  // stateRef tracks eventCount (for session response metadata) and resumeSessionId
44
39
  const stateRef = { eventCount: 0, resumeSessionId: conv?.claudeSessionId || null };
45
- const onEvent = createEventHandler({ queries, activeExecutions, broadcastSync, rateLimitState, batcherRef: stateRef, sessionId, conversationId, messageId, content, agentId, model, subAgent, getJsonlWatcher, scheduleRetry, eagerTTS, debugLog, parseRateLimitResetTime });
46
- >>>>>>> 6bfde951cbeb65ec72b73da9c23b9c8c0ba0bbc1
40
+ const onEvent = createEventHandler({ queries, activeExecutions, broadcastSync, rateLimitState, batcherRef: stateRef, sessionId, conversationId, messageId, content, agentId, model, subAgent, isJsonlBacked, getJsonlWatcher, scheduleRetry, eagerTTS, debugLog, parseRateLimitResetTime });
47
41
  try {
48
- debugLog(`[stream] Starting: conversationId=${conversationId}, sessionId=${sessionId}`);
49
- let resolvedAgentId = agentId || 'claude-code';
50
- const wrapperAgent = discoveredAgents.find(a => a.id === resolvedAgentId && a.protocol === 'cli-wrapper' && a.acpId);
51
- if (wrapperAgent) resolvedAgentId = wrapperAgent.acpId;
42
+ debugLog(`[stream] Starting: conversationId=${conversationId}, sessionId=${sessionId}, agent=${resolvedAgentId}, jsonlBacked=${isJsonlBacked}`);
52
43
  const resolvedModel = model || conv?.model || null;
53
44
  const resolvedSubAgent = subAgent || conv?.subAgent || null;
54
45
  const config = {
55
46
  verbose: true, outputFormat: 'stream-json', timeout: 1800000, print: true,
56
- <<<<<<< HEAD
57
- resumeSessionId: batcherRef.resumeSessionId,
58
- =======
59
47
  resumeSessionId: stateRef.resumeSessionId,
60
- >>>>>>> 6bfde951cbeb65ec72b73da9c23b9c8c0ba0bbc1
61
48
  systemPrompt: buildSystemPrompt(agentId, resolvedModel, resolvedSubAgent),
62
49
  model: resolvedModel || undefined, subAgent: resolvedSubAgent || undefined, onEvent,
63
50
  onPid: (pid) => { const e = activeExecutions.get(conversationId); if (e) e.pid = pid; execMachine.send(conversationId, { type: 'SET_PID', pid }); },
@@ -70,19 +57,6 @@ export function createProcessMessage({ queries, activeExecutions, rateLimitState
70
57
  }
71
58
  activeExecutions.delete(conversationId);
72
59
  execMachine.send(conversationId, { type: 'COMPLETE' });
73
- <<<<<<< HEAD
74
- batcher.drain();
75
- if (claudeSessionId) ownedSessionIds.delete(claudeSessionId);
76
- debugLog(`[stream] Claude returned ${outputs.length} outputs, sessionId=${claudeSessionId}`);
77
- queries.updateSession(sessionId, { status: 'complete', response: JSON.stringify({ outputs, eventCount: batcherRef.eventCount }), completed_at: Date.now() });
78
- broadcastSync({ type: 'streaming_complete', sessionId, conversationId, agentId, eventCount: batcherRef.eventCount, seq: currentSequenceRef.val, timestamp: Date.now() });
79
- debugLog(`[stream] Completed: ${outputs.length} outputs, ${batcherRef.eventCount} events`);
80
- } catch (error) {
81
- const elapsed = Date.now() - startTime;
82
- debugLog(`[stream] Error after ${elapsed}ms: ${error.message}`);
83
- const conv2 = queries.getConversation(conversationId);
84
- if (conv2?.claudeSessionId) ownedSessionIds.delete(conv2.claudeSessionId);
85
- =======
86
60
  debugLog(`[stream] Claude returned ${outputs.length} outputs, sessionId=${claudeSessionId}`);
87
61
  queries.updateSession(sessionId, { status: 'complete', response: JSON.stringify({ outputs, eventCount: stateRef.eventCount }), completed_at: Date.now() });
88
62
  // streaming_complete is broadcast by JsonlParser when it sees the turn_duration event.
@@ -93,7 +67,6 @@ export function createProcessMessage({ queries, activeExecutions, rateLimitState
93
67
  } catch (error) {
94
68
  const elapsed = Date.now() - startTime;
95
69
  debugLog(`[stream] Error after ${elapsed}ms: ${error.message}`);
96
- >>>>>>> 6bfde951cbeb65ec72b73da9c23b9c8c0ba0bbc1
97
70
  if (rateLimitState.get(conversationId)?.isStreamDetected) {
98
71
  debugLog(`[rate-limit] Rate limit already handled in stream for conv ${conversationId}, skipping catch handler`);
99
72
  return;
@@ -107,10 +80,6 @@ export function createProcessMessage({ queries, activeExecutions, rateLimitState
107
80
  const errMsg = queries.createMessage(conversationId, 'assistant', `Error: Authentication failed. ${error.message}. Please update your credentials and try again.`);
108
81
  broadcastSync({ type: 'message_created', conversationId, message: errMsg, timestamp: Date.now() });
109
82
  queries.setIsStreaming(conversationId, false);
110
- <<<<<<< HEAD
111
- batcher.drain();
112
- =======
113
- >>>>>>> 6bfde951cbeb65ec72b73da9c23b9c8c0ba0bbc1
114
83
  activeExecutions.delete(conversationId);
115
84
  return;
116
85
  }
@@ -129,10 +98,6 @@ export function createProcessMessage({ queries, activeExecutions, rateLimitState
129
98
  const retryAt = Date.now() + cooldownMs;
130
99
  rateLimitState.set(conversationId, { retryAt, cooldownMs, retryCount });
131
100
  broadcastSync({ type: 'rate_limit_hit', sessionId, conversationId, retryAfterMs: cooldownMs, retryAt, retryCount, timestamp: Date.now() });
132
- <<<<<<< HEAD
133
- batcher.drain();
134
- =======
135
- >>>>>>> 6bfde951cbeb65ec72b73da9c23b9c8c0ba0bbc1
136
101
  debugLog(`[rate-limit] Scheduling retry for conv ${conversationId} in ${cooldownMs}ms (attempt ${retryCount + 1})`);
137
102
  setTimeout(() => {
138
103
  debugLog(`[rate-limit] Timeout fired for conv ${conversationId}, calling scheduleRetry`);
@@ -142,21 +107,13 @@ export function createProcessMessage({ queries, activeExecutions, rateLimitState
142
107
  }, cooldownMs);
143
108
  return;
144
109
  }
145
- <<<<<<< HEAD
146
- const isSessionConflict = error.exitCode === null && batcherRef.eventCount === 0;
147
- =======
148
110
  const isSessionConflict = error.exitCode === null && stateRef.eventCount === 0;
149
- >>>>>>> 6bfde951cbeb65ec72b73da9c23b9c8c0ba0bbc1
150
111
  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() });
151
112
  if (!isSessionConflict) {
152
113
  const errMsg = queries.createMessage(conversationId, 'assistant', `Error: ${error.message}`);
153
114
  broadcastSync({ type: 'message_created', conversationId, message: errMsg, timestamp: Date.now() });
154
115
  }
155
116
  } finally {
156
- <<<<<<< HEAD
157
- batcher.drain();
158
- =======
159
- >>>>>>> 6bfde951cbeb65ec72b73da9c23b9c8c0ba0bbc1
160
117
  if (!rateLimitState.has(conversationId)) {
161
118
  cleanupExecution(conversationId);
162
119
  drainMessageQueue(conversationId);
@@ -33,7 +33,7 @@ export function register(deps) {
33
33
  };
34
34
 
35
35
  routes['POST /api/tools/update'] = async (req, res) => {
36
- const allToolIds = ['cli-claude', 'cli-opencode', 'cli-gemini', 'cli-kilo', 'cli-codex', 'gm-cc', 'gm-oc', 'gm-gc', 'gm-kilo'];
36
+ const allToolIds = ['cli-claude', 'cli-opencode', 'cli-gemini', 'cli-kilo', 'cli-codex', 'cli-agent-browser', 'gm-cc', 'gm-oc', 'gm-gc', 'gm-kilo', 'gm-codex'];
37
37
  sendJSON(req, res, 200, { updating: true, toolCount: allToolIds.length });
38
38
  broadcastSync({ type: 'tools_update_started', tools: allToolIds });
39
39
  setImmediate(async () => {
@@ -1,10 +1,6 @@
1
1
  import { JsonlWatcher } from './jsonl-watcher.js';
2
2
 
3
- <<<<<<< HEAD
4
- 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 }) {
5
- =======
6
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 }) {
7
- >>>>>>> 6bfde951cbeb65ec72b73da9c23b9c8c0ba0bbc1
8
4
  let jsonlWatcher = null;
9
5
 
10
6
  function getJsonlWatcher() { return jsonlWatcher; }
@@ -27,14 +23,9 @@ export function createOnServerReady({ queries, broadcastSync, warmAssetCache, st
27
23
  }, 6 * 60 * 60 * 1000);
28
24
 
29
25
  try {
30
- <<<<<<< HEAD
31
- jsonlWatcher = new JsonlWatcher({ broadcastSync, queries, ownedSessionIds });
32
- jsonlWatcher.start();
33
- =======
34
26
  jsonlWatcher = new JsonlWatcher({ broadcastSync, queries });
35
27
  jsonlWatcher.start();
36
28
  if (setWatcher) setWatcher(jsonlWatcher);
37
- >>>>>>> 6bfde951cbeb65ec72b73da9c23b9c8c0ba0bbc1
38
29
  console.log('[JSONL] Watcher started');
39
30
  } catch (err) { console.error('[JSONL] Watcher failed to start:', err.message); }
40
31
 
@@ -60,7 +51,7 @@ export function createOnServerReady({ queries, broadcastSync, warmAssetCache, st
60
51
  }, 6000);
61
52
  }).catch(err => console.error('[ACP] Startup error:', err.message));
62
53
 
63
- const toolIds = ['cli-claude', 'cli-opencode', 'cli-gemini', 'cli-kilo', 'cli-codex', 'cli-agent-browser', 'gm-cc', 'gm-oc', 'gm-gc', 'gm-kilo'];
54
+ const toolIds = ['cli-claude', 'cli-opencode', 'cli-gemini', 'cli-kilo', 'cli-codex', 'cli-agent-browser', 'gm-cc', 'gm-oc', 'gm-gc', 'gm-kilo', 'gm-codex'];
64
55
  queries.initializeToolInstallations(toolIds.map(id => ({ id })));
65
56
  console.log('[TOOLS] Starting background provisioning...');
66
57
 
@@ -1,48 +1,21 @@
1
- <<<<<<< HEAD
2
- export function createEventHandler({ queries, activeExecutions, broadcastSync, rateLimitState, batcherRef, sessionId, conversationId, messageId, content, agentId, model, subAgent, ownedSessionIds, allBlocksRef, currentSequenceRef, scheduleRetry, eagerTTS, debugLog, parseRateLimitResetTime }) {
3
- =======
4
1
  /**
5
- * Minimal Claude stdout event handler.
2
+ * Claude stdout event handler.
6
3
  *
7
- * Now that JsonlParser owns all event broadcasting (streaming_start,
8
- * streaming_progress, streaming_complete) via the JSONL file watcher,
9
- * this handler only needs to:
10
- * 1. Extract session_id → call setClaudeSessionId + registerSession so the
11
- * parser links the file to the correct existing conversation/session.
12
- * 2. Detect inline rate-limit messages in text/result blocks → scheduleRetry.
4
+ * For claude-code (isJsonlBacked=true):
5
+ * JsonlParser owns all event broadcasting via the JSONL file watcher.
6
+ * This handler only needs to:
7
+ * 1. Extract session_id → setClaudeSessionId + registerSession
8
+ * 2. Detect inline rate-limit messages scheduleRetry
13
9
  *
14
- * No batcher, no broadcastSync for individual blocks.
10
+ * For ACP agents (isJsonlBacked=false):
11
+ * No JSONL file is written. This handler broadcasts streaming_start,
12
+ * streaming_progress blocks, and persists chunks to the DB.
15
13
  */
16
- export function createEventHandler({ queries, activeExecutions, broadcastSync, rateLimitState, batcherRef, sessionId, conversationId, messageId, content, agentId, model, subAgent, getJsonlWatcher, scheduleRetry, eagerTTS, debugLog, parseRateLimitResetTime }) {
17
- >>>>>>> 6bfde951cbeb65ec72b73da9c23b9c8c0ba0bbc1
14
+ export function createEventHandler({ queries, activeExecutions, broadcastSync, rateLimitState, batcherRef, sessionId, conversationId, messageId, content, agentId, model, subAgent, isJsonlBacked, getJsonlWatcher, scheduleRetry, eagerTTS, debugLog, parseRateLimitResetTime }) {
18
15
  return function onEvent(parsed) {
19
16
  batcherRef.eventCount++;
20
17
  const entry = activeExecutions.get(conversationId);
21
18
  if (entry) entry.lastActivity = Date.now();
22
- <<<<<<< HEAD
23
- if (parsed.session_id) {
24
- ownedSessionIds.add(parsed.session_id);
25
- if (!batcherRef.resumeSessionId || batcherRef.resumeSessionId !== parsed.session_id) {
26
- batcherRef.resumeSessionId = parsed.session_id;
27
- queries.setClaudeSessionId(conversationId, parsed.session_id, sessionId);
28
- }
29
- }
30
- debugLog(`[stream] Event ${batcherRef.eventCount}: type=${parsed.type}`);
31
-
32
- if (parsed.type === 'system') {
33
- if (parsed.subtype === 'task_notification') return;
34
- if (!parsed.model && !parsed.cwd && !parsed.tools) return;
35
- const block = { type: 'system', subtype: parsed.subtype, model: parsed.model, cwd: parsed.cwd, tools: parsed.tools, session_id: parsed.session_id };
36
- currentSequenceRef.val++;
37
- batcherRef.batcher.add(sessionId, conversationId, currentSequenceRef.val, 'system', block);
38
- broadcastSync({ type: 'streaming_progress', sessionId, conversationId, block, blockRole: 'system', blockIndex: allBlocksRef.val.length, seq: currentSequenceRef.val, timestamp: Date.now() });
39
- } else if (parsed.type === 'assistant' && parsed.message?.content) {
40
- for (const block of parsed.message.content) {
41
- allBlocksRef.val.push(block);
42
- currentSequenceRef.val++;
43
- batcherRef.batcher.add(sessionId, conversationId, currentSequenceRef.val, block.type || 'assistant', block);
44
- broadcastSync({ type: 'streaming_progress', sessionId, conversationId, block, blockRole: 'assistant', blockIndex: allBlocksRef.val.length - 1, seq: currentSequenceRef.val, timestamp: Date.now() });
45
- =======
46
19
 
47
20
  // Register session with file watcher as soon as we see the session_id.
48
21
  // This pre-maps claudeSessionId → (convId, dbSessionId) in JsonlParser before
@@ -57,26 +30,48 @@ export function createEventHandler({ queries, activeExecutions, broadcastSync, r
57
30
 
58
31
  debugLog(`[stream] Event ${batcherRef.eventCount}: type=${parsed.type}`);
59
32
 
60
- // Rate-limit detection in assistant text blocks
33
+ // --- ACP agent broadcasting (not backed by JSONL watcher) ---
34
+ if (!isJsonlBacked) {
35
+ // Send streaming_start on first event
36
+ if (batcherRef.eventCount === 1) {
37
+ queries.setIsStreaming(conversationId, true);
38
+ broadcastSync({ type: 'streaming_start', sessionId, conversationId, agentId, timestamp: Date.now() });
39
+ }
40
+
41
+ const emitBlock = (block, role, extra) => {
42
+ if (!block || !block.type) return;
43
+ batcherRef.currentSeq = (batcherRef.currentSeq || 0) + 1;
44
+ try { queries.createChunk(sessionId, conversationId, batcherRef.currentSeq, block.type, block); } catch (_) {}
45
+ broadcastSync({ type: 'streaming_progress', sessionId, conversationId, block, blockRole: role, seq: batcherRef.currentSeq, timestamp: Date.now(), ...extra });
46
+ };
47
+
48
+ if (parsed.type === 'assistant' && parsed.message?.content) {
49
+ for (const block of parsed.message.content) emitBlock(block, 'assistant');
50
+ }
51
+
52
+ if (parsed.type === 'user' && parsed.message?.content) {
53
+ const blocks = Array.isArray(parsed.message.content) ? parsed.message.content : [];
54
+ for (const block of blocks) if (block?.type === 'tool_result') emitBlock(block, 'tool_result');
55
+ }
56
+
57
+ if (parsed.type === 'result') {
58
+ const block = { type: 'result', result: parsed.result, subtype: parsed.subtype, duration_ms: parsed.duration_ms, total_cost_usd: parsed.total_cost_usd, is_error: parsed.is_error || false };
59
+ emitBlock(block, 'result', { isResult: true });
60
+ }
61
+ }
62
+
63
+ // --- Rate-limit detection in assistant text blocks (all agents) ---
61
64
  if (parsed.type === 'assistant' && parsed.message?.content) {
62
65
  for (const block of parsed.message.content) {
63
- >>>>>>> 6bfde951cbeb65ec72b73da9c23b9c8c0ba0bbc1
64
66
  if (block.type === 'text' && block.text) {
65
67
  const rateLimitMatch = block.text.match(/you'?ve hit your limit|rate limit exceeded/i);
66
68
  if (rateLimitMatch) {
67
69
  debugLog(`[rate-limit] Detected rate limit message in stream for conv ${conversationId}`);
68
70
  const retryAfterSec = parseRateLimitResetTime(block.text);
69
71
  const entry2 = activeExecutions.get(conversationId);
70
- <<<<<<< HEAD
71
- if (entry2 && entry2.pid) { try { process.kill(entry2.pid); } catch (e) {} }
72
- const existingCount = rateLimitState.get(conversationId)?.retryCount || 0;
73
- if (existingCount >= 3) {
74
- batcherRef.batcher.drain();
75
- =======
76
72
  if (entry2 && entry2.pid) { try { process.kill(entry2.pid); } catch (_) {} }
77
73
  const existingCount = rateLimitState.get(conversationId)?.retryCount || 0;
78
74
  if (existingCount >= 3) {
79
- >>>>>>> 6bfde951cbeb65ec72b73da9c23b9c8c0ba0bbc1
80
75
  activeExecutions.delete(conversationId);
81
76
  queries.setIsStreaming(conversationId, false);
82
77
  const errMsg = queries.createMessage(conversationId, 'assistant', `Error: Rate limit exceeded after ${existingCount + 1} attempts. Please try again later.`);
@@ -86,10 +81,6 @@ export function createEventHandler({ queries, activeExecutions, broadcastSync, r
86
81
  }
87
82
  rateLimitState.set(conversationId, { retryAt: Date.now() + (retryAfterSec * 1000), cooldownMs: retryAfterSec * 1000, retryCount: existingCount + 1, isStreamDetected: true });
88
83
  broadcastSync({ type: 'rate_limit_hit', sessionId, conversationId, retryAfterMs: retryAfterSec * 1000, retryAt: Date.now() + (retryAfterSec * 1000), retryCount: 1, timestamp: Date.now() });
89
- <<<<<<< HEAD
90
- batcherRef.batcher.drain();
91
- =======
92
- >>>>>>> 6bfde951cbeb65ec72b73da9c23b9c8c0ba0bbc1
93
84
  activeExecutions.delete(conversationId);
94
85
  queries.setIsStreaming(conversationId, false);
95
86
  setTimeout(() => {
@@ -102,64 +93,9 @@ export function createEventHandler({ queries, activeExecutions, broadcastSync, r
102
93
  eagerTTS(block.text, conversationId, sessionId);
103
94
  }
104
95
  }
105
- <<<<<<< HEAD
106
- } else if (parsed.type === 'user' && parsed.message?.content) {
107
- for (const block of parsed.message.content) {
108
- if (block.type === 'tool_result') {
109
- 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 };
110
- currentSequenceRef.val++;
111
- batcherRef.batcher.add(sessionId, conversationId, currentSequenceRef.val, 'tool_result', toolResultBlock);
112
- broadcastSync({ type: 'streaming_progress', sessionId, conversationId, block: toolResultBlock, blockRole: 'tool_result', blockIndex: allBlocksRef.val.length, seq: currentSequenceRef.val, timestamp: Date.now() });
113
- }
114
- }
115
- } else if (parsed.type === 'result') {
116
- 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 };
117
- currentSequenceRef.val++;
118
- batcherRef.batcher.add(sessionId, conversationId, currentSequenceRef.val, 'result', resultBlock);
119
- broadcastSync({ type: 'streaming_progress', sessionId, conversationId, block: resultBlock, blockRole: 'result', blockIndex: allBlocksRef.val.length, isResult: true, seq: currentSequenceRef.val, timestamp: Date.now() });
120
- if (parsed.result) {
121
- const resultText = typeof parsed.result === 'string' ? parsed.result : JSON.stringify(parsed.result);
122
- const rlMatch = resultText.match(/you'?ve hit your limit|rate limit exceeded/i);
123
- if (rlMatch) {
124
- debugLog(`[rate-limit] Detected rate limit in result for conv ${conversationId}`);
125
- const retryAfterSec = parseRateLimitResetTime(resultText);
126
- const entry3 = activeExecutions.get(conversationId);
127
- if (entry3 && entry3.pid) { try { process.kill(entry3.pid); } catch (e) {} }
128
- const existingCount2 = rateLimitState.get(conversationId)?.retryCount || 0;
129
- if (existingCount2 >= 3) {
130
- batcherRef.batcher.drain();
131
- activeExecutions.delete(conversationId);
132
- queries.setIsStreaming(conversationId, false);
133
- const errMsg2 = queries.createMessage(conversationId, 'assistant', `Error: Rate limit exceeded after ${existingCount2 + 1} attempts. Please try again later.`);
134
- broadcastSync({ type: 'message_created', conversationId, message: errMsg2, timestamp: Date.now() });
135
- broadcastSync({ type: 'streaming_complete', sessionId, conversationId, interrupted: true, timestamp: Date.now() });
136
- return;
137
- }
138
- rateLimitState.set(conversationId, { retryAt: Date.now() + (retryAfterSec * 1000), cooldownMs: retryAfterSec * 1000, retryCount: existingCount2 + 1, isStreamDetected: true });
139
- broadcastSync({ type: 'rate_limit_hit', sessionId, conversationId, retryAfterMs: retryAfterSec * 1000, retryAt: Date.now() + (retryAfterSec * 1000), retryCount: existingCount2 + 1, timestamp: Date.now() });
140
- batcherRef.batcher.drain();
141
- activeExecutions.delete(conversationId);
142
- queries.setIsStreaming(conversationId, false);
143
- setTimeout(() => {
144
- rateLimitState.delete(conversationId);
145
- broadcastSync({ type: 'rate_limit_clear', conversationId, timestamp: Date.now() });
146
- scheduleRetry(conversationId, messageId, content, agentId, model, subAgent);
147
- }, retryAfterSec * 1000);
148
- return;
149
- }
150
- if (resultText) eagerTTS(resultText, conversationId, sessionId);
151
- }
152
- if (parsed.result && allBlocksRef.val.length === 0) allBlocksRef.val.push({ type: 'text', text: String(parsed.result) });
153
- } else if (parsed.type === 'tool_status') {
154
- 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() });
155
- } else if (parsed.type === 'usage') {
156
- broadcastSync({ type: 'streaming_progress', sessionId, conversationId, block: { type: 'usage', usage: parsed.usage }, seq: currentSequenceRef.val, timestamp: Date.now() });
157
- } else if (parsed.type === 'plan') {
158
- broadcastSync({ type: 'streaming_progress', sessionId, conversationId, block: { type: 'plan', entries: parsed.entries }, seq: currentSequenceRef.val, timestamp: Date.now() });
159
- =======
160
96
  }
161
97
 
162
- // Rate-limit detection in result blocks
98
+ // --- Rate-limit detection in result blocks (all agents) ---
163
99
  if (parsed.type === 'result' && parsed.result) {
164
100
  const resultText = typeof parsed.result === 'string' ? parsed.result : JSON.stringify(parsed.result);
165
101
  const rlMatch = resultText.match(/you'?ve hit your limit|rate limit exceeded/i);
@@ -189,7 +125,6 @@ export function createEventHandler({ queries, activeExecutions, broadcastSync, r
189
125
  return;
190
126
  }
191
127
  if (resultText) eagerTTS(resultText, conversationId, sessionId);
192
- >>>>>>> 6bfde951cbeb65ec72b73da9c23b9c8c0ba0bbc1
193
128
  }
194
129
  };
195
130
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.857",
3
+ "version": "1.0.859",
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
@@ -98,7 +98,7 @@ const _assetDeps = { compressAndSend, acceptsEncoding, watch, BASE_URL, PKG_VERS
98
98
  function serveFile(filePath, res, req) { return _serveFile(filePath, res, req, _assetDeps); }
99
99
 
100
100
  const _routes = {};
101
- const server = http.createServer(createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, serveFile, staticDir, messageQueues, get wss() { return wss; }, activeExecutions, getACPStatus, discoveredAgents, PKG_VERSION, RATE_LIMIT_MAX, rateLimitMap: _rateLimitMap, routes: _routes, handleGeminiOAuthCallback, handleCodexOAuthCallback, PORT }));
101
+ const server = http.createServer(createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, serveFile, staticDir, messageQueues, getWss: () => wss, activeExecutions, getACPStatus, discoveredAgents, PKG_VERSION, RATE_LIMIT_MAX, rateLimitMap: _rateLimitMap, routes: _routes, handleGeminiOAuthCallback, handleCodexOAuthCallback, PORT }));
102
102
 
103
103
  let broadcastSeq = 0;
104
104
  const syncClients = new Set();