agentgui 1.0.653 → 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.653",
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
 
@@ -2208,13 +2159,13 @@ const server = http.createServer(async (req, res) => {
2208
2159
  if (modelsMatch && req.method === 'GET') {
2209
2160
  const agentId = modelsMatch[1];
2210
2161
  const cached = modelCache.get(agentId);
2211
- if (cached && (Date.now() - cached.ts) < 300000) {
2162
+ if (cached && (Date.now() - cached.timestamp) < 300000) {
2212
2163
  sendJSON(req, res, 200, { models: cached.models });
2213
2164
  return;
2214
2165
  }
2215
2166
  try {
2216
2167
  const models = await getModelsForAgent(agentId);
2217
- modelCache.set(agentId, { models, ts: Date.now() });
2168
+ modelCache.set(agentId, { models, timestamp: Date.now() });
2218
2169
  sendJSON(req, res, 200, { models });
2219
2170
  } catch (err) {
2220
2171
  sendJSON(req, res, 200, { models: [] });
@@ -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))); }
@@ -2,10 +2,7 @@
2
2
  var activeDialogs = [];
3
3
  var dialogZIndex = 10000;
4
4
 
5
- function escapeHtml(text) {
6
- var map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
7
- return String(text).replace(/[&<>"']/g, function(c) { return map[c]; });
8
- }
5
+ function escapeHtml(text) { return window._escHtml(text); }
9
6
 
10
7
  function createOverlay() {
11
8
  var overlay = document.createElement('div');
@@ -1,71 +1,10 @@
1
- /**
2
- * Event Processor
3
- * Transforms, validates, and enriches streaming events
4
- * Handles ANSI colors, markdown, diffs, and other data transformations
5
- */
6
-
7
1
  class EventProcessor {
8
2
  constructor(config = {}) {
9
3
  this.config = {
10
4
  enableSyntaxHighlight: config.enableSyntaxHighlight !== false,
11
- enableMarkdown: config.enableMarkdown !== false,
12
- enableANSI: config.enableANSI !== false,
13
- maxContentLength: config.maxContentLength || 100000,
14
5
  ...config
15
6
  };
16
7
 
17
- // ANSI color codes mapping
18
- this.ansiCodes = {
19
- reset: '\x1b[0m',
20
- bold: '\x1b[1m',
21
- dim: '\x1b[2m',
22
- italic: '\x1b[3m',
23
- underline: '\x1b[4m',
24
- blink: '\x1b[5m',
25
- reverse: '\x1b[7m',
26
- hidden: '\x1b[8m',
27
- strikethrough: '\x1b[9m',
28
- // Foreground colors
29
- black: '\x1b[30m',
30
- red: '\x1b[31m',
31
- green: '\x1b[32m',
32
- yellow: '\x1b[33m',
33
- blue: '\x1b[34m',
34
- magenta: '\x1b[35m',
35
- cyan: '\x1b[36m',
36
- white: '\x1b[37m',
37
- // Background colors
38
- bgBlack: '\x1b[40m',
39
- bgRed: '\x1b[41m',
40
- bgGreen: '\x1b[42m',
41
- bgYellow: '\x1b[43m',
42
- bgBlue: '\x1b[44m',
43
- bgMagenta: '\x1b[45m',
44
- bgCyan: '\x1b[46m',
45
- bgWhite: '\x1b[47m'
46
- };
47
-
48
- // CSS color mapping
49
- this.colorMap = {
50
- '30': '#000000', // black
51
- '31': '#ff6b6b', // red
52
- '32': '#51cf66', // green
53
- '33': '#ffd43b', // yellow
54
- '34': '#4dabf7', // blue
55
- '35': '#da77f2', // magenta
56
- '36': '#20c997', // cyan
57
- '37': '#ffffff', // white
58
- '90': '#666666', // bright black
59
- '91': '#ff8787', // bright red
60
- '92': '#69db7c', // bright green
61
- '93': '#ffe066', // bright yellow
62
- '94': '#74c0fc', // bright blue
63
- '95': '#e599f7', // bright magenta
64
- '96': '#38f9d7', // bright cyan
65
- '97': '#f8f9fa' // bright white
66
- };
67
-
68
- // Statistics
69
8
  this.stats = {
70
9
  totalEvents: 0,
71
10
  processedEvents: 0,
@@ -103,29 +42,13 @@ class EventProcessor {
103
42
  processTime: 0
104
43
  };
105
44
 
106
- // Transform event based on type
107
- if (event.type === 'text_block' || event.type === 'code_block') {
108
- processed.content = this.transformContent(event.content || '', event.type);
109
- this.stats.transformedEvents++;
110
- }
111
-
112
- if (event.type === 'command_execute' && event.output) {
113
- processed.output = this.transformANSI(event.output);
114
- this.stats.transformedEvents++;
115
- }
116
-
117
- if (event.type === 'file_diff' || event.type === 'git_diff') {
118
- processed.diff = this.transformDiff(event.diff || event.content || '');
119
- this.stats.transformedEvents++;
120
- }
121
-
122
45
  if (event.type === 'file_read' && event.path && this.isImagePath(event.path)) {
123
46
  processed.isImage = true;
124
47
  processed.imagePath = event.path;
125
48
  this.stats.transformedEvents++;
126
49
  }
127
50
 
128
- if ((event.type === 'text_block' || event.type === 'command_execute' || event.type === 'streaming_progress') && event.content || event.output) {
51
+ if ((event.type === 'text_block' || event.type === 'command_execute' || event.type === 'streaming_progress') && (event.content || event.output)) {
129
52
  const imagePaths = this.extractImagePaths(event.content || event.output || '');
130
53
  if (imagePaths.length > 0) {
131
54
  processed.detectedImages = imagePaths;
@@ -178,151 +101,6 @@ class EventProcessor {
178
101
  return true;
179
102
  }
180
103
 
181
- /**
182
- * Transform content based on type
183
- */
184
- transformContent(content, type) {
185
- if (typeof content !== 'string') {
186
- return content;
187
- }
188
-
189
- if (content.length > this.config.maxContentLength) {
190
- return content.substring(0, this.config.maxContentLength) + '\n... (truncated)';
191
- }
192
-
193
- return content;
194
- }
195
-
196
- /**
197
- * Transform ANSI escape codes to HTML/CSS
198
- */
199
- transformANSI(text) {
200
- if (!this.config.enableANSI || typeof text !== 'string') {
201
- return text;
202
- }
203
-
204
- let result = '';
205
- let currentStyle = { fg: null, bg: null, bold: false };
206
- const stack = [];
207
-
208
- // Pattern for ANSI escape sequences
209
- const pattern = /\x1b\[([0-9;]*?)m/g;
210
- let lastIndex = 0;
211
- let match;
212
-
213
- while ((match = pattern.exec(text)) !== null) {
214
- // Add text before this escape sequence
215
- if (match.index > lastIndex) {
216
- const plainText = text.substring(lastIndex, match.index);
217
- result += this.escapeHtml(plainText);
218
- }
219
-
220
- // Parse ANSI code
221
- const codes = match[1].split(';').map(c => parseInt(c, 10));
222
- for (const code of codes) {
223
- if (code === 0) {
224
- // Reset
225
- currentStyle = { fg: null, bg: null, bold: false };
226
- } else if (code === 1) {
227
- currentStyle.bold = true;
228
- } else if (code >= 30 && code <= 37) {
229
- currentStyle.fg = this.colorMap[code];
230
- } else if (code >= 40 && code <= 47) {
231
- currentStyle.bg = this.colorMap[String(code - 10)];
232
- } else if (code >= 90 && code <= 97) {
233
- currentStyle.fg = this.colorMap[code];
234
- }
235
- }
236
-
237
- lastIndex = pattern.lastIndex;
238
- }
239
-
240
- // Add remaining text
241
- if (lastIndex < text.length) {
242
- result += this.escapeHtml(text.substring(lastIndex));
243
- }
244
-
245
- return result;
246
- }
247
-
248
- /**
249
- * Transform unified diff format
250
- */
251
- transformDiff(diffText) {
252
- if (typeof diffText !== 'string') {
253
- return diffText;
254
- }
255
-
256
- const lines = diffText.split('\n');
257
- const parsed = {
258
- headers: [],
259
- hunks: []
260
- };
261
-
262
- let currentHunk = null;
263
-
264
- for (const line of lines) {
265
- if (line.startsWith('---') || line.startsWith('+++')) {
266
- parsed.headers.push(line);
267
- } else if (line.startsWith('@@')) {
268
- if (currentHunk) {
269
- parsed.hunks.push(currentHunk);
270
- }
271
- currentHunk = {
272
- header: line,
273
- changes: []
274
- };
275
- } else if (currentHunk) {
276
- if (line.startsWith('-')) {
277
- currentHunk.changes.push({ type: 'deleted', line: line.substring(1) });
278
- } else if (line.startsWith('+')) {
279
- currentHunk.changes.push({ type: 'added', line: line.substring(1) });
280
- } else if (line.startsWith(' ')) {
281
- currentHunk.changes.push({ type: 'context', line: line.substring(1) });
282
- }
283
- }
284
- }
285
-
286
- if (currentHunk) {
287
- parsed.hunks.push(currentHunk);
288
- }
289
-
290
- return parsed;
291
- }
292
-
293
- /**
294
- * Convert markdown to HTML (simple implementation)
295
- */
296
- transformMarkdown(markdown) {
297
- if (!this.config.enableMarkdown || typeof markdown !== 'string') {
298
- return markdown;
299
- }
300
-
301
- let html = this.escapeHtml(markdown);
302
-
303
- // Bold
304
- html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
305
-
306
- // Italic
307
- html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
308
-
309
- // Code
310
- html = html.replace(/`(.+?)`/g, '<code>$1</code>');
311
-
312
- // Links
313
- html = html.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2" target="_blank">$1</a>');
314
-
315
- // Headings
316
- html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
317
- html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
318
- html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
319
-
320
- // Line breaks
321
- html = html.replace(/\n/g, '<br>');
322
-
323
- return html;
324
- }
325
-
326
104
  /**
327
105
  * Detect language from content or hint
328
106
  */
@@ -379,40 +157,6 @@ class EventProcessor {
379
157
  return match ? match[1].toLowerCase() : null;
380
158
  }
381
159
 
382
- /**
383
- * Determine syntax highlighter language from file extension
384
- */
385
- getLanguageFromExtension(ext) {
386
- const extMap = {
387
- 'js': 'javascript',
388
- 'jsx': 'jsx',
389
- 'ts': 'typescript',
390
- 'tsx': 'typescript',
391
- 'py': 'python',
392
- 'java': 'java',
393
- 'cpp': 'cpp',
394
- 'c': 'c',
395
- 'cs': 'csharp',
396
- 'php': 'php',
397
- 'rb': 'ruby',
398
- 'go': 'go',
399
- 'rs': 'rust',
400
- 'json': 'json',
401
- 'xml': 'xml',
402
- 'html': 'html',
403
- 'css': 'css',
404
- 'scss': 'scss',
405
- 'yaml': 'yaml',
406
- 'yml': 'yaml',
407
- 'sql': 'sql',
408
- 'sh': 'bash',
409
- 'bash': 'bash',
410
- 'zsh': 'bash'
411
- };
412
-
413
- return extMap[ext?.toLowerCase()] || 'plaintext';
414
- }
415
-
416
160
  /**
417
161
  * Truncate text with ellipsis
418
162
  */
@@ -133,16 +133,14 @@ class SyncDebugger {
133
133
  }
134
134
  }
135
135
 
136
- // Create global instance
137
- window.syncDebugger = new SyncDebugger();
138
-
139
- // Expose commands
140
- window.debugSync = {
141
- enable: () => window.syncDebugger.enable(),
142
- disable: () => window.syncDebugger.disable(),
143
- report: () => window.syncDebugger.printReport(),
144
- clear: () => window.syncDebugger.clearLogs(),
145
- get: () => window.syncDebugger.getReport()
146
- };
147
-
148
- console.log('[SyncDebug] Available. Use: debugSync.enable(), debugSync.report(), debugSync.clear()');
136
+ if (window.__DEBUG__) {
137
+ window.syncDebugger = new SyncDebugger();
138
+ window.debugSync = {
139
+ enable: () => window.syncDebugger.enable(),
140
+ disable: () => window.syncDebugger.disable(),
141
+ report: () => window.syncDebugger.printReport(),
142
+ clear: () => window.syncDebugger.clearLogs(),
143
+ get: () => window.syncDebugger.getReport()
144
+ };
145
+ console.log('[SyncDebug] Available. Use: debugSync.enable(), debugSync.report(), debugSync.clear()');
146
+ }
@@ -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;