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.
@@ -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
+ }
@@ -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 = () => {} } = deps;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.707",
3
+ "version": "1.0.709",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
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));