agentgui 1.0.593 → 1.0.595

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.593",
3
+ "version": "1.0.595",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/static/index.html CHANGED
@@ -3240,6 +3240,7 @@
3240
3240
  <script defer src="/gm/js/syntax-highlighter.js"></script>
3241
3241
  <script defer src="/gm/js/dialogs.js"></script>
3242
3242
  <script defer src="/gm/js/ui-components.js"></script>
3243
+ <script defer src="/gm/js/state-barrier.js"></script>
3243
3244
  <script defer src="/gm/js/conversations.js"></script>
3244
3245
  <script defer src="/gm/js/terminal.js"></script>
3245
3246
  <script defer src="/gm/js/script-runner.js"></script>
@@ -491,17 +491,21 @@ class AgentGUIClient {
491
491
  this.showError('Please enter a message to steer');
492
492
  return;
493
493
  }
494
- try {
495
- const data = await window.wsClient.rpc('conv.steer', { id: this.state.currentConversation.id, content: message });
496
- console.log('Steer response:', data);
497
- if (this.ui.messageInput) {
498
- this.ui.messageInput.value = '';
499
- this.ui.messageInput.style.height = 'auto';
500
- }
501
- } catch (err) {
502
- console.error('Failed to steer:', err);
503
- this.showError('Failed to steer: ' + err.message);
494
+
495
+ // Capture message and clear UI immediately (no await)
496
+ const steerMsg = message;
497
+ if (this.ui.messageInput) {
498
+ this.ui.messageInput.value = '';
499
+ this.ui.messageInput.style.height = 'auto';
504
500
  }
501
+
502
+ // Fire RPC in background, don't await
503
+ window.wsClient.rpc('conv.steer', { id: this.state.currentConversation.id, content: steerMsg })
504
+ .then(data => console.log('Steer response:', data))
505
+ .catch(err => {
506
+ console.error('Failed to steer:', err);
507
+ this.showError('Failed to steer: ' + err.message);
508
+ });
505
509
  } else {
506
510
  const instructions = await window.UIDialog.prompt('Enter instructions to inject into the running agent:', '', 'Inject Instructions');
507
511
  if (!instructions) return;
@@ -584,6 +588,7 @@ class AgentGUIClient {
584
588
 
585
589
  // Listen for active conversation deletion
586
590
  window.addEventListener('conversation-deselected', () => {
591
+ window.ConversationState?.clear('deselected');
587
592
  this.state.currentConversation = null;
588
593
  this.state.currentSession = null;
589
594
  this.updateUrlForConversation(null);
@@ -1318,6 +1323,7 @@ class AgentGUIClient {
1318
1323
  }
1319
1324
 
1320
1325
  async handleAllConversationsDeleted(data) {
1326
+ window.ConversationState?.clear('all_deleted');
1321
1327
  this.state.currentConversation = null;
1322
1328
  this.state.conversations = [];
1323
1329
  this.state.sessionEvents = [];
@@ -1556,6 +1562,7 @@ class AgentGUIClient {
1556
1562
  if (model) body.model = model;
1557
1563
  if (subAgent) body.subAgent = subAgent;
1558
1564
  const { conversation } = await window.wsClient.rpc('conv.new', body);
1565
+ window.ConversationState?.selectConversation(conversation.id, 'conversation_created', 1);
1559
1566
  this.state.currentConversation = conversation;
1560
1567
  this.lockAgentAndModel(agentId, model);
1561
1568
 
@@ -1834,6 +1841,7 @@ class AgentGUIClient {
1834
1841
  if (model) createBody.model = model;
1835
1842
  if (subAgent) createBody.subAgent = subAgent;
1836
1843
  const { conversation: newConv } = await window.wsClient.rpc('conv.new', createBody);
1844
+ window.ConversationState?.selectConversation(newConv.id, 'stream_recreate', 1);
1837
1845
  this.state.currentConversation = newConv;
1838
1846
  if (window.conversationManager) {
1839
1847
  window.conversationManager.loadConversations();
@@ -2556,6 +2564,7 @@ class AgentGUIClient {
2556
2564
 
2557
2565
  const cachedConv = this.state.conversations.find(c => c.id === conversationId);
2558
2566
  if (cachedConv && this.state.currentConversation?.id !== conversationId) {
2567
+ window.ConversationState?.selectConversation(conversationId, 'cache_load', 1);
2559
2568
  this.state.currentConversation = cachedConv;
2560
2569
  }
2561
2570
 
@@ -2572,6 +2581,7 @@ class AgentGUIClient {
2572
2581
  while (cached.dom.firstChild) {
2573
2582
  outputEl.appendChild(cached.dom.firstChild);
2574
2583
  }
2584
+ window.ConversationState?.selectConversation(conversationId, 'dom_cache_load', 1);
2575
2585
  this.state.currentConversation = cached.conversation;
2576
2586
  const cachedHasActivity = cached.conversation.messageCount > 0 || this.state.streamingConversations.has(conversationId);
2577
2587
  this.applyAgentAndModelSelection(cached.conversation, cachedHasActivity);
@@ -2594,6 +2604,7 @@ class AgentGUIClient {
2594
2604
  } catch (e) {
2595
2605
  if (e.code === 404) {
2596
2606
  console.warn('Conversation no longer exists:', conversationId);
2607
+ window.ConversationState?.clear('conversation_not_found');
2597
2608
  this.state.currentConversation = null;
2598
2609
  if (window.conversationManager) window.conversationManager.loadConversations();
2599
2610
  // Resume from last successful conversation if available, or fall back to any available conversation
@@ -2613,6 +2624,7 @@ class AgentGUIClient {
2613
2624
  }
2614
2625
  const { conversation, isActivelyStreaming, latestSession, chunks: rawChunks, totalChunks, messages: allMessages } = fullData;
2615
2626
 
2627
+ window.ConversationState?.selectConversation(conversationId, 'server_load', 1);
2616
2628
  this.state.currentConversation = conversation;
2617
2629
  const hasActivity = (allMessages && allMessages.length > 0) || isActivelyStreaming || latestSession || this.state.streamingConversations.has(conversationId);
2618
2630
  this.applyAgentAndModelSelection(conversation, hasActivity);
@@ -301,6 +301,7 @@ class ConversationManager {
301
301
  await window.wsClient.rpc('conv.del.all', {});
302
302
  console.log('[ConversationManager] Deleted all conversations');
303
303
  this._updateConversations([], 'clear_all');
304
+ window.ConversationState?.clear('delete_all');
304
305
  this.activeId = null;
305
306
  window.dispatchEvent(new CustomEvent('conversation-deselected'));
306
307
  this.render();
@@ -535,6 +536,11 @@ class ConversationManager {
535
536
  }
536
537
 
537
538
  select(convId) {
539
+ const result = window.ConversationState?.selectConversation(convId, 'user_click', 1) || { success: false };
540
+ if (!result.success && result.reason !== 'already_selected') {
541
+ console.error('[ConvMgr] activeId mutation rejected:', result.reason);
542
+ return;
543
+ }
538
544
  this.activeId = convId;
539
545
 
540
546
  document.querySelectorAll('.conversation-item').forEach(item => {
@@ -589,6 +595,7 @@ class ConversationManager {
589
595
  const newConvs = this.conversations.filter(c => c.id !== convId);
590
596
  this._updateConversations(newConvs, 'delete', { convId });
591
597
  if (wasActive) {
598
+ window.ConversationState?.deleteConversation(convId, 1);
592
599
  this.activeId = null;
593
600
  window.dispatchEvent(new CustomEvent('conversation-deselected'));
594
601
  }
@@ -607,6 +614,7 @@ class ConversationManager {
607
614
  this.deleteConversation(msg.conversationId);
608
615
  } else if (msg.type === 'all_conversations_deleted') {
609
616
  this._updateConversations([], 'ws_clear_all');
617
+ window.ConversationState?.clear('all_deleted');
610
618
  this.activeId = null;
611
619
  this.streamingConversations.clear();
612
620
  this.showEmpty('No conversations yet');
@@ -0,0 +1,109 @@
1
+ /**
2
+ * State Barrier - Atomic state machine for conversation management
3
+ * Eliminates race conditions through single source of truth and version tracking
4
+ */
5
+
6
+ class ConversationState {
7
+ constructor() {
8
+ this.current = {
9
+ id: null,
10
+ version: 0,
11
+ data: null,
12
+ timestamp: 0,
13
+ reason: null
14
+ };
15
+ this.history = [];
16
+ this.MAX_HISTORY = 50;
17
+ }
18
+
19
+ selectConversation(id, reason, serverVersion) {
20
+ if (id === this.current.id && serverVersion === this.current.version) {
21
+ return { success: false, reason: 'already_selected', prevState: this.current, newState: this.current };
22
+ }
23
+ const prevState = { ...this.current };
24
+ this.current.id = id;
25
+ this.current.version = serverVersion || (this.current.version + 1);
26
+ this.current.timestamp = Date.now();
27
+ this.current.reason = reason;
28
+ this.current.data = null;
29
+ this._recordHistory('selectConversation', prevState, this.current, reason);
30
+ return { success: true, reason: 'selected', prevState, newState: { ...this.current } };
31
+ }
32
+
33
+ updateConversation(id, data, serverVersion) {
34
+ if (id !== this.current.id) {
35
+ return { success: false, reason: 'version_mismatch', prevState: this.current, newState: this.current };
36
+ }
37
+ if (serverVersion && serverVersion < this.current.version) {
38
+ return { success: false, reason: 'stale_version', prevState: this.current, newState: this.current };
39
+ }
40
+ const prevState = { ...this.current };
41
+ this.current.data = { ...this.current.data, ...data };
42
+ this.current.version = serverVersion || (this.current.version + 1);
43
+ this.current.timestamp = Date.now();
44
+ this._recordHistory('updateConversation', prevState, this.current, 'update');
45
+ return { success: true, reason: 'updated', prevState, newState: { ...this.current } };
46
+ }
47
+
48
+ deleteConversation(id, serverVersion) {
49
+ if (id !== this.current.id) {
50
+ return { success: false, reason: 'not_current', prevState: this.current, newState: this.current };
51
+ }
52
+ const prevState = { ...this.current };
53
+ this.current.id = null;
54
+ this.current.version = 0;
55
+ this.current.data = null;
56
+ this.current.timestamp = Date.now();
57
+ this.current.reason = 'deleted';
58
+ this._recordHistory('deleteConversation', prevState, this.current, 'delete');
59
+ return { success: true, reason: 'deleted', prevState, newState: { ...this.current } };
60
+ }
61
+
62
+ clear(reason) {
63
+ const prevState = { ...this.current };
64
+ this.current.id = null;
65
+ this.current.version = 0;
66
+ this.current.data = null;
67
+ this.current.timestamp = Date.now();
68
+ this.current.reason = reason;
69
+ this._recordHistory('clear', prevState, this.current, reason);
70
+ return { success: true, reason: 'cleared', prevState, newState: { ...this.current } };
71
+ }
72
+
73
+ getCurrent() {
74
+ return { ...this.current };
75
+ }
76
+
77
+ getVersion() {
78
+ return this.current.version;
79
+ }
80
+
81
+ _recordHistory(operation, prevState, newState, detail) {
82
+ this.history.push({
83
+ operation,
84
+ prevState,
85
+ newState,
86
+ detail,
87
+ timestamp: Date.now()
88
+ });
89
+ if (this.history.length > this.MAX_HISTORY) {
90
+ this.history.shift();
91
+ }
92
+ }
93
+
94
+ getHistory() {
95
+ return [...this.history];
96
+ }
97
+
98
+ debugDump() {
99
+ return {
100
+ current: { ...this.current },
101
+ history: this.getHistory(),
102
+ timestamp: Date.now()
103
+ };
104
+ }
105
+ }
106
+
107
+ if (typeof window !== 'undefined') {
108
+ window.ConversationState = new ConversationState();
109
+ }