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 +1 -1
- package/static/index.html +1 -0
- package/static/js/client.js +8 -0
- package/static/js/conversations.js +8 -0
- package/static/js/state-barrier.js +109 -0
package/package.json
CHANGED
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>
|
package/static/js/client.js
CHANGED
|
@@ -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
|
+
}
|