agentgui 1.0.853 → 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.
- package/lib/jsonl-parser.js +32 -13
- package/lib/jsonl-watcher.js +20 -3
- package/lib/process-message.js +12 -18
- package/lib/server-startup.js +3 -2
- package/lib/stream-event-handler.js +49 -65
- package/package.json +1 -1
- package/server.js +6 -5
- package/static/css/main.css +514 -55
- package/static/index.html +80 -56
- package/static/js/client.js +3490 -4
- package/static/js/conversations.js +699 -0
package/lib/jsonl-parser.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/lib/jsonl-watcher.js
CHANGED
|
@@ -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
|
|
6
|
+
constructor({ broadcastSync, queries }) {
|
|
7
7
|
super();
|
|
8
|
-
this._parser = new JsonlParser({ broadcastSync, queries
|
|
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();
|
package/lib/process-message.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function createProcessMessage({ queries, activeExecutions, rateLimitState, execMachine, broadcastSync, runClaudeWithStreaming, cleanupExecution, checkpointManager, discoveredAgents,
|
|
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
|
-
|
|
33
|
-
const
|
|
34
|
-
const
|
|
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:
|
|
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:
|
|
62
|
-
|
|
63
|
-
|
|
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 &&
|
|
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);
|
package/lib/server-startup.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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 (
|
|
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
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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 (
|
|
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
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
|
|
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
|
-
|
|
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,
|
|
125
|
-
parseRateLimitResetTime, eagerTTS, touchACP,
|
|
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,
|
|
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
|