agentgui 1.0.652 → 1.0.654

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.652",
3
+ "version": "1.0.654",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -302,7 +302,6 @@ const rateLimitState = new Map();
302
302
  const activeProcessesByRunId = new Map();
303
303
  const activeProcessesByConvId = new Map(); // Store process handles by conversationId for steering
304
304
  const steeringTimeouts = new Map(); // Track timeout handles for process cleanup
305
- const acpQueries = queries;
306
305
  const checkpointManager = new CheckpointManager(queries);
307
306
  const STUCK_AGENT_THRESHOLD_MS = 600000;
308
307
  const NO_PID_GRACE_PERIOD_MS = 60000;
@@ -1592,115 +1591,6 @@ const server = http.createServer(async (req, res) => {
1592
1591
  return;
1593
1592
  }
1594
1593
 
1595
- const oldRunByIdMatch = pathOnly.match(/^\/api\/runs\/([^/]+)$/);
1596
- if (oldRunByIdMatch) {
1597
- const runId = oldRunByIdMatch[1];
1598
- const session = queries.getSession(runId);
1599
-
1600
- if (!session) {
1601
- sendJSON(req, res, 404, { error: 'Run not found' });
1602
- return;
1603
- }
1604
-
1605
- if (req.method === 'GET') {
1606
- sendJSON(req, res, 200, {
1607
- id: session.id,
1608
- status: session.status,
1609
- started_at: session.started_at,
1610
- completed_at: session.completed_at,
1611
- agentId: session.agentId,
1612
- input: null,
1613
- output: null
1614
- });
1615
- return;
1616
- }
1617
-
1618
- if (req.method === 'DELETE') {
1619
- queries.deleteSession(runId);
1620
- sendJSON(req, res, 204, {});
1621
- return;
1622
- }
1623
-
1624
- if (req.method === 'POST') {
1625
- if (session.status !== 'interrupted') {
1626
- sendJSON(req, res, 409, { error: 'Can only resume interrupted runs' });
1627
- return;
1628
- }
1629
-
1630
- let body = '';
1631
- for await (const chunk of req) { body += chunk; }
1632
- let parsed = {};
1633
- try { parsed = body ? JSON.parse(body) : {}; } catch {}
1634
-
1635
- const { input } = parsed;
1636
- if (!input) {
1637
- sendJSON(req, res, 400, { error: 'Missing input in request body' });
1638
- return;
1639
- }
1640
-
1641
- const conv = queries.getConversation(session.conversationId);
1642
- const resolvedAgentId = session.agentId || conv?.agentId || 'claude-code';
1643
- const resolvedModel = conv?.model || null;
1644
- const cwd = conv?.workingDirectory || STARTUP_CWD;
1645
-
1646
- queries.updateSession(runId, { status: 'pending' });
1647
-
1648
- const message = queries.createMessage(session.conversationId, 'user', typeof input === 'string' ? input : JSON.stringify(input));
1649
-
1650
- processMessageWithStreaming(session.conversationId, message.id, runId, typeof input === 'string' ? input : JSON.stringify(input), resolvedAgentId, resolvedModel);
1651
-
1652
- sendJSON(req, res, 200, {
1653
- id: session.id,
1654
- status: 'pending',
1655
- started_at: session.started_at,
1656
- agentId: resolvedAgentId
1657
- });
1658
- return;
1659
- }
1660
- }
1661
-
1662
- const oldRunCancelMatch = pathOnly.match(/^\/api\/runs\/([^/]+)\/cancel$/);
1663
- if (oldRunCancelMatch && req.method === 'POST') {
1664
- const runId = oldRunCancelMatch[1];
1665
- const session = queries.getSession(runId);
1666
-
1667
- if (!session) {
1668
- sendJSON(req, res, 404, { error: 'Run not found' });
1669
- return;
1670
- }
1671
-
1672
- const conversationId = session.conversationId;
1673
- const entry = activeExecutions.get(conversationId);
1674
-
1675
- if (entry && entry.sessionId === runId) {
1676
- const { pid } = entry;
1677
- if (pid) {
1678
- try {
1679
- process.kill(-pid, 'SIGKILL');
1680
- } catch {
1681
- try {
1682
- process.kill(pid, 'SIGKILL');
1683
- } catch (e) {}
1684
- }
1685
- }
1686
- }
1687
-
1688
- queries.updateSession(runId, { status: 'interrupted', completed_at: Date.now() });
1689
- queries.setIsStreaming(conversationId, false);
1690
- activeExecutions.delete(conversationId);
1691
-
1692
- broadcastSync({
1693
- type: 'streaming_complete',
1694
- sessionId: runId,
1695
- conversationId,
1696
- interrupted: true,
1697
- timestamp: Date.now()
1698
- });
1699
-
1700
- sendJSON(req, res, 204, {});
1701
- return;
1702
- }
1703
-
1704
1594
  const scriptsMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/scripts$/);
