agentgui 1.0.654 → 1.0.658

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/codec.js ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Binary codec for WebSocket messages.
3
+ * Wraps msgpackr for framing + GPT tokenizer BPE compression for large text fields.
4
+ *
5
+ * Wire format: msgpackr-packed object where string fields > THRESHOLD chars
6
+ * are replaced with { __tok: true, d: Uint32Array } — tokenized and decompressed
7
+ * transparently on both sides.
8
+ */
9
+
10
+ import { pack, unpack } from 'msgpackr';
11
+ import { encode as tokEncode, decode as tokDecode } from 'gpt-tokenizer';
12
+
13
+ const THRESHOLD = 200; // bytes before compression is worthwhile
14
+ const COMPRESSIBLE = new Set(['content', 'text', 'output', 'response', 'prompt', 'input', 'data']);
15
+
16
+ function compressText(str) {
17
+ return { __tok: true, d: tokEncode(str) };
18
+ }
19
+
20
+ function decompressText(val) {
21
+ return tokDecode(val.d);
22
+ }
23
+
24
+ function encodeObj(obj) {
25
+ if (!obj || typeof obj !== 'object') return obj;
26
+ if (Array.isArray(obj)) return obj.map(encodeObj);
27
+ const out = {};
28
+ for (const k of Object.keys(obj)) {
29
+ const v = obj[k];
30
+ if (COMPRESSIBLE.has(k) && typeof v === 'string' && v.length > THRESHOLD) {
31
+ out[k] = compressText(v);
32
+ } else if (v && typeof v === 'object' && !ArrayBuffer.isView(v)) {
33
+ out[k] = encodeObj(v);
34
+ } else {
35
+ out[k] = v;
36
+ }
37
+ }
38
+ return out;
39
+ }
40
+
41
+ function decodeObj(obj) {
42
+ if (!obj || typeof obj !== 'object') return obj;
43
+ if (Array.isArray(obj)) return obj.map(decodeObj);
44
+ if (obj.__tok && obj.d) return decompressText(obj);
45
+ const out = {};
46
+ for (const k of Object.keys(obj)) {
47
+ out[k] = decodeObj(obj[k]);
48
+ }
49
+ return out;
50
+ }
51
+
52
+ export function encode(obj) { return pack(encodeObj(obj)); }
53
+ export function decode(buf) { return decodeObj(unpack(buf instanceof Uint8Array ? buf : new Uint8Array(buf))); }
@@ -1,4 +1,4 @@
1
- import { pack } from 'msgpackr';
1
+ import { encode } from './codec.js';
2
2
 
