agentgui 1.0.707 → 1.0.708
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 +143 -0
- package/package.json +1 -1
- package/server.js +17 -0
|
@@ -0,0 +1,143 @@
|
|
|
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
|
+
_scan() {
|
|
40
|
+
try {
|
|
41
|
+
for (const d of fs.readdirSync(PROJECTS_DIR, { withFileTypes: true })) {
|
|
42
|
+
if (!d.isDirectory()) continue;
|
|
43
|
+
try {
|
|
44
|
+
for (const f of fs.readdirSync(path.join(PROJECTS_DIR, d.name)))
|
|
45
|
+
if (f.endsWith('.jsonl')) this._debounce(path.join(PROJECTS_DIR, d.name, f));
|
|
46
|
+
} catch (_) {}
|
|
47
|
+
}
|
|
48
|
+
} catch (_) {}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_debounce(fp) {
|
|
52
|
+
const t = this._timers.get(fp);
|
|
53
|
+
if (t) clearTimeout(t);
|
|
54
|
+
this._timers.set(fp, setTimeout(() => { this._timers.delete(fp); this._read(fp); }, DEBOUNCE_MS));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
_read(fp) {
|
|
58
|
+
let s = this._tails.get(fp);
|
|
59
|
+
if (!s) { s = { fd: null, offset: 0, partial: '' }; this._tails.set(fp, s); }
|
|
60
|
+
try {
|
|
61
|
+
if (s.fd === null) s.fd = fs.openSync(fp, 'r');
|
|
62
|
+
const stat = fs.fstatSync(s.fd);
|
|
63
|
+
if (stat.size <= s.offset) return;
|
|
64
|
+
const buf = Buffer.allocUnsafe(stat.size - s.offset);
|
|
65
|
+
const n = fs.readSync(s.fd, buf, 0, buf.length, s.offset);
|
|
66
|
+
s.offset += n;
|
|
67
|
+
const text = s.partial + buf.toString('utf8', 0, n);
|
|
68
|
+
const t0 = process.hrtime.bigint();
|
|
69
|
+
const lines = []; let start = 0, idx;
|
|
70
|
+
while ((idx = text.indexOf('\n', start)) !== -1) { lines.push(text.slice(start, idx)); start = idx + 1; }
|
|
71
|
+
s.partial = text.slice(start);
|
|
72
|
+
const ms = Number(process.hrtime.bigint() - t0) / 1e6;
|
|
73
|
+
if (ms > 5) console.warn(`[JsonlWatcher] hot path ${ms.toFixed(1)}ms (${lines.length} lines)`);
|
|
74
|
+
for (const l of lines) this._line(fp, l);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
if (e.code !== 'ENOENT') console.error('[JsonlWatcher] read error:', e.message);
|
|
77
|
+
if (s.fd !== null) { try { fs.closeSync(s.fd); } catch (_) {} s.fd = null; }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
_line(fp, line) {
|
|
82
|
+
line = line.trim(); if (!line) return;
|
|
83
|
+
let e; try { e = JSON.parse(line); } catch (_) { return; }
|
|
84
|
+
if (!e || !e.sessionId) return;
|
|
85
|
+
if (this._owned?.has(e.sessionId)) return;
|
|
86
|
+
const cid = this._conv(e.sessionId, e, fp);
|
|
87
|
+
if (cid) this._route(cid, e.sessionId, e);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_conv(sid, e, fp) {
|
|
91
|
+
if (this._convMap.has(sid)) return this._convMap.get(sid);
|
|
92
|
+
const found = this._q.getConversations().find(c => c.claudeSessionId === sid);
|
|
93
|
+
if (found) { this._convMap.set(sid, found.id); return found.id; }
|
|
94
|
+
if (e.type !== 'user' || e.isMeta) return null;
|
|
95
|
+
const cwd = e.cwd || process.cwd();
|
|
96
|
+
const branch = e.gitBranch || '';
|
|
97
|
+
const base = path.basename(cwd);
|
|
98
|
+
const title = branch ? `External: ${branch} @ ${base}` : `External: ${base}`;
|
|
99
|
+
const conv = this._q.createConversation('cli-claude', title, cwd);
|
|
100
|
+
this._q.setClaudeSessionId(conv.id, sid);
|
|
101
|
+
this._convMap.set(sid, conv.id);
|
|
102
|
+
this._bc({ type: 'conversation_created', conversation: conv, timestamp: Date.now() });
|
|
103
|
+
return conv.id;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_seq(sid) { const n = (this._seqs.get(sid) || 0) + 1; this._seqs.set(sid, n); return n; }
|
|
107
|
+
|
|
108
|
+
_route(cid, sid, e) {
|
|
109
|
+
if (e.type === 'queue-operation' || (e.type === 'user' && e.isMeta)) return;
|
|
110
|
+
|
|
111
|
+
if (e.type === 'system') {
|
|
112
|
+
if (e.subtype === 'init') { this._bc({ type: 'streaming_start', sessionId: sid, conversationId: cid, agentId: 'cli-claude', timestamp: Date.now() }); return; }
|
|
113
|
+
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; }
|
|
114
|
+
const b = { type: 'system', subtype: e.subtype, model: e.model, cwd: e.cwd, tools: e.tools, preTokens: e.compactMetadata?.preTokens };
|
|
115
|
+
this._bc({ type: 'streaming_progress', sessionId: sid, conversationId: cid, block: b, blockRole: 'system', seq: this._seq(sid), timestamp: Date.now() });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (e.type === 'assistant' && e.message?.content) {
|
|
120
|
+
const key = `${sid}:${e.message.id}`;
|
|
121
|
+
if (e.message.stop_reason === null || e.message.stop_reason === undefined) { this._frags.set(key, e); return; }
|
|
122
|
+
this._frags.delete(key);
|
|
123
|
+
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() });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (e.type === 'user' && e.message?.content) {
|
|
128
|
+
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; }
|
|
129
|
+
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() });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
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; }
|
|
134
|
+
|
|
135
|
+
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; }
|
|
136
|
+
|
|
137
|
+
if (e.type === 'result') {
|
|
138
|
+
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 };
|
|
139
|
+
this._bc({ type: 'streaming_progress', sessionId: sid, conversationId: cid, block: b, blockRole: 'result', isResult: true, seq: this._seq(sid), timestamp: Date.now() });
|
|
140
|
+
this._bc({ type: 'streaming_complete', sessionId: sid, conversationId: cid, agentId: 'cli-claude', eventCount: 0, seq: this._seq(sid), timestamp: Date.now() });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
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);
|
|
@@ -5021,6 +5028,8 @@ function performAgentHealthCheck() {
|
|
|
5021
5028
|
}
|
|
5022
5029
|
}
|
|
5023
5030
|
|
|
5031
|
+
let jsonlWatcher = null;
|
|
5032
|
+
|
|
5024
5033
|
function onServerReady() {
|
|
5025
5034
|
// Clear tool status cache on startup to ensure fresh detection
|
|
5026
5035
|
toolManager.clearStatusCache();
|
|
@@ -5036,6 +5045,14 @@ function onServerReady() {
|
|
|
5036
5045
|
|
|
5037
5046
|
recoverStaleSessions();
|
|
5038
5047
|
|
|
5048
|
+
try {
|
|
5049
|
+
jsonlWatcher = new JsonlWatcher({ broadcastSync, queries, ownedSessionIds });
|
|
5050
|
+
jsonlWatcher.start();
|
|
5051
|
+
console.log('[JSONL] Watcher started');
|
|
5052
|
+
} catch (err) {
|
|
5053
|
+
console.error('[JSONL] Watcher failed to start:', err.message);
|
|
5054
|
+
}
|
|
5055
|
+
|
|
5039
5056
|
resumeInterruptedStreams().catch(err => console.error('[RESUME] Startup error:', err.message));
|
|
5040
5057
|
|
|
5041
5058
|
installGMAgentConfigs().catch(err => console.error('[GM-CONFIG] Startup error:', err.message));
|