1705
1595
  if (scriptsMatch && req.method === 'GET') {
1706
1596
  const conv = queries.getConversation(scriptsMatch[1]);
@@ -2318,13 +2208,13 @@ const server = http.createServer(async (req, res) => {
2318
2208
  if (modelsMatch && req.method === 'GET') {
2319
2209
  const agentId = modelsMatch[1];
2320
2210
  const cached = modelCache.get(agentId);
2321
- if (cached && (Date.now() - cached.ts) < 300000) {
2211
+ if (cached && (Date.now() - cached.timestamp) < 300000) {
2322
2212
  sendJSON(req, res, 200, { models: cached.models });
2323
2213
  return;
2324
2214
  }
2325
2215
  try {
2326
2216
  const models = await getModelsForAgent(agentId);
2327
- modelCache.set(agentId, { models, ts: Date.now() });
2217
+ modelCache.set(agentId, { models, timestamp: Date.now() });
2328
2218
  sendJSON(req, res, 200, { models });
2329
2219
  } catch (err) {
2330
2220
  sendJSON(req, res, 200, { models: [] });
@@ -3597,6 +3487,21 @@ function createChunkBatcher() {
3597
3487
  return { add, drain };
3598
3488
  }
3599
3489
 
3490
+ function parseRateLimitResetTime(text) {
3491
+ const match = text.match(/resets?\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*\(?(UTC|[A-Z]{2,4})\)?/i);
3492
+ if (!match) return 300;
3493
+ let hours = parseInt(match[1], 10);
3494
+ const minutes = match[2] ? parseInt(match[2], 10) : 0;
3495
+ const period = match[3]?.toLowerCase();
3496
+ if (period === 'pm' && hours !== 12) hours += 12;
3497
+ if (period === 'am' && hours === 12) hours = 0;
3498
+ const now = new Date();
3499
+ const resetTime = new Date(now);
3500
+ resetTime.setUTCHours(hours, minutes, 0, 0);
3501
+ if (resetTime <= now) resetTime.setUTCDate(resetTime.getUTCDate() + 1);
3502
+ return Math.max(60, Math.ceil((resetTime.getTime() - now.getTime()) / 1000));
3503
+ }
3504
+
3600
3505
  async function processMessageWithStreaming(conversationId, messageId, sessionId, content, agentId, model, subAgent) {
3601
3506
  const startTime = Date.now();
3602
3507
  touchACP(agentId);
@@ -3700,28 +3605,8 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
3700
3605
  if (rateLimitTextMatch) {
3701
3606
  debugLog(`[rate-limit] Detected rate limit message in stream for conv ${conversationId}`);
3702
3607
 
3703
- // Extract reset time from message
3704
- let retryAfterSec = 300; // default 5 minutes
3705
- const resetTimeMatch = block.text.match(/resets?\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*\(?(UTC|[A-Z]{2,4})\)?/i);
3706
- if (resetTimeMatch) {
3707
- let hours = parseInt(resetTimeMatch[1], 10);
3708
- const minutes = resetTimeMatch[2] ? parseInt(resetTimeMatch[2], 10) : 0;
3709
- const period = resetTimeMatch[3]?.toLowerCase();
3710
-
3711
- if (period === 'pm' && hours !== 12) hours += 12;
3712
- if (period === 'am' && hours === 12) hours = 0;
3713
-
3714
- const now = new Date();
3715
- const resetTime = new Date(now);
3716
- resetTime.setUTCHours(hours, minutes, 0, 0);
3717
-
3718
- if (resetTime <= now) {
3719
- resetTime.setUTCDate(resetTime.getUTCDate() + 1);
3720
- }
3721
-
3722
- retryAfterSec = Math.max(60, Math.ceil((resetTime.getTime() - now.getTime()) / 1000));
3723
- debugLog(`[rate-limit] Parsed reset time: ${resetTime.toISOString()}, retry in ${retryAfterSec}s`);
3724
- }
3608
+ const retryAfterSec = parseRateLimitResetTime(block.text);
3609
+ debugLog(`[rate-limit] Parsed reset time, retry in ${retryAfterSec}s`);
3725
3610
 
3726
3611
  // Kill the running process
3727
3612
  const entry = activeExecutions.get(conversationId);
@@ -3833,26 +3718,7 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
3833
3718
  if (rateLimitResultMatch) {
3834
3719
  debugLog(`[rate-limit] Detected rate limit in result for conv ${conversationId}`);
3835
3720
 
3836
- let retryAfterSec = 300;
3837
- const resetTimeMatch = resultText.match(/resets?\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*\(?(UTC|[A-Z]{2,4})\)?/i);
3838
- if (resetTimeMatch) {
3839
- let hours = parseInt(resetTimeMatch[1], 10);
3840
- const minutes = resetTimeMatch[2] ? parseInt(resetTimeMatch[2], 10) : 0;
3841
- const period = resetTimeMatch[3]?.toLowerCase();
3842
-
3843
- if (period === 'pm' && hours !== 12) hours += 12;
3844
- if (period === 'am' && hours === 12) hours = 0;
3845
-
3846
- const now = new Date();
3847
- const resetTime = new Date(now);
3848
- resetTime.setUTCHours(hours, minutes, 0, 0);
3849
-
3850
- if (resetTime <= now) {
3851
- resetTime.setUTCDate(resetTime.getUTCDate() + 1);
3852
- }
3853
-
3854
- retryAfterSec = Math.max(60, Math.ceil((resetTime.getTime() - now.getTime()) / 1000));
3855
- }
3721
+ const retryAfterSec = parseRateLimitResetTime(resultText);
3856
3722
 
3857
3723
  const entry = activeExecutions.get(conversationId);
3858
3724
  if (entry && entry.pid) {
package/static/index.html CHANGED
@@ -3245,6 +3245,7 @@
3245
3245
  var _escHtmlRe = /[&<>"']/g;
3246
3246
  window._escHtml = function(t) { return t.replace(_escHtmlRe, function(c) { return _escHtmlMap[c]; }); };
3247
3247
  </script>
3248
+ <script defer src="/gm/js/conversations.js"></script>
3248
3249
  <script defer src="/gm/js/event-processor.js"></script>
3249
3250
  <script defer src="/gm/js/streaming-renderer.js"></script>
3250
3251
  <script defer src="/gm/js/image-loader.js"></script>
@@ -3252,12 +3253,10 @@
3252
3253
  <script defer src="/gm/js/event-consolidator.js"></script>
3253
3254
  <script defer src="/gm/js/websocket-manager.js"></script>
3254
3255
  <script defer src="/gm/js/ws-client.js"></script>
3255
- <script defer src="/gm/js/event-filter.js"></script>
3256
- <script defer src="/gm/js/syntax-highlighter.js"></script>
3256
+ <script defer src="/gm/js/syntax-highlighter.js"></script>
3257
3257
  <script defer src="/gm/js/dialogs.js"></script>
3258
3258
  <script defer src="/gm/js/ui-components.js"></script>
3259
3259
  <script defer src="/gm/js/state-barrier.js"></script>
3260
- <script defer src="/gm/js/conversations.js"></script>
3261
3260
  <script defer src="/gm/js/terminal.js"></script>
3262
3261
  <script defer src="/gm/js/script-runner.js"></script>
3263
3262
  <script defer src="/gm/js/tools-manager.js"></script>
@@ -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
  */
@@ -4,15 +4,6 @@
4
4
  * for Claude Code streaming execution display
5
5
  */
6
6
 
7
- function pathSplit(p) {
8
- return p.split(/[\/\\]/).filter(Boolean);
9
- }
10
-
11
- function pathBasename(p) {
12
- const parts = pathSplit(p);
13
- return parts.length ? parts.pop() : '';
14
- }
15
-
16
7
  class StreamingRenderer {
17
8
  constructor(config = {}) {
18
9
  // Configuration
@@ -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
+ }
package/lib/compressor.js DELETED
@@ -1,125 +0,0 @@
1
- /**
2
- * Compressor: tokenize text fields → msgpackr → optional gzip
3
- *
4
- * Transport: event → msgpackr.pack() → gzip if >512B → binary WS frame
5
- * Storage: text → token array (Uint32) → msgpackr.pack() → BLOB
6
- */
7
- import zlib from 'zlib';
8
- import { pack, unpack } from 'msgpackr';
9
- import { encode as encodeTokens, decode as decodeTokens } from 'gpt-tokenizer';
10
-
11
- // Magic prefix stored at start of compressed BLOBs so we can detect them
12
- const MAGIC = Buffer.from([0xC0, 0xDE]); // "CODE"
13
- const GZIP_MAGIC = Buffer.from([0x1f, 0x8b]);
14
-
15
- // ── Token helpers ─────────────────────────────────────────────────────────────
16
-
17
- export function tokenize(text) {
18
- if (typeof text !== 'string' || text.length === 0) return null;
19
- try {
20
- return new Uint32Array(encodeTokens(text));
21
- } catch {
22
- return null;
23
- }
24
- }
25
-
26
- export function detokenize(tokens) {
27
- try {
28
- const arr = tokens instanceof Uint32Array ? Array.from(tokens) : tokens;
29
- return decodeTokens(arr);
30
- } catch {
31
- return null;
32
- }
33
- }
34
-
35
- // ── Storage compression (text → tokens → msgpack BLOB) ────────────────────────
36
-
37
- /**
38
- * Compress a string for database storage.
39
- * Returns a Buffer starting with MAGIC prefix.
40
- */
41
- export function compressForStorage(text) {
42
- if (typeof text !== 'string') return null;
43
- const tokens = tokenize(text);
44
- if (!tokens) return null;
45
- const packed = pack({ t: Array.from(tokens) });
46
- return Buffer.concat([MAGIC, packed]);
47
- }
48
-
49
- /**
50
- * Decompress a storage BLOB back to a string.
51
- * Returns null if not a compressed BLOB (caller should use raw value).
52
- */
53
- export function decompressFromStorage(buf) {
54
- if (!Buffer.isBuffer(buf) && !(buf instanceof Uint8Array)) return null;
55
- const b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf);
56
- if (b.length < MAGIC.length || b[0] !== MAGIC[0] || b[1] !== MAGIC[1]) return null;
57
- try {
58
- const { t } = unpack(b.slice(MAGIC.length));
59
- return detokenize(t);
60
- } catch {
61
- return null;
62
- }
63
- }
64
-
65
- // ── Transport compression (event → msgpack → gzip → binary frame) ─────────────
66
-
67
- const GZ_THRESHOLD = 512; // bytes — compress if msgpack payload exceeds this
68
-
69
- /**
70
- * Pack one event (or array of events) into a binary buffer for WebSocket transport.
71
- * Format: [ 0x01 ] + gzip(msgpack(data)) when compressed
72
- * [ 0x00 ] + msgpack(data) when not compressed
73
- */
74
- export function packForTransport(data) {
75
- const packed = pack(data);
76
- if (packed.length > GZ_THRESHOLD) {
77
- try {
78
- const compressed = zlib.gzipSync(packed, { level: 6 });
79
- if (compressed.length < packed.length * 0.9) {
80
- const out = Buffer.allocUnsafe(1 + compressed.length);
81
- out[0] = 0x01; // flag: gzipped
82
- compressed.copy(out, 1);
83
- return out;
84
- }
85
- } catch (_) {}
86
- }
87
- const out = Buffer.allocUnsafe(1 + packed.length);
88
- out[0] = 0x00; // flag: plain msgpack
89
- packed.copy(out, 1);
90
- return out;
91
- }
92
-
93
- /**
94
- * Unpack a binary buffer received from WebSocket transport.
95
- */
96
- export function unpackFromTransport(buf) {
97
- const b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf);
98
- if (b.length < 2) return null;
99
- const flag = b[0];
100
- const payload = b.slice(1);
101
- if (flag === 0x01) {
102
- return unpack(zlib.gunzipSync(payload));
103
- }
104
- return unpack(payload);
105
- }
106
-
107
- // ── Tokenize specific fields in an event object (for storage) ─────────────────
108
-
109
- const TEXT_FIELDS = ['content', 'text', 'data', 'output', 'input', 'message', 'prompt', 'response'];
110
-
111
- /**
112
- * Walk an event object and compress any large text fields in-place for storage.
113
- * Returns the (possibly mutated) object — safe to pass to JSON.stringify or pack().
114
- */
115
- export function compressEventFields(obj) {
116
- if (!obj || typeof obj !== 'object') return obj;
117
- for (const key of TEXT_FIELDS) {
118
- const val = obj[key];
119
- if (typeof val === 'string' && val.length > 64) {
120
- const buf = compressForStorage(val);
121
- if (buf) obj[key] = buf;
122
- }
123
- }
124
- return obj;
125
- }
@@ -1,138 +0,0 @@
1
- /**
2
- * Request Manager - Phase 2: Request Lifetime Management
3
- * Tracks in-flight requests with unique IDs, enables cancellation on navigation
4
- * Prevents race conditions where older requests complete after newer ones
5
- */
6
-
7
- class RequestManager {
8
- constructor() {
9
- this._requestId = 0;
10
- this._inflightRequests = new Map(); // requestId -> { conversationId, abortController, timestamp, priority }
11
- this._activeLoadId = null; // Track which request is currently being rendered
12
- }
13
-
14
- /**
15
- * Start a new load request for a conversation
16
- * Returns a request token that must be verified before rendering
17
- */
18
- startLoadRequest(conversationId, priority = 'normal') {
19
- const requestId = ++this._requestId;
20
- const abortController = new AbortController();
21
-
22
- this._inflightRequests.set(requestId, {
23
- conversationId,
24
- abortController,
25
- timestamp: Date.now(),
26
- priority,
27
- status: 'pending'
28
- });
29
-
30
- return {
31
- requestId,
32
- abortSignal: abortController.signal,
33
- cancel: () => this._cancelRequest(requestId),
34
- verify: () => this._verifyRequest(requestId, conversationId)
35
- };
36
- }
37
-
38
- /**
39
- * Mark request as completed (allows rendering)
40
- */
41
- completeRequest(requestId) {
42
- const req = this._inflightRequests.get(requestId);
43
- if (req) {
44
- req.status = 'completed';
45
- this._activeLoadId = requestId;
46
- }
47
- }
48
-
49
- /**
50
- * Verify request is still valid before rendering
51
- * Returns true only if this is the most recent request for this conversation
52
- */
53
- _verifyRequest(requestId, conversationId) {
54
- const req = this._inflightRequests.get(requestId);
55
-
56
- // Request not found or cancelled
57
- if (!req) return false;
58
-
59
- // Request is for different conversation
60
- if (req.conversationId !== conversationId) return false;
61
-
62
- // Find all requests for this conversation
63
- const allForConv = Array.from(this._inflightRequests.entries())
64
- .filter(([_, r]) => r.conversationId === conversationId && r.status === 'completed')
65
- .sort((a, b) => b[0] - a[0]); // Sort by requestId descending (newest first)
66
-
67
- // This request is the newest completed one for this conversation
68
- return allForConv.length > 0 && allForConv[0][0] === requestId;
69
- }
70
-
71
- /**
72
- * Cancel a request (aborts any pending network operations)
73
- */
74
- _cancelRequest(requestId) {
75
- const req = this._inflightRequests.get(requestId);
76
- if (req) {
77
- req.status = 'cancelled';
78
- req.abortController.abort();
79
- }
80
- }
81
-
82
- /**
83
- * Cancel all pending requests for a conversation
84
- */
85
- cancelConversationRequests(conversationId) {
86
- for (const [id, req] of this._inflightRequests.entries()) {
87
- if (req.conversationId === conversationId && req.status !== 'completed') {
88
- this._cancelRequest(id);
89
- }
90
- }
91
- }
92
-
93
- /**
94
- * Cancel all in-flight requests
95
- */
96
- cancelAllRequests() {
97
- for (const [id, req] of this._inflightRequests.entries()) {
98
- if (req.status !== 'completed') {
99
- this._cancelRequest(id);
100
- }
101
- }
102
- }
103
-
104
- /**
105
- * Clean up old requests to prevent memory leak
106
- */
107
- cleanup() {
108
- const now = Date.now();
109
- const maxAge = 60000; // Keep requests for 60 seconds
110
-
111
- for (const [id, req] of this._inflightRequests.entries()) {
112
- if (now - req.timestamp > maxAge) {
113
- this._inflightRequests.delete(id);
114
- }
115
- }
116
- }
117
-
118
- /**
119
- * Get debug info about in-flight requests
120
- */
121
- getDebugInfo() {
122
- return {
123
- activeLoadId: this._activeLoadId,
124
- inflightRequests: Array.from(this._inflightRequests.entries()).map(([id, req]) => ({
125
- requestId: id,
126
- conversationId: req.conversationId,
127
- timestamp: req.timestamp,
128
- status: req.status,
129
- priority: req.priority,
130
- age: Date.now() - req.timestamp
131
- }))
132
- };
133
- }
134
- }
135
-
136
- if (typeof window !== 'undefined') {
137
- window.RequestManager = new RequestManager();
138
- }