3
3
  const MESSAGE_PRIORITY = {
4
4
  high: ['streaming_error', 'streaming_complete', 'rate_limit_hit', 'streaming_cancelled', 'run_cancelled', 'tool_install_complete', 'tool_update_complete', 'tool_install_failed', 'tool_update_failed'],
@@ -84,9 +84,8 @@ class ClientQueue {
84
84
  if (allowedCount <= 0) { this.scheduleFlush(); return; }
85
85
  batch.splice(allowedCount);
86
86
  }
87
- // Pack as msgpackr binary — perMessageDeflate on the WS server handles gzip
88
87
  const envelope = batch.length === 1 ? batch[0] : batch;
89
- const binary = pack(envelope);
88
+ const binary = encode(envelope);
90
89
  this.ws.send(binary);
91
90
  this.messageCount += batch.length;
92
91
  this.bytesSent += binary.length;
@@ -1,7 +1,7 @@
1
- import { pack, unpack } from 'msgpackr';
1
+ import { encode, decode } from './codec.js';
2
2
 
3
3
  function sendBinary(ws, obj) {
4
- if (ws.readyState === 1) ws.send(pack(obj));
4
+ if (ws.readyState === 1) ws.send(encode(obj));
5
5
  }
6
6
 
7
7
  class WsRouter {
@@ -33,7 +33,7 @@ class WsRouter {
33
33
  }
34
34
 
35
35
  broadcast(clients, type, data) {
36
- const msg = pack({ t: type, d: data || {} });
36
+ const msg = encode({ t: type, d: data || {} });
37
37
  for (const ws of clients) {
38
38
  if (ws.readyState === 1) ws.send(msg);
39
39
  }
@@ -42,12 +42,7 @@ class WsRouter {
42
42
  async onMessage(ws, rawData) {
43
43
  let parsed;
44
44
  try {
45
- // Accept binary (msgpackr) or text (JSON fallback / legacy hot-reload)
46
- if (Buffer.isBuffer(rawData) || rawData instanceof Uint8Array) {
47
- parsed = unpack(rawData);
48
- } else {
49
- parsed = JSON.parse(rawData.toString());
50
- }
45
+ parsed = decode(rawData);
51
46
  } catch {
52
47
  sendBinary(ws, { r: null, e: { c: 400, m: 'Invalid message' } });
53
48
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.654",
3
+ "version": "1.0.658",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -16,9 +16,10 @@ import fsbrowse from 'fsbrowse';
16
16
  import { queries } from './database.js';
17
17
  import { runClaudeWithStreaming } from './lib/claude-runner.js';
18
18
  import { initializeDescriptors, getAgentDescriptor } from './lib/agent-descriptors.js';
19
- import { SSEStreamManager } from './lib/sse-stream.js';
20
19
  import { WSOptimizer } from './lib/ws-optimizer.js';
21
20
  import { WsRouter } from './lib/ws-protocol.js';
21
+ import { encode as wsEncode } from './lib/codec.js';
22
+ const sendWs = (ws, obj) => { if (ws.readyState === 1) ws.send(wsEncode(obj)); };
22
23
  import { register as registerConvHandlers } from './lib/ws-handlers-conv.js';
23
24
  import { register as registerSessionHandlers } from './lib/ws-handlers-session.js';
24
25
  import { register as registerRunHandlers } from './lib/ws-handlers-run.js';
@@ -278,10 +279,8 @@ function pushTTSAudio(cacheKey, wav, conversationId, sessionId, voiceId) {
278
279
  }
279
280
 
280
281
 
281
- const VOICE_INSTRUCTIONS = `Plain text. Spoken aloud. Conversational. No markdown, formatting, bullets, or lists. Short sentences. Technical facts only.`;
282
-
283
282
  function buildSystemPrompt(agentId, model, subAgent) {
284
- const parts = [VOICE_INSTRUCTIONS];
283
+ const parts = [];
285
284
  if (agentId && agentId !== 'claude-code') {
286
285
  const displayAgentId = agentId.split('-·-')[0];
287
286
  parts.push(`Use ${displayAgentId} subagent for all tasks.`);
@@ -1537,57 +1536,9 @@ const server = http.createServer(async (req, res) => {
1537
1536
  return;
1538
1537
  }
1539
1538
 
1540
- // POST /runs/stream - Create stateless run and stream output (MUST be before generic /runs/:id route)
1539
+ // POST /runs/stream - SSE removed, use WebSocket
1541
1540
  if (pathOnly === '/api/runs/stream' && req.method === 'POST') {
1542
- const body = await parseBody(req);
1543
- const { agent_id, input, config } = body;
1544
- if (!agent_id) {
1545
- sendJSON(req, res, 422, { error: 'agent_id is required' });
1546
- return;
1547
- }
1548
- const agent = discoveredAgents.find(a => a.id === agent_id);
1549
- if (!agent) {
1550
- sendJSON(req, res, 404, { error: 'Agent not found' });
1551
- return;
1552
- }
1553
- const run = queries.createRun(agent_id, null, input, config);
1554
- const sseManager = new SSEStreamManager(res, run.run_id);
1555
- sseManager.start();
1556
- sseManager.sendProgress({ type: 'run_created', run_id: run.run_id });
1557
-
1558
- const eventHandler = (eventData) => {
1559
- if (eventData.sessionId === run.run_id || eventData.conversationId === run.thread_id) {
1560
- if (eventData.type === 'streaming_progress' && eventData.block) {
1561
- sseManager.sendProgress(eventData.block);
1562
- } else if (eventData.type === 'streaming_error') {
1563
- sseManager.sendError(eventData.error || 'Execution error');
1564
- } else if (eventData.type === 'streaming_complete') {
1565
- sseManager.sendComplete({ eventCount: eventData.eventCount }, { timestamp: eventData.timestamp });
1566
- sseManager.cleanup();
1567
- }
1568
- }
1569
- };
1570
-
1571
- sseStreamHandlers.set(run.run_id, eventHandler);
1572
- req.on('close', () => {
1573
- sseStreamHandlers.delete(run.run_id);
1574
- sseManager.cleanup();
1575
- });
1576
-
1577
- const statelessThreadId = queries.getRun(run.run_id)?.thread_id;
1578
- if (statelessThreadId) {
1579
- const conv = queries.getConversation(statelessThreadId);
1580
- if (conv && input?.content) {
1581
- const statelessSession = queries.createSession(statelessThreadId);
1582
- queries.updateRunStatus(run.run_id, 'active');
1583
- activeExecutions.set(statelessThreadId, { pid: null, startTime: Date.now(), sessionId: statelessSession.id, lastActivity: Date.now() });
1584
- activeProcessesByRunId.set(run.run_id, { threadId: statelessThreadId, sessionId: statelessSession.id });
1585
- queries.setIsStreaming(statelessThreadId, true);
1586
- processMessageWithStreaming(statelessThreadId, null, statelessSession.id, input.content, agent_id, config?.model || null)
1587
- .then(() => { queries.updateRunStatus(run.run_id, 'success'); activeProcessesByRunId.delete(run.run_id); })
1588
- .catch((err) => { queries.updateRunStatus(run.run_id, 'error'); activeProcessesByRunId.delete(run.run_id); sseManager.sendError(err.message); sseManager.cleanup(); });
1589
- }
1590
- }
1541
+ res.writeHead(410); res.end(JSON.stringify({ error: 'SSE removed, use WebSocket' }));
1591
1542
  return;
1592
1543
  }
1593
1544
 
@@ -2346,35 +2297,7 @@ const server = http.createServer(async (req, res) => {
2346
2297
 
2347
2298
  const runStreamMatch = pathOnly.match(/^\/api\/runs\/([^/]+)\/stream$/);
2348
2299
  if (runStreamMatch && req.method === 'GET') {
2349
- const runId = runStreamMatch[1];
2350
- const run = queries.getRun(runId);
2351
- if (!run) {
2352
- sendJSON(req, res, 404, { error: 'Run not found' });
2353
- return;
2354
- }
2355
-
2356
- const sseManager = new SSEStreamManager(res, runId);
2357
- sseManager.start();
2358
- sseManager.sendProgress({ type: 'joined', run_id: runId });
2359
-
2360
- const eventHandler = (eventData) => {
2361
- if (eventData.sessionId === runId || eventData.conversationId === run.thread_id) {
2362
- if (eventData.type === 'streaming_progress' && eventData.block) {
2363
- sseManager.sendProgress(eventData.block);
2364
- } else if (eventData.type === 'streaming_error') {
2365
- sseManager.sendError(eventData.error || 'Execution error');
2366
- } else if (eventData.type === 'streaming_complete') {
2367
- sseManager.sendComplete({ eventCount: eventData.eventCount }, { timestamp: eventData.timestamp });
2368
- sseManager.cleanup();
2369
- }
2370
- }
2371
- };
2372
-
2373
- sseStreamHandlers.set(runId, eventHandler);
2374
- req.on('close', () => {
2375
- sseStreamHandlers.delete(runId);
2376
- sseManager.cleanup();
2377
- });
2300
+ res.writeHead(410); res.end(JSON.stringify({ error: 'SSE removed, use WebSocket' }));
2378
2301
  return;
2379
2302
  }
2380
2303
 
@@ -3220,112 +3143,17 @@ const server = http.createServer(async (req, res) => {
3220
3143
  return;
3221
3144
  }
3222
3145
 
3223
- // POST /threads/{thread_id}/runs/stream - Create run on thread and stream output
3146
+ // POST /threads/{thread_id}/runs/stream - SSE removed, use WebSocket
3224
3147
  const threadRunsStreamMatch = pathOnly.match(/^\/api\/threads\/([a-f0-9-]{36})\/runs\/stream$/);
3225
3148
  if (threadRunsStreamMatch && req.method === 'POST') {
3226
- const threadId = threadRunsStreamMatch[1];
3227
- try {
3228
- const body = await parseBody(req);
3229
- const { agent_id, input, config } = body;
3230
-
3231
- const thread = queries.getThread(threadId);
3232
- if (!thread) {
3233
- sendJSON(req, res, 404, { error: 'Thread not found', type: 'not_found' });
3234
- return;
3235
- }
3236
-
3237
- if (thread.status !== 'idle') {
3238
- sendJSON(req, res, 409, { error: 'Thread has pending runs', type: 'conflict' });
3239
- return;
3240
- }
3241
-
3242
- const agent = discoveredAgents.find(a => a.id === agent_id);
3243
- if (!agent) {
3244
- sendJSON(req, res, 404, { error: 'Agent not found', type: 'not_found' });
3245
- return;
3246
- }
3247
-
3248
- const run = queries.createRun(agent_id, threadId, input, config);
3249
- const sseManager = new SSEStreamManager(res, run.run_id);
3250
- sseManager.start();
3251
- sseManager.sendProgress({ type: 'run_created', run_id: run.run_id, thread_id: threadId });
3252
-
3253
- const eventHandler = (eventData) => {
3254
- if (eventData.sessionId === run.run_id || eventData.conversationId === threadId) {
3255
- if (eventData.type === 'streaming_progress' && eventData.block) {
3256
- sseManager.sendProgress(eventData.block);
3257
- } else if (eventData.type === 'streaming_error') {
3258
- sseManager.sendError(eventData.error || 'Execution error');
3259
- } else if (eventData.type === 'streaming_complete') {
3260
- sseManager.sendComplete({ eventCount: eventData.eventCount }, { timestamp: eventData.timestamp });
3261
- sseManager.cleanup();
3262
- }
3263
- }
3264
- };
3265
-
3266
- sseStreamHandlers.set(run.run_id, eventHandler);
3267
- req.on('close', () => {
3268
- sseStreamHandlers.delete(run.run_id);
3269
- sseManager.cleanup();
3270
- });
3271
-
3272
- const conv = queries.getConversation(threadId);
3273
- if (conv && input?.content) {
3274
- const session = queries.createSession(threadId);
3275
- queries.updateRunStatus(run.run_id, 'active');
3276
- activeExecutions.set(threadId, { pid: null, startTime: Date.now(), sessionId: session.id, lastActivity: Date.now() });
3277
- activeProcessesByRunId.set(run.run_id, { threadId, sessionId: session.id });
3278
- queries.setIsStreaming(threadId, true);
3279
- processMessageWithStreaming(threadId, null, session.id, input.content, agent_id, config?.model || null)
3280
- .then(() => { queries.updateRunStatus(run.run_id, 'success'); activeProcessesByRunId.delete(run.run_id); })
3281
- .catch((err) => { queries.updateRunStatus(run.run_id, 'error'); activeProcessesByRunId.delete(run.run_id); sseManager.sendError(err.message); sseManager.cleanup(); });
3282
- }
3283
- } catch (err) {
3284
- sendJSON(req, res, 422, { error: err.message, type: 'validation_error' });
3285
- }
3149
+ res.writeHead(410); res.end(JSON.stringify({ error: 'SSE removed, use WebSocket' }));
3286
3150
  return;
3287
3151
  }
3288
3152
 
3289
- // GET /threads/{thread_id}/runs/{run_id}/stream - Stream output from run on thread
3153
+ // GET /threads/{thread_id}/runs/{run_id}/stream - SSE removed, use WebSocket
3290
3154
  const threadRunStreamMatch = pathOnly.match(/^\/api\/threads\/([a-f0-9-]{36})\/runs\/([a-f0-9-]{36})\/stream$/);
3291
3155
  if (threadRunStreamMatch && req.method === 'GET') {
3292
- const threadId = threadRunStreamMatch[1];
3293
- const runId = threadRunStreamMatch[2];
3294
-
3295
- const thread = queries.getThread(threadId);
3296
- if (!thread) {
3297
- sendJSON(req, res, 404, { error: 'Thread not found', type: 'not_found' });
3298
- return;
3299
- }
3300
-
3301
- const run = queries.getRun(runId);
3302
- if (!run || run.thread_id !== threadId) {
3303
- sendJSON(req, res, 404, { error: 'Run not found on thread', type: 'not_found' });
3304
- return;
3305
- }
3306
-
3307
- const sseManager = new SSEStreamManager(res, runId);
3308
- sseManager.start();
3309
- sseManager.sendProgress({ type: 'joined', run_id: runId, thread_id: threadId });
3310
-
3311
- const eventHandler = (eventData) => {
3312
- if (eventData.sessionId === runId || eventData.conversationId === threadId) {
3313
- if (eventData.type === 'streaming_progress' && eventData.block) {
3314
- sseManager.sendProgress(eventData.block);
3315
- } else if (eventData.type === 'streaming_error') {
3316
- sseManager.sendError(eventData.error || 'Execution error');
3317
- } else if (eventData.type === 'streaming_complete') {
3318
- sseManager.sendComplete({ eventCount: eventData.eventCount }, { timestamp: eventData.timestamp });
3319
- sseManager.cleanup();
3320
- }
3321
- }
3322
- };
3323
-
3324
- sseStreamHandlers.set(runId, eventHandler);
3325
- req.on('close', () => {
3326
- sseStreamHandlers.delete(runId);
3327
- sseManager.cleanup();
3328
- });
3156
+ res.writeHead(410); res.end(JSON.stringify({ error: 'SSE removed, use WebSocket' }));
3329
3157
  return;
3330
3158
  }
3331
3159
 
@@ -4102,7 +3930,6 @@ wss.on('error', (err) => {
4102
3930
  const hotReloadClients = [];
4103
3931
  const syncClients = new Set();
4104
3932
  const subscriptionIndex = new Map();
4105
- const sseStreamHandlers = new Map();
4106
3933
  const pm2Subscribers = new Set();
4107
3934
 
4108
3935
  wss.on('connection', (ws, req) => {
@@ -4117,7 +3944,7 @@ wss.on('connection', (ws, req) => {
4117
3944
  ws.subscriptions = new Set();
4118
3945
  ws.clientId = `client-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
4119
3946
 
4120
- ws.send(JSON.stringify({
3947
+ sendWs(ws, ({
4121
3948
  type: 'sync_connected',
4122
3949
  clientId: ws.clientId,
4123
3950
  timestamp: Date.now()
@@ -4187,11 +4014,6 @@ function broadcastSync(event) {
4187
4014
  }
4188
4015
  }
4189
4016
 
4190
- if (sseStreamHandlers.size > 0) {
4191
- for (const [runId, handler] of sseStreamHandlers.entries()) {
4192
- try { handler(event); } catch (e) {}
4193
- }
4194
- }
4195
4017
  } catch (err) {
4196
4018
  console.error('[BROADCAST] Error (contained):', err.message);
4197
4019
  }
@@ -4242,7 +4064,7 @@ wsRouter.onLegacy((data, ws) => {
4242
4064
  }
4243
4065
  const subTarget = data.sessionId || data.conversationId;
4244
4066
  debugLog(`[WebSocket] Client ${ws.clientId} subscribed to ${subTarget}`);
4245
- ws.send(JSON.stringify({
4067
+ sendWs(ws, ({
4246
4068
  type: 'subscription_confirmed',
4247
4069
  sessionId: data.sessionId,
4248
4070
  conversationId: data.conversationId,
@@ -4253,7 +4075,7 @@ wsRouter.onLegacy((data, ws) => {
4253
4075
  if (data.conversationId && activeExecutions.has(data.conversationId)) {
4254
4076
  const execution = activeExecutions.get(data.conversationId);
4255
4077
  const conv = queries.getConversation(data.conversationId);
4256
- ws.send(JSON.stringify({
4078
+ sendWs(ws, ({
4257
4079
  type: 'streaming_start',
4258
4080
  sessionId: execution.sessionId,
4259
4081
  conversationId: data.conversationId,
@@ -4271,7 +4093,7 @@ wsRouter.onLegacy((data, ws) => {
4271
4093
 
4272
4094
  const latestSession = queries.getLatestSession(data.conversationId);
4273
4095
  if (latestSession) {
4274
- ws.send(JSON.stringify({
4096
+ sendWs(ws, ({
4275
4097
  type: 'streaming_resumed',
4276
4098
  sessionId: latestSession.id,
4277
4099
  conversationId: data.conversationId,
@@ -4282,7 +4104,7 @@ wsRouter.onLegacy((data, ws) => {
4282
4104
  }));
4283
4105
 
4284
4106
  checkpointManager.injectCheckpointEvents(latestSession.id, checkpoint, (evt) => {
4285
- ws.send(JSON.stringify({
4107
+ sendWs(ws, ({
4286
4108
  ...evt,
4287
4109
  sessionId: latestSession.id,
4288
4110
  conversationId: data.conversationId
@@ -4305,7 +4127,7 @@ wsRouter.onLegacy((data, ws) => {
4305
4127
  }
4306
4128
  debugLog(`[WebSocket] Client ${ws.clientId} unsubscribed from ${data.sessionId || data.conversationId}`);
4307
4129
  } else if (data.type === 'get_subscriptions') {
4308
- ws.send(JSON.stringify({
4130
+ sendWs(ws, ({
4309
4131
  type: 'subscriptions',
4310
4132
  subscriptions: Array.from(ws.subscriptions),
4311
4133
  timestamp: Date.now()
@@ -4317,7 +4139,7 @@ wsRouter.onLegacy((data, ws) => {
4317
4139
  ws.latencyAvg = data.avg || 0;
4318
4140
  ws.latencyTrend = data.trend || 'stable';
4319
4141
  } else if (data.type === 'ping') {
4320
- ws.send(JSON.stringify({
4142
+ sendWs(ws, ({
4321
4143
  type: 'pong',
4322
4144
  requestId: data.requestId,
4323
4145
  timestamp: Date.now()
@@ -4340,18 +4162,18 @@ wsRouter.onLegacy((data, ws) => {
4340
4162
  ws.terminalProc = proc;
4341
4163
  ws.terminalPty = true;
4342
4164
  proc.on('data', (chunk) => {
4343
- if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'terminal_output', data: Buffer.from(chunk).toString('base64'), encoding: 'base64' }));
4165
+ if (ws.readyState === 1) sendWs(ws, ({ type: 'terminal_output', data: Buffer.from(chunk).toString('base64'), encoding: 'base64' }));
4344
4166
  });
4345
4167
  proc.on('exit', (code) => {
4346
- if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'terminal_exit', code }));
4168
+ if (ws.readyState === 1) sendWs(ws, { type: 'terminal_exit', code });
4347
4169
  ws.terminalProc = null;
4348
4170
  });
4349
4171
  proc.on('error', (err) => {
4350
4172
  console.error('[TERMINAL] PTY error (contained):', err.message);
4351
- if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'terminal_exit', code: 1, error: err.message }));
4173
+ if (ws.readyState === 1) sendWs(ws, { type: 'terminal_exit', code: 1, error: err.message });
4352
4174
  ws.terminalProc = null;
4353
4175
  });
4354
- ws.send(JSON.stringify({ type: 'terminal_started', timestamp: Date.now() }));
4176
+ sendWs(ws, ({ type: 'terminal_started', timestamp: Date.now() }));
4355
4177
  } catch (e) {
4356
4178
  console.error('[TERMINAL] Failed to spawn PTY, falling back to pipes:', e.message);
4357
4179
  const { spawn } = require('child_process');
@@ -4361,24 +4183,24 @@ wsRouter.onLegacy((data, ws) => {
4361
4183
  ws.terminalProc = proc;
4362
4184
  ws.terminalPty = false;
4363
4185
  proc.stdout.on('data', (chunk) => {
4364
- if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'terminal_output', data: chunk.toString('base64'), encoding: 'base64' }));
4186
+ if (ws.readyState === 1) sendWs(ws, ({ type: 'terminal_output', data: chunk.toString('base64'), encoding: 'base64' }));
4365
4187
  });
4366
4188
  proc.stderr.on('data', (chunk) => {
4367
- if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'terminal_output', data: chunk.toString('base64'), encoding: 'base64' }));
4189
+ if (ws.readyState === 1) sendWs(ws, ({ type: 'terminal_output', data: chunk.toString('base64'), encoding: 'base64' }));
4368
4190
  });
4369
4191
  proc.on('exit', (code) => {
4370
- if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'terminal_exit', code }));
4192
+ if (ws.readyState === 1) sendWs(ws, { type: 'terminal_exit', code });
4371
4193
  ws.terminalProc = null;
4372
4194
  });
4373
4195
  proc.on('error', (err) => {
4374
4196
  console.error('[TERMINAL] Spawn error (contained):', err.message);
4375
- if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'terminal_exit', code: 1, error: err.message }));
4197
+ if (ws.readyState === 1) sendWs(ws, { type: 'terminal_exit', code: 1, error: err.message });
4376
4198
  ws.terminalProc = null;
4377
4199
  });
4378
4200
  proc.stdin.on('error', () => {});
4379
4201
  proc.stdout.on('error', () => {});
4380
4202
  proc.stderr.on('error', () => {});
4381
- ws.send(JSON.stringify({ type: 'terminal_started', timestamp: Date.now() }));
4203
+ sendWs(ws, ({ type: 'terminal_started', timestamp: Date.now() }));
4382
4204
  }
4383
4205
  } else if (data.type === 'terminal_input') {
4384
4206
  if (ws.terminalProc) {
@@ -4407,56 +4229,56 @@ wsRouter.onLegacy((data, ws) => {
4407
4229
  }
4408
4230
  } else if (data.type === 'pm2_list') {
4409
4231
  if (!pm2Manager.connected) {
4410
- if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'pm2_unavailable', reason: 'PM2 not connected', timestamp: Date.now() }));
4232
+ if (ws.readyState === 1) sendWs(ws, ({ type: 'pm2_unavailable', reason: 'PM2 not connected', timestamp: Date.now() }));
4411
4233
  } else {
4412
4234
  pm2Manager.listProcesses().then(processes => {
4413
4235
  if (ws.readyState === 1) {
4414
4236
  const hasActive = processes.some(p => ['online','launching','stopping','waiting restart'].includes(p.status));
4415
- ws.send(JSON.stringify({ type: 'pm2_list_response', processes, hasActive }));
4237
+ sendWs(ws, { type: 'pm2_list_response', processes, hasActive });
4416
4238
  }
4417
4239
  }).catch(() => {
4418
- if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'pm2_unavailable', reason: 'list failed', timestamp: Date.now() }));
4240
+ if (ws.readyState === 1) sendWs(ws, ({ type: 'pm2_unavailable', reason: 'list failed', timestamp: Date.now() }));
4419
4241
  });
4420
4242
  }
4421
4243
  } else if (data.type === 'pm2_start_monitoring') {
4422
4244
  pm2Subscribers.add(ws);
4423
4245
  ws.pm2Subscribed = true;
4424
4246
  if (!pm2Manager.connected) {
4425
- if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'pm2_unavailable', reason: 'PM2 not connected', timestamp: Date.now() }));
4247
+ if (ws.readyState === 1) sendWs(ws, ({ type: 'pm2_unavailable', reason: 'PM2 not connected', timestamp: Date.now() }));
4426
4248
  } else {
4427
- ws.send(JSON.stringify({ type: 'pm2_monitoring_started' }));
4249
+ sendWs(ws, { type: 'pm2_monitoring_started' });
4428
4250
  }
4429
4251
  } else if (data.type === 'pm2_stop_monitoring') {
4430
4252
  pm2Subscribers.delete(ws);
4431
4253
  ws.pm2Subscribed = false;
4432
- ws.send(JSON.stringify({ type: 'pm2_monitoring_stopped' }));
4254
+ sendWs(ws, { type: 'pm2_monitoring_stopped' });
4433
4255
  } else if (data.type === 'pm2_start') {
4434
4256
  pm2Manager.startProcess(data.name).then(result => {
4435
- ws.send(JSON.stringify({ type: 'pm2_start_response', name: data.name, ...result }));
4257
+ sendWs(ws, { type: 'pm2_start_response', name: data.name, ...result });
4436
4258
  });
4437
4259
  } else if (data.type === 'pm2_stop') {
4438
4260
  pm2Manager.stopProcess(data.name).then(result => {
4439
- ws.send(JSON.stringify({ type: 'pm2_stop_response', name: data.name, ...result }));
4261
+ sendWs(ws, { type: 'pm2_stop_response', name: data.name, ...result });
4440
4262
  });
4441
4263
  } else if (data.type === 'pm2_restart') {
4442
4264
  pm2Manager.restartProcess(data.name).then(result => {
4443
- ws.send(JSON.stringify({ type: 'pm2_restart_response', name: data.name, ...result }));
4265
+ sendWs(ws, { type: 'pm2_restart_response', name: data.name, ...result });
4444
4266
  });
4445
4267
  } else if (data.type === 'pm2_delete') {
4446
4268
  pm2Manager.deleteProcess(data.name).then(result => {
4447
- ws.send(JSON.stringify({ type: 'pm2_delete_response', name: data.name, ...result }));
4269
+ sendWs(ws, { type: 'pm2_delete_response', name: data.name, ...result });
4448
4270
  });
4449
4271
  } else if (data.type === 'pm2_logs') {
4450
4272
  pm2Manager.getLogs(data.name, { lines: data.lines || 100 }).then(result => {
4451
- ws.send(JSON.stringify({ type: 'pm2_logs_response', name: data.name, ...result }));
4273
+ sendWs(ws, { type: 'pm2_logs_response', name: data.name, ...result });
4452
4274
  });
4453
4275
  } else if (data.type === 'pm2_flush_logs') {
4454
4276
  pm2Manager.flushLogs(data.name).then(result => {
4455
- ws.send(JSON.stringify({ type: 'pm2_flush_logs_response', name: data.name, ...result }));
4277
+ sendWs(ws, { type: 'pm2_flush_logs_response', name: data.name, ...result });
4456
4278
  });
4457
4279
  } else if (data.type === 'pm2_ping') {
4458
4280
  pm2Manager.ping().then(result => {
4459
- ws.send(JSON.stringify({ type: 'pm2_ping_response', ...result }));
4281
+ sendWs(ws, { type: 'pm2_ping_response', ...result });
4460
4282
  });
4461
4283
  }
4462
4284
 
package/static/index.html CHANGED
@@ -3250,7 +3250,6 @@
3250
3250
  <script defer src="/gm/js/streaming-renderer.js"></script>
3251
3251
  <script defer src="/gm/js/image-loader.js"></script>
3252
3252
  <script src="/gm/lib/msgpackr.min.js"></script>
3253
- <script defer src="/gm/js/event-consolidator.js"></script>
3254
3253
  <script defer src="/gm/js/websocket-manager.js"></script>
3255
3254
  <script defer src="/gm/js/ws-client.js"></script>
3256
3255
  <script defer src="/gm/js/syntax-highlighter.js"></script>
@@ -80,7 +80,6 @@ class AgentGUIClient {
80
80
  this._scrollAnimating = false;
81
81
  this._scrollLerpFactor = config.scrollAnimationSpeed || 0.15;
82
82
 
83
- this._consolidator = typeof EventConsolidator !== 'undefined' ? new EventConsolidator() : null;
84
83
 
85
84
  this._serverProcessingEstimate = 2000;
86
85
  this._lastSendTime = 0;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Client-side codec — mirrors lib/codec.js exactly.
3
+ * msgpackr (global from msgpackr.min.js) + GPT tokenizer BPE compression.
4
+ */
5
+ import { encode as tokEncode, decode as tokDecode } from 'https://esm.sh/gpt-tokenizer';
6
+
7
+ const THRESHOLD = 200;
8
+ const COMPRESSIBLE = new Set(['content', 'text', 'output', 'response', 'prompt', 'input', 'data']);
9
+
10
+ function compressText(str) {
11
+ return { __tok: true, d: tokEncode(str) };
12
+ }
13
+
14
+ function decompressText(val) {
15
+ return tokDecode(val.d);
16
+ }
17
+
18
+ function encodeObj(obj) {
19
+ if (!obj || typeof obj !== 'object') return obj;
20
+ if (Array.isArray(obj)) return obj.map(encodeObj);
21
+ const out = {};
22
+ for (const k of Object.keys(obj)) {
23
+ const v = obj[k];
24
+ if (COMPRESSIBLE.has(k) && typeof v === 'string' && v.length > THRESHOLD) {
25
+ out[k] = compressText(v);
26
+ } else if (v && typeof v === 'object' && !(v instanceof ArrayBuffer) && !ArrayBuffer.isView(v)) {
27
+ out[k] = encodeObj(v);
28
+ } else {
29
+ out[k] = v;
30
+ }
31
+ }
32
+ return out;
33
+ }
34
+
35
+ function decodeObj(obj) {
36
+ if (!obj || typeof obj !== 'object') return obj;
37
+ if (Array.isArray(obj)) return obj.map(decodeObj);
38
+ if (obj.__tok && obj.d) return decompressText(obj);
39
+ const out = {};
40
+ for (const k of Object.keys(obj)) {
41
+ out[k] = decodeObj(obj[k]);
42
+ }
43
+ return out;
44
+ }
45
+
46
+ export function encode(obj) { return msgpackr.pack(encodeObj(obj)); }
47
+ export function decode(buf) { return decodeObj(msgpackr.unpack(new Uint8Array(buf instanceof ArrayBuffer ? buf : buf))); }
@@ -1,3 +1,6 @@
1
+ // codec is loaded as ES module and exposed globally by ws-client.js
2
+ // or inline: import('./codec.js').then(m => window._codec = m)
3
+
1
4
  class WebSocketManager {
2
5
  constructor(config = {}) {
3
6
  this.config = {
@@ -133,16 +136,8 @@ class WebSocketManager {
133
136
  async onMessage(event) {
134
137
  try {
135
138
  let parsed;
136
- if (event.data instanceof Blob || event.data instanceof ArrayBuffer) {
137
- // Binary frame: msgpackr-encoded (perMessageDeflate decompressed by browser)
138
- const buf = event.data instanceof Blob
139
- ? await event.data.arrayBuffer()
140
- : event.data;
141
- parsed = msgpackr.unpack(new Uint8Array(buf));
142
- } else {
143
- // Fallback: plain JSON (ping/pong control frames, legacy)
144
- parsed = JSON.parse(event.data);
145
- }
139
+ const buf = event.data instanceof Blob ? await event.data.arrayBuffer() : event.data;
140
+ parsed = window._codec ? window._codec.decode(buf) : msgpackr.unpack(new Uint8Array(buf));
146
141
  const messages = Array.isArray(parsed) ? parsed : [parsed];
147
142
  this.stats.totalMessagesReceived += messages.length;
148
143
 
@@ -443,7 +438,7 @@ class WebSocketManager {
443
438
  }
444
439
 
445
440
  try {
446
- this.ws.send(msgpackr.pack(data));
441
+ this.ws.send(window._codec ? window._codec.encode(data) : msgpackr.pack(data));
447
442
  this.stats.totalMessagesSent++;
448
443
  return true;
449
444
  } catch (error) {
@@ -467,7 +462,7 @@ class WebSocketManager {
467
462
  this.messageBuffer = [];
468
463
  for (const message of messages) {
469
464
  try {
470
- this.ws.send(msgpackr.pack(message));
465
+ this.ws.send(window._codec ? window._codec.encode(message) : msgpackr.pack(message));
471
466
  this.stats.totalMessagesSent++;
472
467
  } catch (error) {
473
468
  this.bufferMessage(message);
@@ -491,7 +486,7 @@ class WebSocketManager {
491
486
  if (type === 'session') msg.sessionId = id;
492
487
  else msg.conversationId = id;
493
488
  try {
494
- this.ws.send(msgpackr.pack(msg));
489
+ this.ws.send(window._codec ? window._codec.encode(msg) : msgpackr.pack(msg));
495
490
  this.stats.totalMessagesSent++;
496
491
  } catch (_) {}
497
492
  }
@@ -78,10 +78,12 @@ class WsClient {
78
78
 
79
79
  window.WsClient = WsClient;
80
80
 
81
- try {
81
+ // Bootstrap: load codec (which pulls gpt-tokenizer from esm.sh), then connect
82
+ import('./codec.js').then(codec => {
83
+ window._codec = codec;
82
84
  window.wsManager = new WebSocketManager();
83
85
  window.wsClient = new WsClient(window.wsManager);
84
86
  window.wsManager.connect().catch(function() {});
85
- } catch (e) {
86
- console.error('[ws-client] Failed to initialize:', e);
87
- }
87
+ }).catch(e => {
88
+ console.error('[ws-client] Failed to load codec:', e);
89
+ });
package/lib/sse-stream.js DELETED
@@ -1,125 +0,0 @@
1
- import crypto from 'crypto';
2
-
3
- export function formatSSEEvent(eventType, data) {
4
- const lines = [];
5
- if (eventType) {
6
- lines.push(`event: ${eventType}`);
7
- }
8
- if (data) {
9
- const jsonData = typeof data === 'string' ? data : JSON.stringify(data);
10
- lines.push(`data: ${jsonData}`);
11
- }
12
- lines.push('');
13
- return lines.join('\n') + '\n';
14
- }
15
-
16
- export function convertToACPRunOutputStream(sessionId, block, runStatus = 'active') {
17
- const eventId = crypto.randomUUID();
18
- return {
19
- id: eventId,
20
- event: 'agent_event',
21
- data: {
22
- type: 'custom',
23
- run_id: sessionId,
24
- status: runStatus,
25
- update: block
26
- }
27
- };
28
- }
29
-
30
- export function createErrorEvent(runId, errorMessage, errorCode = 'execution_error') {
31
- const eventId = crypto.randomUUID();
32
- return {
33
- id: eventId,
34
- event: 'agent_event',
35
- data: {
36
- type: 'error',
37
- run_id: runId,
38
- error: errorMessage,
39
- code: errorCode,
40
- status: 'error'
41
- }
42
- };
43
- }
44
-
45
- export function createCompletionEvent(runId, values = {}, metadata = {}) {
46
- const eventId = crypto.randomUUID();
47
- return {
48
- id: eventId,
49
- event: 'agent_event',
50
- data: {
51
- type: 'result',
52
- run_id: runId,
53
- status: 'completed',
54
- values,
55
- metadata
56
- }
57
- };
58
- }
59
-
60
- export function createKeepAlive() {
61
- return ': ping\n\n';
62
- }
63
-
64
- export class SSEStreamManager {
65
- constructor(res, runId) {
66
- this.res = res;
67
- this.runId = runId;
68
- this.keepAliveInterval = null;
69
- this.closed = false;
70
- }
71
-
72
- start() {
73
- this.res.writeHead(200, {
74
- 'Content-Type': 'text/event-stream',
75
- 'Cache-Control': 'no-cache',
76
- 'Connection': 'keep-alive',
77
- 'X-Accel-Buffering': 'no'
78
- });
79
-
80
- this.keepAliveInterval = setInterval(() => {
81
- if (!this.closed) {
82
- this.writeRaw(createKeepAlive());
83
- }
84
- }, 15000);
85
-
86
- this.res.on('close', () => {
87
- this.cleanup();
88
- });
89
- }
90
-
91
- writeRaw(text) {
92
- if (!this.closed) {
93
- this.res.write(text);
94
- }
95
- }
96
-
97
- sendProgress(block, runStatus = 'active') {
98
- const acpEvent = convertToACPRunOutputStream(this.runId, block, runStatus);
99
- const sse = formatSSEEvent('message', acpEvent.data);
100
- this.writeRaw(sse);
101
- }
102
-
103
- sendError(errorMessage, errorCode = 'execution_error') {
104
- const errorEvent = createErrorEvent(this.runId, errorMessage, errorCode);
105
- const sse = formatSSEEvent('error', errorEvent.data);
106
- this.writeRaw(sse);
107
- }
108
-
109
- sendComplete(values = {}, metadata = {}) {
110
- const completionEvent = createCompletionEvent(this.runId, values, metadata);
111
- const sse = formatSSEEvent('done', completionEvent.data);
112
- this.writeRaw(sse);
113
- }
114
-
115
- cleanup() {
116
- if (this.keepAliveInterval) {
117
- clearInterval(this.keepAliveInterval);
118
- this.keepAliveInterval = null;
119
- }
120
- this.closed = true;
121
- if (!this.res.writableEnded) {
122
- this.res.end();
123
- }
124
- }
125
- }
package/lib/ws-events.js DELETED
@@ -1,20 +0,0 @@
1
- const S2L = {
2
- 's.start': 'streaming_start', 's.prog': 'streaming_progress',
3
- 's.done': 'streaming_complete', 's.err': 'streaming_error',
4
- 's.cancel': 'streaming_cancelled', 'conv.new': 'conversation_created',
5
- 'conv.upd': 'conversation_updated', 'convs.upd': 'conversations_updated',
6
- 'conv.del': 'conversation_deleted', 'msg.new': 'message_created',
7
- 'q.stat': 'queue_status', 'q.upd': 'queue_updated',
8
- 'rl.hit': 'rate_limit_hit', 'rl.clr': 'rate_limit_clear',
9
- 'scr.start': 'script_started', 'scr.stop': 'script_stopped',
10
- 'scr.out': 'script_output', 'mdl.prog': 'model_download_progress',
11
- 'stt.prog': 'stt_progress', 'tts.prog': 'tts_setup_progress',
12
- 'voice.ls': 'voice_list', 'sub.ok': 'subscription_confirmed',
13
- 'term.out': 'terminal_output', 'term.exit': 'terminal_exit',
14
- 'term.start': 'terminal_started'
15
- };
16
- const L2S = Object.fromEntries(Object.entries(S2L).map(([k, v]) => [v, k]));
17
- const toLong = (s) => S2L[s] || s;
18
- const toShort = (l) => L2S[l] || l;
19
- if (typeof window !== 'undefined') window.wsEvents = { S2L, L2S, toLong, toShort };
20
- export { S2L, L2S, toLong, toShort };
@@ -1,100 +0,0 @@
1
- class EventConsolidator {
2
- consolidate(chunks) {
3
- const stats = { original: chunks.length, deduplicated: 0, textMerged: 0, toolsCollapsed: 0, systemSuperseded: 0 };
4
- if (chunks.length <= 1) return { consolidated: chunks, stats };
5
-
6
- const sorted = chunks.slice().sort((a, b) => (a.sequence || 0) - (b.sequence || 0));
7
-
8
- const seen = new Set();
9
- const deduped = [];
10
- for (const c of sorted) {
11
- const key = c.sessionId + ':' + c.sequence;
12
- if (c.sequence !== undefined && seen.has(key)) { stats.deduplicated++; continue; }
13
- if (c.sequence !== undefined) seen.add(key);
14
- deduped.push(c);
15
- }
16
-
17
- const bySession = {};
18
- for (const c of deduped) {
19
- const sid = c.sessionId || '_';
20
- if (!bySession[sid]) bySession[sid] = [];
21
- bySession[sid].push(c);
22
- }
23
-
24
- const result = [];
25
- for (const sid of Object.keys(bySession)) {
26
- const sessionChunks = bySession[sid];
27
- const merged = this._mergeTextBlocks(sessionChunks, stats);
28
- this._collapseToolPairs(merged, stats);
29
- const superseded = this._supersedeSystemBlocks(merged, stats);
30
- result.push(...superseded);
31
- }
32
-
33
- result.sort((a, b) => (a.sequence || 0) - (b.sequence || 0));
34
- return { consolidated: result, stats };
35
- }
36
-
37
- _mergeTextBlocks(chunks, stats) {
38
- const result = [];
39
- let pending = null;
40
- const MAX_MERGE = 50 * 1024;
41
-
42
- for (const c of chunks) {
43
- if (c.block?.type === 'text') {
44
- if (pending) {
45
- const pendingText = pending.block.text || '';
46
- const newText = c.block.text || '';
47
- const combined = pendingText + newText;
48
- if (combined.length <= MAX_MERGE) {
49
- pending = {
50
- ...pending,
51
- block: { ...pending.block, text: combined },
52
- created_at: c.created_at,
53
- _mergedSequences: [...(pending._mergedSequences || [pending.sequence]), c.sequence]
54
- };
55
- stats.textMerged++;
56
- continue;
57
- }
58
- }
59
- if (pending) result.push(pending);
60
- pending = { ...c, _mergedSequences: [c.sequence] };
61
- } else {
62
- if (pending) { result.push(pending); pending = null; }
63
- result.push(c);
64
- }
65
- }
66
- if (pending) result.push(pending);
67
- return result;
68
- }
69
-
70
- _collapseToolPairs(chunks, stats) {
71
- const toolUseMap = {};
72
- for (const c of chunks) {
73
- if (c.block?.type === 'tool_use' && c.block.id) toolUseMap[c.block.id] = c;
74
- }
75
- for (const c of chunks) {
76
- if (c.block?.type === 'tool_result' && c.block.tool_use_id) {
77
- const match = toolUseMap[c.block.tool_use_id];
78
- if (match) {
79
- match.block._hasResult = true;
80
- c.block._collapsed = true;
81
- stats.toolsCollapsed++;
82
- }
83
- }
84
- }
85
- }
86
-
87
- _supersedeSystemBlocks(chunks, stats) {
88
- const systemIndices = [];
89
- for (let i = 0; i < chunks.length; i++) {
90
- if (chunks[i].block?.type === 'system') systemIndices.push(i);
91
- }
92
- if (systemIndices.length <= 1) return chunks;
93
- const keep = new Set();
94
- keep.add(systemIndices[systemIndices.length - 1]);
95
- stats.systemSuperseded += systemIndices.length - 1;
96
- return chunks.filter((_, i) => !systemIndices.includes(i) || keep.has(i));
97
- }
98
- }
99
-
100
- if (typeof module !== 'undefined' && module.exports) module.exports = EventConsolidator;