agentgui 1.0.707 → 1.0.709
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-watcher.js +168 -0
- package/lib/ws-handlers-conv.js +4 -1
- package/package.json +1 -1
- package/server.js +19 -1
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
const PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
|
|
6
|
+
const DEBOUNCE_MS = 16;
|
|
7
|
+
const genId = (p) => `${p}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
8
|
+
|
|
9
|
+
export class JsonlWatcher {
|
|
10
|
+
constructor({ broadcastSync, queries, ownedSessionIds }) {
|
|
11
|
+
this._bc = broadcastSync;
|
|
12
|
+
this._q = queries;
|
|
13
|
+
this._owned = ownedSessionIds;
|
|
14
|
+
this._tails = new Map();
|
|
15
|
+
this._convMap = new Map();
|
|
16
|
+
this._frags = new Map();
|
|
17
|
+
this._timers = new Map();
|
|
18
|
+
this._seqs = new Map();
|
|
19
|
+
this._watcher = null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
start() {
|
|
23
|
+
if (!fs.existsSync(PROJECTS_DIR)) return;
|
|
24
|
+
this._scan();
|
|
25
|
+
try {
|
|
26
|
+
this._watcher = fs.watch(PROJECTS_DIR, { recursive: true }, (_, f) => {
|
|
27
|
+
if (f && f.endsWith('.jsonl')) this._debounce(path.join(PROJECTS_DIR, f));
|
|
28
|
+
});
|
|
29
|
+
this._watcher.on('error', (e) => console.error('[JsonlWatcher]', e.message));
|
|
30
|
+
} catch (e) { console.error('[JsonlWatcher] start failed:', e.message); }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
stop() {
|
|
34
|
+
if (this._watcher) try { this._watcher.close(); } catch (_) {}
|
|
35
|
+
for (const s of this._tails.values()) if (s.fd !== null) try { fs.closeSync(s.fd); } catch (_) {}
|
|
36
|
+
for (const t of this._timers.values()) clearTimeout(t);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
removeConversation(conversationId) {
|
|
40
|
+
const sids = [...this._convMap.entries()].filter(([, cid]) => cid === conversationId).map(([sid]) => sid);
|
|
41
|
+
for (const sid of sids) {
|
|
42
|
+
this._convMap.delete(sid);
|
|
43
|
+
this._seqs.delete(sid);
|
|
44
|
+
for (const key of [...this._frags.keys()]) if (key.startsWith(`${sid}:`)) this._frags.delete(key);
|
|
45
|
+
const fp = path.join(PROJECTS_DIR, sid + '.jsonl');
|
|
46
|
+
const s = this._tails.get(fp);
|
|
47
|
+
if (s?.fd !== null) try { fs.closeSync(s.fd); } catch (_) {}
|
|
48
|
+
this._tails.delete(fp);
|
|
49
|
+
const t = this._timers.get(fp);
|
|
50
|
+
if (t) { clearTimeout(t); this._timers.delete(fp); }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
removeAllConversations() {
|
|
55
|
+
for (const s of this._tails.values()) if (s.fd !== null) try { fs.closeSync(s.fd); } catch (_) {}
|
|
56
|
+
for (const t of this._timers.values()) clearTimeout(t);
|
|
57
|
+
this._tails.clear();
|
|
58
|
+
this._convMap.clear();
|
|
59
|
+
this._frags.clear();
|
|
60
|
+
this._timers.clear();
|
|
61
|
+
this._seqs.clear();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_scan() {
|
|
65
|
+
try {
|
|
66
|
+
for (const d of fs.readdirSync(PROJECTS_DIR, { withFileTypes: true })) {
|
|
67
|
+
if (!d.isDirectory()) continue;
|
|
68
|
+
try {
|
|
69
|
+
for (const f of fs.readdirSync(path.join(PROJECTS_DIR, d.name)))
|
|
70
|
+
if (f.endsWith('.jsonl')) this._debounce(path.join(PROJECTS_DIR, d.name, f));
|
|
71
|
+
} catch (_) {}
|
|
72
|
+
}
|
|
73
|
+
} catch (_) {}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_debounce(fp) {
|
|
77
|
+
const t = this._timers.get(fp);
|
|
78
|
+
if (t) clearTimeout(t);
|
|
79
|
+
this._timers.set(fp, setTimeout(() => { this._timers.delete(fp); this._read(fp); }, DEBOUNCE_MS));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
_read(fp) {
|
|
83
|
+
let s = this._tails.get(fp);
|
|
84
|
+
if (!s) { s = { fd: null, offset: 0, partial: '' }; this._tails.set(fp, s); }
|
|
85
|
+
try {
|
|
86
|
+
if (s.fd === null) s.fd = fs.openSync(fp, 'r');
|
|
87
|
+
const stat = fs.fstatSync(s.fd);
|
|
88
|
+
if (stat.size <= s.offset) return;
|
|
89
|
+
const buf = Buffer.allocUnsafe(stat.size - s.offset);
|
|
90
|
+
const n = fs.readSync(s.fd, buf, 0, buf.length, s.offset);
|
|
91
|
+
s.offset += n;
|
|
92
|
+
const text = s.partial + buf.toString('utf8', 0, n);
|
|
93
|
+
const t0 = process.hrtime.bigint();
|
|
94
|
+
const lines = []; let start = 0, idx;
|
|
95
|
+
while ((idx = text.indexOf('\n', start)) !== -1) { lines.push(text.slice(start, idx)); start = idx + 1; }
|
|
96
|
+
s.partial = text.slice(start);
|
|
97
|
+
const ms = Number(process.hrtime.bigint() - t0) / 1e6;
|
|
98
|
+
if (ms > 5) console.warn(`[JsonlWatcher] hot path ${ms.toFixed(1)}ms (${lines.length} lines)`);
|
|
99
|
+
for (const l of lines) this._line(fp, l);
|
|
100
|
+
} catch (e) {
|
|
101
|
+
if (e.code !== 'ENOENT') console.error('[JsonlWatcher] read error:', e.message);
|
|
102
|
+
if (s.fd !== null) { try { fs.closeSync(s.fd); } catch (_) {} s.fd = null; }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_line(fp, line) {
|
|
107
|
+
line = line.trim(); if (!line) return;
|
|
108
|
+
let e; try { e = JSON.parse(line); } catch (_) { return; }
|
|
109
|
+
if (!e || !e.sessionId) return;
|
|
110
|
+
if (this._owned?.has(e.sessionId)) return;
|
|
111
|
+
const cid = this._conv(e.sessionId, e, fp);
|
|
112
|
+
if (cid) this._route(cid, e.sessionId, e);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
_conv(sid, e, fp) {
|
|
116
|
+
if (this._convMap.has(sid)) return this._convMap.get(sid);
|
|
117
|
+
const found = this._q.getConversations().find(c => c.claudeSessionId === sid);
|
|
118
|
+
if (found) { this._convMap.set(sid, found.id); return found.id; }
|
|
119
|
+
if (e.type !== 'user' || e.isMeta) return null;
|
|
120
|
+
const cwd = e.cwd || process.cwd();
|
|
121
|
+
const branch = e.gitBranch || '';
|
|
122
|
+
const base = path.basename(cwd);
|
|
123
|
+
const title = branch ? `External: ${branch} @ ${base}` : `External: ${base}`;
|
|
124
|
+
const conv = this._q.createConversation('cli-claude', title, cwd);
|
|
125
|
+
this._q.setClaudeSessionId(conv.id, sid);
|
|
126
|
+
this._convMap.set(sid, conv.id);
|
|
127
|
+
this._bc({ type: 'conversation_created', conversation: conv, timestamp: Date.now() });
|
|
128
|
+
return conv.id;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
_seq(sid) { const n = (this._seqs.get(sid) || 0) + 1; this._seqs.set(sid, n); return n; }
|
|
132
|
+
|
|
133
|
+
_route(cid, sid, e) {
|
|
134
|
+
if (e.type === 'queue-operation' || (e.type === 'user' && e.isMeta)) return;
|
|
135
|
+
|
|
136
|
+
if (e.type === 'system') {
|
|
137
|
+
if (e.subtype === 'init') { this._bc({ type: 'streaming_start', sessionId: sid, conversationId: cid, agentId: 'cli-claude', timestamp: Date.now() }); return; }
|
|
138
|
+
if (e.subtype === 'turn_duration') { this._bc({ type: 'streaming_complete', sessionId: sid, conversationId: cid, agentId: 'cli-claude', eventCount: 0, seq: this._seq(sid), timestamp: Date.now() }); return; }
|
|
139
|
+
const b = { type: 'system', subtype: e.subtype, model: e.model, cwd: e.cwd, tools: e.tools, preTokens: e.compactMetadata?.preTokens };
|
|
140
|
+
this._bc({ type: 'streaming_progress', sessionId: sid, conversationId: cid, block: b, blockRole: 'system', seq: this._seq(sid), timestamp: Date.now() });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (e.type === 'assistant' && e.message?.content) {
|
|
145
|
+
const key = `${sid}:${e.message.id}`;
|
|
146
|
+
if (e.message.stop_reason === null || e.message.stop_reason === undefined) { this._frags.set(key, e); return; }
|
|
147
|
+
this._frags.delete(key);
|
|
148
|
+
for (const b of e.message.content) this._bc({ type: 'streaming_progress', sessionId: sid, conversationId: cid, block: b, blockRole: 'assistant', seq: this._seq(sid), timestamp: Date.now() });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (e.type === 'user' && e.message?.content) {
|
|
153
|
+
if (e.isCompactSummary) { this._bc({ type: 'streaming_progress', sessionId: sid, conversationId: cid, block: { type: 'compact_summary', content: e.message.content }, seq: this._seq(sid), timestamp: Date.now() }); return; }
|
|
154
|
+
for (const b of e.message.content) if (b.type === 'tool_result') this._bc({ type: 'streaming_progress', sessionId: sid, conversationId: cid, block: b, blockRole: 'tool_result', seq: this._seq(sid), timestamp: Date.now() });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (e.type === 'progress') { this._bc({ type: 'streaming_progress', sessionId: sid, conversationId: cid, block: { type: e.subtype || 'progress', content: e.content }, seq: this._seq(sid), timestamp: Date.now() }); return; }
|
|
159
|
+
|
|
160
|
+
if (e.isApiErrorMessage && e.error === 'rate_limit') { this._bc({ type: 'streaming_error', sessionId: sid, conversationId: cid, error: 'Rate limit hit', recoverable: true, timestamp: Date.now() }); return; }
|
|
161
|
+
|
|
162
|
+
if (e.type === 'result') {
|
|
163
|
+
const b = { type: 'result', result: e.result, subtype: e.subtype, duration_ms: e.duration_ms, total_cost_usd: e.total_cost_usd, is_error: e.is_error || false };
|
|
164
|
+
this._bc({ type: 'streaming_progress', sessionId: sid, conversationId: cid, block: b, blockRole: 'result', isResult: true, seq: this._seq(sid), timestamp: Date.now() });
|
|
165
|
+
this._bc({ type: 'streaming_complete', sessionId: sid, conversationId: cid, agentId: 'cli-claude', eventCount: 0, seq: this._seq(sid), timestamp: Date.now() });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
package/lib/ws-handlers-conv.js
CHANGED
|
@@ -41,7 +41,8 @@ const ConvSteerSchema = z.object({
|
|
|
41
41
|
|
|
42
42
|
export function register(router, deps) {
|
|
43
43
|
const { queries, activeExecutions, messageQueues, rateLimitState,
|
|
44
|
-
broadcastSync, processMessageWithStreaming, cleanupExecution, logError = () => {}
|
|
44
|
+
broadcastSync, processMessageWithStreaming, cleanupExecution, logError = () => {},
|
|
45
|
+
getJsonlWatcher = () => null } = deps;
|
|
45
46
|
|
|
46
47
|
// Per-conversation queue seq counter for event ordering
|
|
47
48
|
const queueSeqByConv = new Map();
|
|
@@ -88,12 +89,14 @@ export function register(router, deps) {
|
|
|
88
89
|
|
|
89
90
|
router.handle('conv.del', (p) => {
|
|
90
91
|
if (!queries.deleteConversation(p.id)) notFound();
|
|
92
|
+
getJsonlWatcher()?.removeConversation(p.id);
|
|
91
93
|
broadcastSync({ type: 'conversation_deleted', conversationId: p.id });
|
|
92
94
|
return { deleted: true };
|
|
93
95
|
});
|
|
94
96
|
|
|
95
97
|
router.handle('conv.del.all', (p) => {
|
|
96
98
|
if (!queries.deleteAllConversations()) fail(500, 'Failed to delete all conversations');
|
|
99
|
+
getJsonlWatcher()?.removeAllConversations();
|
|
97
100
|
broadcastSync({ type: 'all_conversations_deleted', timestamp: Date.now() });
|
|
98
101
|
return { deleted: true, message: 'All conversations deleted' };
|
|
99
102
|
});
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -31,6 +31,7 @@ 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';
|
|
33
33
|
import CheckpointManager from './lib/checkpoint-manager.js';
|
|
34
|
+
import { JsonlWatcher } from './lib/jsonl-watcher.js';
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
process.on('uncaughtException', (err, origin) => {
|
|
@@ -46,6 +47,7 @@ process.on('unhandledRejection', (reason, promise) => {
|
|
|
46
47
|
process.on('SIGINT', () => {
|
|
47
48
|
console.log('[SIGNAL] SIGINT received - graceful shutdown');
|
|
48
49
|
try { pm2Manager.disconnect(); } catch (_) {}
|
|
50
|
+
if (jsonlWatcher) try { jsonlWatcher.stop(); } catch (_) {}
|
|
49
51
|
stopACPTools().catch(() => {}).finally(() => {
|
|
50
52
|
try { wss.close(() => server.close(() => process.exit(0))); } catch (_) { process.exit(0); }
|
|
51
53
|
});
|
|
@@ -300,6 +302,7 @@ const activeExecutions = new Map();
|
|
|
300
302
|
const activeScripts = new Map();
|
|
301
303
|
const messageQueues = new Map();
|
|
302
304
|
const rateLimitState = new Map();
|
|
305
|
+
const ownedSessionIds = new Set();
|
|
303
306
|
const activeProcessesByRunId = new Map();
|
|
304
307
|
const checkpointManager = new CheckpointManager(queries);
|
|
305
308
|
const STUCK_AGENT_THRESHOLD_MS = 1800000;
|
|
@@ -3820,6 +3823,7 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
3820
3823
|
eventCount++;
|
|
3821
3824
|
const entry = activeExecutions.get(conversationId);
|
|
3822
3825
|
if (entry) entry.lastActivity = Date.now();
|
|
3826
|
+
if (parsed.session_id) ownedSessionIds.add(parsed.session_id);
|
|
3823
3827
|
debugLog(`[stream] Event ${eventCount}: type=${parsed.type}`);
|
|
3824
3828
|
|
|
3825
3829
|
if (parsed.type === 'system') {
|
|
@@ -4113,6 +4117,7 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
4113
4117
|
activeExecutions.delete(conversationId);
|
|
4114
4118
|
execMachine.send(conversationId, { type: 'COMPLETE' });
|
|
4115
4119
|
batcher.drain();
|
|
4120
|
+
if (claudeSessionId) ownedSessionIds.delete(claudeSessionId);
|
|
4116
4121
|
debugLog(`[stream] Claude returned ${outputs.length} outputs, sessionId=${claudeSessionId}`);
|
|
4117
4122
|
|
|
4118
4123
|
// Clear claudeSessionId after successful completion so next message starts fresh
|
|
@@ -4141,6 +4146,8 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
4141
4146
|
} catch (error) {
|
|
4142
4147
|
const elapsed = Date.now() - startTime;
|
|
4143
4148
|
debugLog(`[stream] Error after ${elapsed}ms: ${error.message}`);
|
|
4149
|
+
const conv2 = queries.getConversation(conversationId);
|
|
4150
|
+
if (conv2?.claudeSessionId) ownedSessionIds.delete(conv2.claudeSessionId);
|
|
4144
4151
|
|
|
4145
4152
|
// Check if rate limit was already handled in stream detection
|
|
4146
4153
|
const existingState = rateLimitState.get(conversationId);
|
|
@@ -4511,7 +4518,8 @@ const wsRouter = new WsRouter();
|
|
|
4511
4518
|
|
|
4512
4519
|
registerConvHandlers(wsRouter, {
|
|
4513
4520
|
queries, activeExecutions, messageQueues, rateLimitState,
|
|
4514
|
-
broadcastSync, processMessageWithStreaming, cleanupExecution, logError
|
|
4521
|
+
broadcastSync, processMessageWithStreaming, cleanupExecution, logError,
|
|
4522
|
+
getJsonlWatcher: () => jsonlWatcher
|
|
4515
4523
|
});
|
|
4516
4524
|
|
|
4517
4525
|
console.log('[INIT] About to call registerSessionHandlers, discoveredAgents.length:', discoveredAgents.length);
|
|
@@ -5021,6 +5029,8 @@ function performAgentHealthCheck() {
|
|
|
5021
5029
|
}
|
|
5022
5030
|
}
|
|
5023
5031
|
|
|
5032
|
+
let jsonlWatcher = null;
|
|
5033
|
+
|
|
5024
5034
|
function onServerReady() {
|
|
5025
5035
|
// Clear tool status cache on startup to ensure fresh detection
|
|
5026
5036
|
toolManager.clearStatusCache();
|
|
@@ -5036,6 +5046,14 @@ function onServerReady() {
|
|
|
5036
5046
|
|
|
5037
5047
|
recoverStaleSessions();
|
|
5038
5048
|
|
|
5049
|
+
try {
|
|
5050
|
+
jsonlWatcher = new JsonlWatcher({ broadcastSync, queries, ownedSessionIds });
|
|
5051
|
+
jsonlWatcher.start();
|
|
5052
|
+
console.log('[JSONL] Watcher started');
|
|
5053
|
+
} catch (err) {
|
|
5054
|
+
console.error('[JSONL] Watcher failed to start:', err.message);
|
|
5055
|
+
}
|
|
5056
|
+
|
|
5039
5057
|
resumeInterruptedStreams().catch(err => console.error('[RESUME] Startup error:', err.message));
|
|
5040
5058
|
|
|
5041
5059
|
installGMAgentConfigs().catch(err => console.error('[GM-CONFIG] Startup error:', err.message));
|