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.
- package/lib/db-queries-tools.js +4 -0
- package/lib/http-handler.js +2 -2
- package/lib/jsonl-parser.js +4 -41
- package/lib/jsonl-watcher.js +0 -13
- package/lib/process-message.js +9 -52
- package/lib/routes-tools.js +1 -1
- package/lib/server-startup.js +1 -10
- package/lib/stream-event-handler.js +42 -107
- package/package.json +1 -1
- package/server.js +1 -1
package/lib/db-queries-tools.js
CHANGED
|
@@ -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)
|
package/lib/http-handler.js
CHANGED
|
@@ -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,
|
|
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:
|
|
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
|
|
package/lib/jsonl-parser.js
CHANGED
|
@@ -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 →
|
|
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
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
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;
|
package/lib/jsonl-watcher.js
CHANGED
|
@@ -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();
|
package/lib/process-message.js
CHANGED
|
@@ -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);
|
package/lib/routes-tools.js
CHANGED
|
@@ -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 () => {
|
package/lib/server-startup.js
CHANGED
|
@@ -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
|
-
*
|
|
2
|
+
* Claude stdout event handler.
|
|
6
3
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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
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,
|
|
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();
|