agentgui 1.0.632 → 1.0.633

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.
@@ -49,6 +49,7 @@ class AgentRunner {
49
49
  this.buildArgs = config.buildArgs || this.defaultBuildArgs;
50
50
  this.parseOutput = config.parseOutput || this.defaultParseOutput;
51
51
  this.supportsStdin = config.supportsStdin ?? true;
52
+ this.closeStdin = config.closeStdin ?? false; // close stdin so process doesn't block waiting for input
52
53
  this.supportedFeatures = config.supportedFeatures || [];
53
54
  this.protocolHandler = config.protocolHandler || null;
54
55
  this.requiresAdapter = config.requiresAdapter || false;
@@ -91,7 +92,11 @@ class AgentRunner {
91
92
  if (Object.keys(this.spawnEnv).length > 0) {
92
93
  spawnOpts.env = { ...spawnOpts.env, ...this.spawnEnv };
93
94
  }
95
+ if (this.closeStdin) {
96
+ spawnOpts.stdio = ['ignore', 'pipe', 'pipe'];
97
+ }
94
98
  const proc = spawn(this.command, args, spawnOpts);
99
+ console.log(`[${this.id}] Spawned PID ${proc.pid} closeStdin=${this.closeStdin}`);
95
100
 
96
101
  if (config.onPid) {
97
102
  try { config.onPid(proc.pid); } catch (e) {}
@@ -116,15 +121,14 @@ class AgentRunner {
116
121
  reject(new Error(`${this.name} timeout after ${timeout}ms`));
117
122
  }, timeout);
118
123
 
119
- // Write to stdin if supported
120
- // Don't close stdin - keep it open for steering/injection during execution
124
+ // Write prompt to stdin if agent uses stdin protocol (not positional args)
121
125
  if (this.supportsStdin) {
122
126
  proc.stdin.write(prompt);
123
- // Don't call stdin.end() - agents need open stdin for session/prompt steering
127
+ // Don't call stdin.end() - agents need open stdin for steering
124
128
  }
125
129
 
126
130
  proc.stdout.on('error', () => {});
127
- proc.stderr.on('error', () => {});
131
+ if (proc.stderr) proc.stderr.on('error', () => {});
128
132
  proc.stdout.on('data', (chunk) => {
129
133
  if (timedOut) return;
130
134
 
@@ -152,7 +156,7 @@ class AgentRunner {
152
156
  }
153
157
  });
154
158
 
155
- proc.stderr.on('data', (chunk) => {
159
+ if (proc.stderr) proc.stderr.on('data', (chunk) => {
156
160
  const errorText = chunk.toString();
157
161
  console.error(`[${this.id}] stderr:`, errorText);
158
162
 
@@ -605,7 +609,9 @@ registry.register({
605
609
  name: 'Claude Code',
606
610
  command: 'claude',
607
611
  protocol: 'direct',
608
- supportsStdin: true,
612
+ supportsStdin: false,
613
+ closeStdin: true, // must close stdin or claude 2.1.72 hangs waiting for input in --print mode
614
+ useJsonRpcStdin: false,
609
615
  supportedFeatures: ['streaming', 'resume', 'system-prompt', 'permissions-skip'],
610
616
  spawnEnv: { MAX_THINKING_TOKENS: '0' },
611
617
 
@@ -627,6 +633,8 @@ registry.register({
627
633
  if (model) flags.push('--model', model);
628
634
  if (resumeSessionId) flags.push('--resume', resumeSessionId);
629
635
  if (systemPrompt) flags.push('--append-system-prompt', systemPrompt);
636
+ // Pass prompt as positional arg (works with claude 2.1.72+)
637
+ flags.push(prompt);
630
638
 
631
639
  return flags;
632
640
  },
@@ -1,4 +1,4 @@
1
- import zlib from 'zlib';
1
+ import { pack } from 'msgpackr';
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'],
@@ -33,19 +33,21 @@ class ClientQueue {
33
33
  this.normalPriority = [];
34
34
  this.lowPriority = [];
35
35
  this.timer = null;
36
- this.lastMessage = null;
36
+ this.lastKey = null;
37
37
  this.messageCount = 0;
38
38
  this.bytesSent = 0;
39
39
  this.windowStart = Date.now();
40
40
  this.rateLimitWarned = false;
41
41
  }
42
42
 
43
- add(data, priority) {
44
- if (this.lastMessage === data) return;
45
- this.lastMessage = data;
46
- if (priority === 3) this.highPriority.push(data);
47
- else if (priority === 2) this.normalPriority.push(data);
48
- else this.lowPriority.push(data);
43
+ add(event, priority) {
44
+ // Deduplicate by type+seq key
45
+ const key = event.type + (event.seq ?? '') + (event.sessionId ?? '');
46
+ if (this.lastKey === key) return;
47
+ this.lastKey = key;
48
+ if (priority === 3) this.highPriority.push(event);
49
+ else if (priority === 2) this.normalPriority.push(event);
50
+ else this.lowPriority.push(event);
49
51
  if (priority === 3) this.flushImmediate();
50
52
  else if (!this.timer) this.scheduleFlush();
51
53
  }
@@ -82,20 +84,12 @@ class ClientQueue {
82
84
  if (allowedCount <= 0) { this.scheduleFlush(); return; }
83
85
  batch.splice(allowedCount);
84
86
  }
85
- let payload = batch.length === 1 ? batch[0] : '[' + batch.join(',') + ']';
86
- if (payload.length > 1024) {
87
- try {
88
- const compressed = zlib.gzipSync(Buffer.from(payload), { level: 6 });
89
- if (compressed.length < payload.length * 0.9) {
90
- this.ws.send(JSON.stringify({ type: '_compressed', encoding: 'gzip' }));
91
- this.ws.send(compressed);
92
- payload = null;
93
- }
94
- } catch (e) {}
95
- }
96
- if (payload) this.ws.send(payload);
87
+ // Pack as msgpackr binary perMessageDeflate on the WS server handles gzip
88
+ const envelope = batch.length === 1 ? batch[0] : batch;
89
+ const binary = pack(envelope);
90
+ this.ws.send(binary);
97
91
  this.messageCount += batch.length;
98
- this.bytesSent += (payload ? payload.length : 0);
92
+ this.bytesSent += binary.length;
99
93
  if (windowDuration >= 3000 && this.bytesSent > 3 * 1024 * 1024) {
100
94
  const mbps = (this.bytesSent / windowDuration * 1000 / 1024 / 1024).toFixed(2);
101
95
  console.warn(`[ws-optimizer] Client ${this.ws.clientId} high bandwidth: ${mbps} MB/sec`);
@@ -116,16 +110,16 @@ class WSOptimizer {
116
110
  this.clientQueues = new Map();
117
111
  }
118
112
 
119
- sendToClient(ws, event, originalType) {
113
+ sendToClient(ws, event) {
120
114
  if (ws.readyState !== 1) return;
121
115
  let queue = this.clientQueues.get(ws);
122
116
  if (!queue) {
123
117
  queue = new ClientQueue(ws);
124
118
  this.clientQueues.set(ws, queue);
125
119
  }
126
- const data = typeof event === 'string' ? event : JSON.stringify(event);
127
- const priority = typeof event === 'object' ? getPriority(originalType || event.type) : 2;
128
- queue.add(data, priority);
120
+ const obj = typeof event === 'string' ? JSON.parse(event) : event;
121
+ const priority = getPriority(obj.type);
122
+ queue.add(obj, priority);
129
123
  }
130
124
 
131
125
  removeClient(ws) {
@@ -1,3 +1,9 @@
1
+ import { pack, unpack } from 'msgpackr';
2
+
3
+ function sendBinary(ws, obj) {
4
+ if (ws.readyState === 1) ws.send(pack(obj));
5
+ }
6
+
1
7
  class WsRouter {
2
8
  constructor() {
3
9
  this.handlers = new Map();
@@ -15,25 +21,19 @@ class WsRouter {
15
21
  }
16
22
 
17
23
  reply(ws, requestId, data) {
18
- if (ws.readyState === 1) {
19
- ws.send(JSON.stringify({ r: requestId, d: data || {} }));
20
- }
24
+ sendBinary(ws, { r: requestId, d: data || {} });
21
25
  }
22
26
 
23
27
  replyError(ws, requestId, code, message) {
24
- if (ws.readyState === 1) {
25
- ws.send(JSON.stringify({ r: requestId, e: { c: code, m: message } }));
26
- }
28
+ sendBinary(ws, { r: requestId, e: { c: code, m: message } });
27
29
  }
28
30
 
29
31
  send(ws, type, data) {
30
- if (ws.readyState === 1) {
31
- ws.send(JSON.stringify({ t: type, d: data || {} }));
32
- }
32
+ sendBinary(ws, { t: type, d: data || {} });
33
33
  }
34
34
 
35
35
  broadcast(clients, type, data) {
36
- const msg = JSON.stringify({ t: type, d: data || {} });
36
+ const msg = pack({ t: type, d: data || {} });
37
37
  for (const ws of clients) {
38
38
  if (ws.readyState === 1) ws.send(msg);
39
39
  }
@@ -42,11 +42,14 @@ class WsRouter {
42
42
  async onMessage(ws, rawData) {
43
43
  let parsed;
44
44
  try {
45
- parsed = JSON.parse(rawData);
46
- } catch {
47
- if (ws.readyState === 1) {
48
- ws.send(JSON.stringify({ r: null, e: { c: 400, m: 'Invalid JSON' } }));
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());
49
50
  }
51
+ } catch {
52
+ sendBinary(ws, { r: null, e: { c: 400, m: 'Invalid message' } });
50
53
  return;
51
54
  }
52
55
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.632",
3
+ "version": "1.0.633",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
@@ -33,6 +33,7 @@
33
33
  "form-data": "^4.0.5",
34
34
  "fsbrowse": "^0.2.18",
35
35
  "google-auth-library": "^10.5.0",
36
+ "gpt-tokenizer": "^3.4.0",
36
37
  "msgpackr": "^1.11.8",
37
38
  "node-pty": "^1.0.0",
38
39
  "onnxruntime-node": "1.21.0",
package/static/index.html CHANGED
@@ -3244,7 +3244,7 @@
3244
3244
  <script defer src="/gm/js/event-processor.js"></script>
3245
3245
  <script defer src="/gm/js/streaming-renderer.js"></script>
3246
3246
  <script defer src="/gm/js/image-loader.js"></script>
3247
- <script defer src="/gm/js/kalman-filter.js"></script>
3247
+ <script src="/gm/lib/msgpackr.min.js"></script>
3248
3248
  <script defer src="/gm/js/event-consolidator.js"></script>
3249
3249
  <script defer src="/gm/js/websocket-manager.js"></script>
3250
3250
  <script defer src="/gm/js/ws-client.js"></script>
@@ -62,19 +62,6 @@ class AgentGUIClient {
62
62
  this._isLoadingConversation = false;
63
63
  this._modelCache = new Map();
64
64
 
65
- this.chunkPollState = {
66
- isPolling: false,
67
- lastFetchTimestamp: 0,
68
- pollTimer: null,
69
- backoffDelay: 100,
70
- maxBackoffDelay: 400,
71
- abortController: null
72
- };
73
-
74
- this._pollIntervalByTier = {
75
- excellent: 100, good: 200, fair: 400, poor: 800, bad: 1500, unknown: 200
76
- };
77
-
78
65
  this._renderedSeqs = new Map();
79
66
  this._inflightRequests = new Map();
80
67
  this._previousConvAbort = null;
@@ -83,16 +70,10 @@ class AgentGUIClient {
83
70
  this._loadInProgress = {}; // { [conversationId]: { requestId, abortController, timestamp, prevConversationId } }
84
71
  this._currentRequestId = 0; // Auto-incrementing request counter
85
72
 
86
- this._scrollKalman = typeof KalmanFilter !== 'undefined' ? new KalmanFilter({ processNoise: 50, measurementNoise: 100 }) : null;
87
73
  this._scrollTarget = 0;
88
74
  this._scrollAnimating = false;
89
75
  this._scrollLerpFactor = config.scrollAnimationSpeed || 0.15;
90
76
 
91
- this._chunkTimingKalman = typeof KalmanFilter !== 'undefined' ? new KalmanFilter({ processNoise: 10, measurementNoise: 200 }) : null;
92
- this._lastChunkArrival = 0;
93
- this._chunkTimingUpdateCount = 0;
94
- this._chunkMissedPredictions = 0;
95
-
96
77
  this._consolidator = typeof EventConsolidator !== 'undefined' ? new EventConsolidator() : null;
97
78
 
98
79
  this._serverProcessingEstimate = 2000;
@@ -603,7 +584,6 @@ class AgentGUIClient {
603
584
  this.state.currentConversation = null;
604
585
  this.state.currentSession = null;
605
586
  this.updateUrlForConversation(null);
606
- this.stopChunkPolling();
607
587
  this.enableControls();
608
588
  this._showWelcomeScreen();
609
589
  if (this.ui.messageInput) {
@@ -936,26 +916,9 @@ class AgentGUIClient {
936
916
  this.scrollToBottom(true);
937
917
  }
938
918
 
939
- // Immediately fetch any existing chunks for this session and start polling
940
- // This ensures real-time feedback appears immediately
941
- try {
942
- const initialChunks = await this.fetchChunks(data.conversationId, 0);
943
- // Filter to only chunks from the current session
944
- const sessionChunks = initialChunks.filter(c => c.sessionId === data.sessionId && c.block && c.block.type);
945
- if (sessionChunks.length > 0) {
946
- this.renderChunkBatch(sessionChunks);
947
- // Update lastFetchTimestamp so polling doesn't duplicate these chunks
948
- const lastChunk = sessionChunks[sessionChunks.length - 1];
949
- if (lastChunk && lastChunk.created_at) {
950
- this.chunkPollState.lastFetchTimestamp = lastChunk.created_at;
951
- }
952
- }
953
- } catch (e) {
954
- console.warn('Initial chunk fetch failed:', e.message);
955
- }
956
-
957
- // Start polling for chunks from database
958
- this.startChunkPolling(data.conversationId);
919
+ // Reset rendered block seq tracker for this session
920
+ this._renderedSeqs = this._renderedSeqs || {};
921
+ this._renderedSeqs[data.sessionId] = new Set();
959
922
 
960
923
  // Show queue/steer UI when streaming starts (for busy prompt)
961
924
  this.showStreamingPromptButtons();
@@ -978,30 +941,33 @@ class AgentGUIClient {
978
941
  }
979
942
 
980
943
  handleStreamingProgress(data) {
981
- // NOTE: With chunk-based architecture, blocks are rendered from polling
982
- // This handler is kept for backward compatibility and to trigger polling updates
983
- // But actual rendering happens in renderChunk() via polling
944
+ if (!data.block || !data.sessionId) return;
984
945
 
985
- if (!data.block) return;
946
+ // Deduplicate by seq number to guarantee exactly-once rendering
947
+ this._renderedSeqs = this._renderedSeqs || {};
948
+ const seen = this._renderedSeqs[data.sessionId] || (this._renderedSeqs[data.sessionId] = new Set());
949
+ if (data.seq !== undefined) {
950
+ if (seen.has(data.seq)) return;
951
+ seen.add(data.seq);
952
+ }
986
953
 
987
954
  const block = data.block;
988
955
  if (!this.state.streamingBlocks) this.state.streamingBlocks = [];
989
956
  this.state.streamingBlocks.push(block);
990
957
 
991
- // Thinking blocks are transient and not stored in DB, so render immediately
992
- if (block.type === 'thinking' && this.state.currentSession?.id === data.sessionId) {
993
- const streamingEl = document.getElementById(`streaming-${data.sessionId}`);
994
- if (streamingEl) {
995
- const blocksEl = streamingEl.querySelector('.streaming-blocks');
996
- if (blocksEl) {
997
- const el = this.renderer.renderBlock(block, data, blocksEl);
998
- if (el) blocksEl.appendChild(el);
999
- }
1000
- }
1001
- }
958
+ // Only render for the currently-visible session
959
+ if (this.state.currentSession?.id !== data.sessionId) return;
1002
960
 
1003
- // WebSocket is now just a notification trigger, not data source
1004
- // Actual blocks come from database polling in startChunkPolling()
961
+ const streamingEl = document.getElementById(`streaming-${data.sessionId}`);
962
+ if (!streamingEl) return;
963
+ const blocksEl = streamingEl.querySelector('.streaming-blocks');
964
+ if (!blocksEl) return;
965
+
966
+ const el = this.renderer.renderBlock(block, data, blocksEl);
967
+ if (el) {
968
+ blocksEl.appendChild(el);
969
+ this.scrollToBottom();
970
+ }
1005
971
  }
1006
972
 
1007
973
  renderBlockContent(block) {
@@ -1102,9 +1068,6 @@ class AgentGUIClient {
1102
1068
  this.state.streamingConversations.delete(conversationId);
1103
1069
  this.updateBusyPromptArea(conversationId);
1104
1070
 
1105
- // Stop polling for chunks
1106
- this.stopChunkPolling();
1107
-
1108
1071
  // Clear queue indicator on error
1109
1072
  const queueEl = document.querySelector('.queue-indicator');
1110
1073
  if (queueEl) queueEl.remove();
@@ -1154,7 +1117,7 @@ class AgentGUIClient {
1154
1117
  this.state.streamingConversations.delete(conversationId);
1155
1118
  this.updateBusyPromptArea(conversationId);
1156
1119
 
1157
- this.stopChunkPolling();
1120
+
1158
1121
 
1159
1122
  // Clear queue indicator when streaming completes
1160
1123
  const queueEl = document.querySelector('.queue-indicator');
@@ -1182,13 +1145,10 @@ class AgentGUIClient {
1182
1145
  this.saveScrollPosition(conversationId);
1183
1146
  }
1184
1147
 
1185
- // Fetch any final chunks that may have been missed during polling
1186
- // This ensures all output is visible without requiring a page refresh
1187
- if (conversationId && sessionId) {
1188
- this.fetchRemainingChunks(conversationId, sessionId).catch(err => {
1189
- console.warn('Final chunk fetch failed:', err.message);
1190
- });
1191
- }
1148
+ // Recover any blocks missed during streaming (e.g. WS reconnects)
1149
+ this._recoverMissedChunks().catch(err => {
1150
+ console.warn('Chunk recovery failed:', err.message);
1151
+ });
1192
1152
 
1193
1153
  this.enableControls();
1194
1154
  this.emit('streaming:complete', data);
@@ -1368,7 +1328,7 @@ class AgentGUIClient {
1368
1328
  handleRateLimitHit(data) {
1369
1329
  if (data.conversationId !== this.state.currentConversation?.id) return;
1370
1330
  this.state.streamingConversations.delete(data.conversationId);
1371
- this.stopChunkPolling();
1331
+
1372
1332
  this.enableControls();
1373
1333
 
1374
1334
  const cooldownMs = data.retryAfterMs || 60000;
@@ -1728,13 +1688,11 @@ class AgentGUIClient {
1728
1688
  block: typeof c.data === 'string' ? JSON.parse(c.data) : c.data
1729
1689
  })).filter(c => c.block && c.block.type);
1730
1690
 
1731
- const dedupedChunks = chunks.filter(c => {
1732
- const seqSet = this._renderedSeqs.get(sessionId);
1733
- return !seqSet || !seqSet.has(c.sequence);
1734
- });
1691
+ const seenSeqs = (this._renderedSeqs || {})[sessionId];
1692
+ const dedupedChunks = chunks.filter(c => !seenSeqs || !seenSeqs.has(c.sequence));
1735
1693
 
1736
1694
  if (dedupedChunks.length > 0) {
1737
- this.renderChunkBatch(dedupedChunks);
1695
+ for (const chunk of dedupedChunks) this.renderChunk(chunk);
1738
1696
  }
1739
1697
  } catch (e) {
1740
1698
  console.warn('Chunk recovery failed:', e.message);
@@ -1752,46 +1710,6 @@ class AgentGUIClient {
1752
1710
  return promise;
1753
1711
  }
1754
1712
 
1755
- _getAdaptivePollInterval() {
1756
- const quality = this.wsManager?.latency?.quality || 'unknown';
1757
- const base = this._pollIntervalByTier[quality] || 200;
1758
- const trend = this.wsManager?.latency?.trend;
1759
- if (!trend || trend === 'stable') return base;
1760
- const tiers = ['excellent', 'good', 'fair', 'poor', 'bad'];
1761
- const idx = tiers.indexOf(quality);
1762
- if (trend === 'rising' && idx < tiers.length - 1) return this._pollIntervalByTier[tiers[idx + 1]];
1763
- if (trend === 'falling' && idx > 0) return this._pollIntervalByTier[tiers[idx - 1]];
1764
- return base;
1765
- }
1766
-
1767
- _chunkArrivalConfidence() {
1768
- if (this._chunkTimingUpdateCount < 2) return 0;
1769
- const base = Math.min(1, this._chunkTimingUpdateCount / 8);
1770
- const penalty = Math.min(1, this._chunkMissedPredictions * 0.33);
1771
- return Math.max(0, base - penalty);
1772
- }
1773
-
1774
- _predictedNextChunkArrival() {
1775
- if (!this._chunkTimingKalman || this._chunkTimingUpdateCount < 2) return 0;
1776
- return this._lastChunkArrival + Math.min(this._chunkTimingKalman.predict(), 5000);
1777
- }
1778
-
1779
- _schedulePreAllocation(sessionId) {
1780
- if (this._placeholderTimer) clearTimeout(this._placeholderTimer);
1781
- if (this._chunkArrivalConfidence() < 0.5) return;
1782
- const scrollContainer = document.getElementById('output-scroll');
1783
- if (!scrollContainer) return;
1784
- const distFromBottom = scrollContainer.scrollHeight - scrollContainer.scrollTop - scrollContainer.clientHeight;
1785
- if (distFromBottom > 150) return;
1786
- const nextArrival = this._predictedNextChunkArrival();
1787
- if (!nextArrival) return;
1788
- const delay = Math.max(0, nextArrival - performance.now() - 100);
1789
- this._placeholderTimer = setTimeout(() => {
1790
- this._placeholderTimer = null;
1791
- this._insertPlaceholder(sessionId);
1792
- }, delay);
1793
- }
1794
-
1795
1713
  _insertPlaceholder(sessionId) {
1796
1714
  this._removePlaceholder();
1797
1715
  const streamingEl = document.getElementById(`streaming-${sessionId}`);
@@ -1857,27 +1775,14 @@ class AgentGUIClient {
1857
1775
 
1858
1776
  _setupDebugHooks() {
1859
1777
  if (typeof window === 'undefined') return;
1860
- const kalmanHistory = { latency: [], scroll: [], chunkTiming: [] };
1861
1778
  const self = this;
1862
- window.__kalman = {
1863
- latency: this.wsManager?._latencyKalman || null,
1864
- scroll: this._scrollKalman || null,
1865
- chunkTiming: this._chunkTimingKalman || null,
1866
- history: kalmanHistory,
1779
+ window.__debug = {
1867
1780
  getState: () => ({
1868
- latency: self.wsManager?._latencyKalman?.getState() || null,
1869
- scroll: self._scrollKalman?.getState() || null,
1870
- chunkTiming: self._chunkTimingKalman?.getState() || null,
1781
+ latencyEma: self.wsManager?._latencyEma || null,
1871
1782
  serverProcessingEstimate: self._serverProcessingEstimate,
1872
- chunkConfidence: self._chunkArrivalConfidence(),
1873
1783
  latencyTrend: self.wsManager?.latency?.trend || null
1874
1784
  })
1875
1785
  };
1876
-
1877
- this.wsManager.on('latency_prediction', (data) => {
1878
- kalmanHistory.latency.push({ time: Date.now(), ...data });
1879
- if (kalmanHistory.latency.length > 100) kalmanHistory.latency.shift();
1880
- });
1881
1786
  }
1882
1787
 
1883
1788
  /**
@@ -2006,174 +1911,6 @@ class AgentGUIClient {
2006
1911
  }
2007
1912
  }
2008
1913
 
2009
- /**
2010
- * Fetch chunks from database for a conversation
2011
- * Supports incremental updates with since parameter
2012
- */
2013
- async fetchChunks(conversationId, since = 0) {
2014
- if (!conversationId) return [];
2015
-
2016
- try {
2017
- const data = await window.wsClient.rpc('conv.chunks', { id: conversationId, since: since > 0 ? since : 0 });
2018
- if (!data.ok || !Array.isArray(data.chunks)) {
2019
- throw new Error('Invalid chunks response');
2020
- }
2021
-
2022
- const chunks = data.chunks.map(chunk => ({
2023
- ...chunk,
2024
- block: typeof chunk.data === 'string' ? JSON.parse(chunk.data) : chunk.data
2025
- }));
2026
-
2027
- return chunks;
2028
- } catch (error) {
2029
- if (error.name === 'AbortError') return [];
2030
- console.error('Error fetching chunks:', error);
2031
- throw error;
2032
- }
2033
- }
2034
-
2035
- /**
2036
- * Poll for new chunks at regular intervals
2037
- * Uses exponential backoff on errors
2038
- * Also checks session status to detect completion
2039
- */
2040
- async startChunkPolling(conversationId) {
2041
- if (!conversationId) return;
2042
-
2043
- const pollState = this.chunkPollState;
2044
- if (pollState.isPolling) return;
2045
-
2046
- pollState.isPolling = true;
2047
- // Only reset lastFetchTimestamp if it wasn't already set by initial fetch
2048
- if (pollState.lastFetchTimestamp === 0) {
2049
- pollState.lastFetchTimestamp = Date.now();
2050
- }
2051
- pollState.backoffDelay = this._getAdaptivePollInterval();
2052
- pollState.sessionCheckCounter = 0;
2053
- pollState.emptyPollCount = 0;
2054
-
2055
- const checkSessionStatus = async () => {
2056
- if (!this.state.currentSession?.id) return false;
2057
- let session;
2058
- try { ({ session } = await window.wsClient.rpc('sess.get', { id: this.state.currentSession.id })); } catch { return false; }
2059
- if (session && (session.status === 'complete' || session.status === 'error')) {
2060
- if (session.status === 'complete') {
2061
- this.handleStreamingComplete({ sessionId: session.id, conversationId, timestamp: Date.now() });
2062
- } else {
2063
- this.handleStreamingError({ sessionId: session.id, conversationId, error: session.error || 'Unknown error', timestamp: Date.now() });
2064
- }
2065
- return true;
2066
- }
2067
- return false;
2068
- };
2069
-
2070
- const pollOnce = async () => {
2071
- if (!pollState.isPolling) return;
2072
-
2073
- try {
2074
- pollState.sessionCheckCounter++;
2075
- const shouldCheckSession = pollState.sessionCheckCounter % 3 === 0 || pollState.emptyPollCount >= 3;
2076
- if (shouldCheckSession) {
2077
- const done = await checkSessionStatus();
2078
- if (done) return;
2079
- if (pollState.emptyPollCount >= 3) pollState.emptyPollCount = 0;
2080
- }
2081
-
2082
- const chunks = await this.fetchChunks(conversationId, pollState.lastFetchTimestamp);
2083
-
2084
- if (chunks.length > 0) {
2085
- pollState.backoffDelay = this._getAdaptivePollInterval();
2086
- pollState.emptyPollCount = 0;
2087
- const lastChunk = chunks[chunks.length - 1];
2088
- pollState.lastFetchTimestamp = lastChunk.created_at;
2089
-
2090
- const now = performance.now();
2091
- if (this._lastChunkArrival > 0 && this._chunkTimingKalman) {
2092
- const delta = now - this._lastChunkArrival;
2093
- this._chunkTimingKalman.update(delta);
2094
- this._chunkTimingUpdateCount++;
2095
- this._chunkMissedPredictions = 0;
2096
- }
2097
- this._lastChunkArrival = now;
2098
-
2099
- this.renderChunkBatch(chunks.filter(c => c.block && c.block.type));
2100
- if (this.state.currentSession?.id) this._schedulePreAllocation(this.state.currentSession.id);
2101
- } else {
2102
- pollState.emptyPollCount++;
2103
- if (this._chunkTimingUpdateCount > 0) this._chunkMissedPredictions++;
2104
- pollState.backoffDelay = Math.min(pollState.backoffDelay + 50, 500);
2105
- }
2106
-
2107
- if (pollState.isPolling) {
2108
- let nextDelay = pollState.backoffDelay;
2109
- if (this._chunkArrivalConfidence() >= 0.3 && this._chunkTimingKalman) {
2110
- const predicted = this._chunkTimingKalman.predict();
2111
- const elapsed = performance.now() - this._lastChunkArrival;
2112
- const untilNext = predicted - elapsed - 20;
2113
- nextDelay = Math.max(50, Math.min(2000, untilNext));
2114
- if (this._chunkMissedPredictions >= 3) {
2115
- this._chunkTimingKalman.setProcessNoise(20);
2116
- } else {
2117
- this._chunkTimingKalman.setProcessNoise(10);
2118
- }
2119
- }
2120
- pollState.pollTimer = setTimeout(pollOnce, nextDelay);
2121
- }
2122
- } catch (error) {
2123
- console.warn('Chunk poll error:', error.message);
2124
- pollState.backoffDelay = Math.min(pollState.backoffDelay * 2, pollState.maxBackoffDelay);
2125
- if (pollState.isPolling) {
2126
- pollState.pollTimer = setTimeout(pollOnce, pollState.backoffDelay);
2127
- }
2128
- }
2129
- };
2130
-
2131
- pollOnce();
2132
- }
2133
-
2134
- /**
2135
- * Stop polling for chunks
2136
- */
2137
- stopChunkPolling() {
2138
- const pollState = this.chunkPollState;
2139
-
2140
- if (pollState.pollTimer) {
2141
- clearTimeout(pollState.pollTimer);
2142
- pollState.pollTimer = null;
2143
- }
2144
-
2145
- if (pollState.abortController) {
2146
- pollState.abortController.abort();
2147
- pollState.abortController = null;
2148
- }
2149
-
2150
- pollState.isPolling = false;
2151
- this._scrollAnimating = false;
2152
- if (this._scrollKalman) this._scrollKalman.reset();
2153
- if (this._chunkTimingKalman) this._chunkTimingKalman.reset();
2154
- this._chunkTimingUpdateCount = 0;
2155
- this._chunkMissedPredictions = 0;
2156
- this._lastChunkArrival = 0;
2157
- if (this._placeholderTimer) { clearTimeout(this._placeholderTimer); this._placeholderTimer = null; }
2158
- }
2159
-
2160
- /**
2161
- * Fetch any remaining chunks after streaming completes
2162
- * Ensures all output is visible without requiring a page refresh
2163
- */
2164
- async fetchRemainingChunks(conversationId, sessionId) {
2165
- try {
2166
- const lastTimestamp = this.chunkPollState.lastFetchTimestamp || 0;
2167
- const chunks = await this.fetchChunks(conversationId, lastTimestamp);
2168
- const sessionChunks = chunks.filter(c => c.sessionId === sessionId && c.block && c.block.type);
2169
- if (sessionChunks.length > 0) {
2170
- this.renderChunkBatch(sessionChunks);
2171
- }
2172
- } catch (err) {
2173
- console.error('Failed to fetch remaining chunks:', err);
2174
- }
2175
- }
2176
-
2177
1914
  /**
2178
1915
  * Render a single chunk to the output
2179
1916
  */
@@ -2203,71 +1940,6 @@ class AgentGUIClient {
2203
1940
  this.scrollToBottom();
2204
1941
  }
2205
1942
 
2206
- renderChunkBatch(chunks) {
2207
- if (!chunks.length) return;
2208
- const deduped = [];
2209
- for (const chunk of chunks) {
2210
- const sid = chunk.sessionId;
2211
- if (!this._renderedSeqs.has(sid)) this._renderedSeqs.set(sid, new Set());
2212
- const seqSet = this._renderedSeqs.get(sid);
2213
- if (chunk.sequence !== undefined && seqSet.has(chunk.sequence)) continue;
2214
- if (chunk.sequence !== undefined) seqSet.add(chunk.sequence);
2215
- deduped.push(chunk);
2216
- }
2217
- if (!deduped.length) return;
2218
-
2219
- let toRender = deduped;
2220
- if (this._consolidator) {
2221
- const { consolidated, stats } = this._consolidator.consolidate(deduped);
2222
- toRender = consolidated;
2223
- for (const c of consolidated) {
2224
- if (c._mergedSequences) {
2225
- const seqSet = this._renderedSeqs.get(c.sessionId);
2226
- if (seqSet) c._mergedSequences.forEach(s => seqSet.add(s));
2227
- }
2228
- }
2229
- if (stats.textMerged || stats.toolsCollapsed || stats.systemSuperseded) {
2230
- console.log('Consolidation:', stats);
2231
- }
2232
- }
2233
-
2234
- this._removePlaceholder();
2235
- const groups = {};
2236
- for (const chunk of toRender) {
2237
- const sid = chunk.sessionId;
2238
- if (!groups[sid]) groups[sid] = [];
2239
- groups[sid].push(chunk);
2240
- }
2241
- let appended = false;
2242
- for (const sid of Object.keys(groups)) {
2243
- const streamingEl = document.getElementById(`streaming-${sid}`);
2244
- if (!streamingEl) continue;
2245
- const blocksEl = streamingEl.querySelector('.streaming-blocks');
2246
- if (!blocksEl) continue;
2247
- for (const chunk of groups[sid]) {
2248
- if (chunk.block.type === 'tool_result') {
2249
- const matchById = chunk.block.tool_use_id && blocksEl.querySelector(`.block-tool-use[data-tool-use-id="${chunk.block.tool_use_id}"]`);
2250
- const lastEl = blocksEl.lastElementChild;
2251
- const toolUseEl = matchById || (lastEl?.classList?.contains('block-tool-use') ? lastEl : null);
2252
- if (toolUseEl) {
2253
- toolUseEl.classList.remove('has-success', 'has-error');
2254
- toolUseEl.classList.add(chunk.block.is_error ? 'has-error' : 'has-success');
2255
- const parentIsOpen = toolUseEl.hasAttribute('open');
2256
- const contextWithParent = { ...chunk, parentIsOpen };
2257
- const el = this.renderer.renderBlock(chunk.block, contextWithParent, blocksEl);
2258
- if (el) { toolUseEl.appendChild(el); appended = true; }
2259
- continue;
2260
- }
2261
- }
2262
- const el = this.renderer.renderBlock(chunk.block, chunk, blocksEl);
2263
- if (!el) { appended = true; continue; }
2264
- blocksEl.appendChild(el);
2265
- appended = true;
2266
- }
2267
- }
2268
- if (appended) this.scrollToBottom();
2269
- }
2270
-
2271
1943
  /**
2272
1944
  * Load agents
2273
1945
  */
@@ -2744,7 +2416,7 @@ class AgentGUIClient {
2744
2416
  const prevConversationId = this.state.currentConversation?.id;
2745
2417
  const availableFallback = this.state.conversations?.find(c => c.id !== conversationId) || null;
2746
2418
  this.cacheCurrentConversation();
2747
- this.stopChunkPolling();
2419
+
2748
2420
  this.removeScrollUpDetection();
2749
2421
  if (this.renderer.resetScrollState) this.renderer.resetScrollState();
2750
2422
  this._userScrolledUp = false;
@@ -3031,12 +2703,6 @@ class AgentGUIClient {
3031
2703
 
3032
2704
  this.updateUrlForConversation(conversationId, latestSession.id);
3033
2705
 
3034
- const lastChunkTime = chunks.length > 0
3035
- ? chunks[chunks.length - 1].created_at
3036
- : 0;
3037
-
3038
- this.chunkPollState.lastFetchTimestamp = lastChunkTime;
3039
- this.startChunkPolling(conversationId);
3040
2706
  // IMMUTABLE: Prompt remains enabled - syncPromptState will set correct state
3041
2707
  this.syncPromptState(conversationId);
3042
2708
  } else {
@@ -3421,7 +3087,7 @@ class AgentGUIClient {
3421
3087
  * Cleanup resources
3422
3088
  */
3423
3089
  destroy() {
3424
- this.stopChunkPolling();
3090
+
3425
3091
  this.renderer.destroy();
3426
3092
  this.wsManager.destroy();
3427
3093
  this.eventHandlers = {};
@@ -40,7 +40,7 @@ class WebSocketManager {
40
40
  pingCounter: 0
41
41
  };
42
42
 
43
- this._latencyKalman = typeof KalmanFilter !== 'undefined' ? new KalmanFilter({ processNoise: 1, measurementNoise: 10 }) : null;
43
+ this._latencyEma = null; // exponential moving average for latency
44
44
  this._trendHistory = [];
45
45
  this._trendCount = 0;
46
46
  this._reconnectedAt = 0;
@@ -84,6 +84,7 @@ class WebSocketManager {
84
84
 
85
85
  try {
86
86
  this.ws = new WebSocket(this.config.url);
87
+ this.ws.binaryType = 'arraybuffer';
87
88
  this.ws.onopen = () => this.onOpen();
88
89
  this.ws.onmessage = (event) => this.onMessage(event);
89
90
  this.ws.onerror = (error) => this.onError(error);
@@ -129,9 +130,19 @@ class WebSocketManager {
129
130
  this.emit('connected', { timestamp: Date.now() });
130
131
  }
131
132
 
132
- onMessage(event) {
133
+ async onMessage(event) {
133
134
  try {
134
- const parsed = JSON.parse(event.data);
135
+ 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
+ }
135
146
  const messages = Array.isArray(parsed) ? parsed : [parsed];
136
147
  this.stats.totalMessagesReceived += messages.length;
137
148
 
@@ -154,6 +165,11 @@ class WebSocketManager {
154
165
  );
155
166
  }
156
167
 
168
+ // RPC reply envelopes — emit for WsClient to intercept, then skip broadcast
169
+ if (data.r !== undefined && !data.type) {
170
+ this.emit('message', data);
171
+ continue;
172
+ }
157
173
  this.emit('message', data);
158
174
  if (data.type) this.emit('message:' + data.type, data);
159
175
  }
@@ -181,21 +197,12 @@ class WebSocketManager {
181
197
 
182
198
  this.latency.current = rtt;
183
199
 
184
- if (this._latencyKalman && samples.length > 3) {
185
- if (this._reconnectedAt && Date.now() - this._reconnectedAt < 5000) {
186
- this._latencyKalman.setMeasurementNoise(50);
187
- } else {
188
- this._latencyKalman.setMeasurementNoise(10);
189
- }
190
- const result = this._latencyKalman.update(rtt);
191
- this.latency.predicted = result.estimate;
192
- this.latency.predictedNext = this._latencyKalman.predict();
193
- this.latency.avg = result.estimate;
194
- } else {
195
- this.latency.avg = samples.reduce((a, b) => a + b, 0) / samples.length;
196
- this.latency.predicted = this.latency.avg;
197
- this.latency.predictedNext = this.latency.avg;
198
- }
200
+ // EMA smoothing (α=0.2 slow adaptation, less noise)
201
+ const alpha = 0.2;
202
+ this._latencyEma = this._latencyEma === null ? rtt : alpha * rtt + (1 - alpha) * this._latencyEma;
203
+ this.latency.avg = this._latencyEma;
204
+ this.latency.predicted = this._latencyEma;
205
+ this.latency.predictedNext = this._latencyEma;
199
206
 
200
207
  if (samples.length > 1) {
201
208
  const mean = samples.reduce((a, b) => a + b, 0) / samples.length;
@@ -233,7 +240,7 @@ class WebSocketManager {
233
240
  predicted: this.latency.predicted,
234
241
  predictedNext: this.latency.predictedNext,
235
242
  trend: this.latency.trend,
236
- gain: this._latencyKalman ? this._latencyKalman.getState().gain : 0
243
+ gain: 0
237
244
  });
238
245
 
239
246
  this._checkDegradation();
@@ -370,8 +377,8 @@ class WebSocketManager {
370
377
  this._hiddenAt = Date.now();
371
378
  return;
372
379
  }
373
- if (this._hiddenAt && this._latencyKalman && Date.now() - this._hiddenAt > 30000) {
374
- this._latencyKalman.reset();
380
+ if (this._hiddenAt && Date.now() - this._hiddenAt > 30000) {
381
+ this._latencyEma = null;
375
382
  this._trendHistory = [];
376
383
  this.latency.trend = 'stable';
377
384
  }
@@ -436,7 +443,7 @@ class WebSocketManager {
436
443
  }
437
444
 
438
445
  try {
439
- this.ws.send(JSON.stringify(data));
446
+ this.ws.send(msgpackr.pack(data));
440
447
  this.stats.totalMessagesSent++;
441
448
  return true;
442
449
  } catch (error) {
@@ -460,7 +467,7 @@ class WebSocketManager {
460
467
  this.messageBuffer = [];
461
468
  for (const message of messages) {
462
469
  try {
463
- this.ws.send(JSON.stringify(message));
470
+ this.ws.send(msgpackr.pack(message));
464
471
  this.stats.totalMessagesSent++;
465
472
  } catch (error) {
466
473
  this.bufferMessage(message);
@@ -484,7 +491,7 @@ class WebSocketManager {
484
491
  if (type === 'session') msg.sessionId = id;
485
492
  else msg.conversationId = id;
486
493
  try {
487
- this.ws.send(JSON.stringify(msg));
494
+ this.ws.send(msgpackr.pack(msg));
488
495
  this.stats.totalMessagesSent++;
489
496
  } catch (_) {}
490
497
  }
@@ -10,36 +10,21 @@ class WsClient {
10
10
  _install() {
11
11
  if (this._installed) return;
12
12
  this._installed = true;
13
- const origOnMessage = this._ws.onMessage.bind(this._ws);
14
- this._ws.onMessage = (event) => {
15
- try {
16
- const parsed = JSON.parse(event.data);
17
- const messages = Array.isArray(parsed) ? parsed : [parsed];
18
- const passthrough = [];
19
- for (const msg of messages) {
20
- if (msg.r && this._pending.has(msg.r)) {
21
- const p = this._pending.get(msg.r);
22
- this._pending.delete(msg.r);
23
- clearTimeout(p.timer);
24
- if (msg.e) {
25
- p.reject(Object.assign(new Error(msg.e.m || 'RPC error'), { code: msg.e.c }));
26
- } else {
27
- p.resolve(msg.d);
28
- }
29
- } else {
30
- passthrough.push(msg);
31
- }
13
+ // Listen on decoded message objects — websocket-manager emits 'message' with decoded obj
14
+ this._ws.on('message', (data) => {
15
+ if (data.r && this._pending.has(data.r)) {
16
+ const p = this._pending.get(data.r);
17
+ this._pending.delete(data.r);
18
+ clearTimeout(p.timer);
19
+ if (data.e) {
20
+ p.reject(Object.assign(new Error(data.e.m || 'RPC error'), { code: data.e.c }));
21
+ } else {
22
+ p.resolve(data.d);
32
23
  }
33
- if (passthrough.length > 0) {
34
- const rebuilt = passthrough.length === 1
35
- ? JSON.stringify(passthrough[0])
36
- : JSON.stringify(passthrough);
37
- origOnMessage({ data: rebuilt });
38
- }
39
- } catch (_) {
40
- origOnMessage(event);
24
+ return; // consumed don't re-emit
41
25
  }
42
- };
26
+ // Non-RPC messages are already emitted by websocket-manager; nothing to do
27
+ });
43
28
  this._ws.on('disconnected', () => this.cancelAll());
44
29
  }
45
30
 
@@ -79,7 +64,7 @@ class WsClient {
79
64
  }
80
65
 
81
66
  cancelAll() {
82
- for (const [id, p] of this._pending) {
67
+ for (const [, p] of this._pending) {
83
68
  clearTimeout(p.timer);
84
69
  p.reject(new Error('Connection lost'));
85
70
  }
@@ -1,67 +0,0 @@
1
- class KalmanFilter {
2
- constructor(config = {}) {
3
- this._initEst = config.initialEstimate || 0;
4
- this._initErr = config.initialError || 1000;
5
- this._q = Math.max(config.processNoise || 1, 0.001);
6
- this._r = config.measurementNoise || 10;
7
- this._est = this._initEst;
8
- this._err = this._initErr;
9
- this._gain = 0;
10
- this._initialized = false;
11
- this._lastValid = this._initEst;
12
- }
13
-
14
- update(measurement) {
15
- if (!Number.isFinite(measurement)) {
16
- return { estimate: this._est, error: this._err, gain: this._gain };
17
- }
18
- if (measurement < 0) measurement = 0;
19
- if (!this._initialized) {
20
- this._est = measurement;
21
- this._err = this._r;
22
- this._initialized = true;
23
- this._lastValid = measurement;
24
- this._gain = 1;
25
- return { estimate: this._est, error: this._err, gain: this._gain };
26
- }
27
- let r = this._r;
28
- if (this._est > 0 && Math.abs(measurement - this._est) > this._est * 10) {
29
- r = r * 100;
30
- }
31
- const predErr = this._err + this._q;
32
- this._gain = predErr / (predErr + r);
33
- this._est = this._est + this._gain * (measurement - this._est);
34
- this._err = (1 - this._gain) * predErr;
35
- if (this._err < 1e-10) this._err = 1e-10;
36
- this._lastValid = this._est;
37
- return { estimate: this._est, error: this._err, gain: this._gain };
38
- }
39
-
40
- predict() {
41
- return this._est;
42
- }
43
-
44
- setProcessNoise(q) { this._q = Math.max(q, 0.001); }
45
- setMeasurementNoise(r) { this._r = r; }
46
-
47
- getState() {
48
- return {
49
- estimate: this._est,
50
- error: this._err,
51
- gain: this._gain,
52
- processNoise: this._q,
53
- measurementNoise: this._r,
54
- initialized: this._initialized
55
- };
56
- }
57
-
58
- reset() {
59
- this._est = this._initEst;
60
- this._err = this._initErr;
61
- this._gain = 0;
62
- this._initialized = false;
63
- this._lastValid = this._initEst;
64
- }
65
- }
66
-
67
- if (typeof module !== 'undefined' && module.exports) module.exports = KalmanFilter;