agentgui 1.0.593 → 1.0.594

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.594",
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>
@@ -584,6 +584,7 @@ class AgentGUIClient {
584
584
 
585
585
  // Listen for active conversation deletion
586
586
  window.addEventListener('conversation-deselected', () => {
587
+ window.ConversationState?.clear('deselected');
587
588
  this.state.currentConversation = null;
588
589
  this.state.currentSession = null;
589
590
  this.updateUrlForConversation(null);
@@ -1318,6 +1319,7 @@ class AgentGUIClient {
1318
1319
  }
1319
1320
 
1320
1321
  async handleAllConversationsDeleted(data) {
1322
+ window.ConversationState?.clear('all_deleted');
1321
1323
  this.state.currentConversation = null;
1322
1324
  this.state.conversations = [];
1323
1325
  this.state.sessionEvents = [];
@@ -1556,6 +1558,7 @@ class AgentGUIClient {
1556
1558
  if (model) body.model = model;
1557
1559
  if (subAgent) body.subAgent = subAgent;
1558
1560
  const { conversation } = await window.wsClient.rpc('conv.new', body);
1561
+ window.ConversationState?.selectConversation(conversation.id, 'conversation_created', 1);
1559
1562
  this.state.currentConversation = conversation;
1560
1563
  this.lockAgentAndModel(agentId, model);
1561
1564
 
@@ -1834,6 +1837,7 @@ class AgentGUIClient {
1834
1837
  if (model) createBody.model = model;
1835
1838
  if (subAgent) createBody.subAgent = subAgent;
1836
1839
  const { conversation: newConv } = await window.wsClient.rpc('conv.new', createBody);
1840
+ window.ConversationState?.selectConversation(newConv.id, 'stream_recreate', 1);
1837
1841
  this.state.currentConversation = newConv;
1838
1842
  if (window.conversationManager) {
1839
1843
  window.conversationManager.loadConversations();
@@ -2556,6 +2560,7 @@ class AgentGUIClient {
2556
2560
 
2557
2561
  const cachedConv = this.state.conversations.find(c => c.id === conversationId);
2558
2562
  if (cachedConv && this.state.currentConversation?.id !== conversationId) {
2563
+ window.ConversationState?.selectConversation(conversationId, 'cache_load', 1);
2559
2564
  this.state.currentConversation = cachedConv;
2560
2565
  }
2561
2566
 
@@ -2572,6 +2577,7 @@ class AgentGUIClient {
2572
2577
  while (cached.dom.firstChild) {
2573
2578
  outputEl.appendChild(cached.dom.firstChild);
2574
2579
  }
2580
+ window.ConversationState?.selectConversation(conversationId, 'dom_cache_load', 1);
2575
2581
  this.state.currentConversation = cached.conversation;
2576
2582
  const cachedHasActivity = cached.conversation.messageCount > 0 || this.state.streamingConversations.has(conversationId);
2577
2583
  this.applyAgentAndModelSelection(cached.conversation, cachedHasActivity);
@@ -2594,6 +2600,7 @@ class AgentGUIClient {
2594
2600
  } catch (e) {
2595
2601
  if (e.code === 404) {
2596
2602
  console.warn('Conversation no longer exists:', conversationId);
2603
+ window.ConversationState?.clear('conversation_not_found');
2597
2604
  this.state.currentConversation = null;
2598
2605
  if (window.conversationManager) window.conversationManager.loadConversations();
2599
2606
  // Resume from last successful conversation if available, or fall back to any available conversation
@@ -2613,6 +2620,7 @@ class AgentGUIClient {
2613
2620
  }
2614
2621
  const { conversation, isActivelyStreaming, latestSession, chunks: rawChunks, totalChunks, messages: allMessages } = fullData;
2615
2622
 
2623
+ window.ConversationState?.selectConversation(conversationId, 'server_load', 1);
2616
2624
  this.state.currentConversation = conversation;
2617
2625
  const hasActivity = (allMessages && allMessages.length > 0) || isActivelyStreaming || latestSession || this.state.streamingConversations.has(conversationId);
2618
2626
  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
+ }