agentgui 1.0.38 → 1.0.40

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.
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Sync Manager - Handles real-time synchronization with automatic reconnection
3
+ * Guarantees: No lost data, perfect recovery, consistent state
4
+ */
5
+ class SyncManager {
6
+ constructor() {
7
+ this.ws = null;
8
+ this.clientId = null;
9
+ this.subscriptions = new Map();
10
+ this.reconnectAttempts = 0;
11
+ this.maxReconnectAttempts = 10;
12
+ this.reconnectDelay = 1000;
13
+ this.isConnected = false;
14
+ this.handlers = new Map();
15
+ this.lastCheckpoint = new Map();
16
+ }
17
+
18
+ /**
19
+ * Connect to sync server with automatic reconnection
20
+ */
21
+ connect() {
22
+ return new Promise((resolve, reject) => {
23
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
24
+ const url = `${protocol}//${window.location.host}${window.__BASE_URL || '/gm'}/sync`;
25
+
26
+ try {
27
+ this.ws = new WebSocket(url);
28
+
29
+ this.ws.onopen = () => {
30
+ console.log('[SyncManager] Connected to server');
31
+ this.isConnected = true;
32
+ this.reconnectAttempts = 0;
33
+ this.emit('connected', { clientId: this.clientId });
34
+
35
+ // Resubscribe to all previously subscribed sessions
36
+ for (const [sessionId, handlers] of this.subscriptions) {
37
+ this.subscribe(sessionId, handlers.onUpdate, handlers.onRecover);
38
+ }
39
+
40
+ resolve();
41
+ };
42
+
43
+ this.ws.onmessage = (event) => {
44
+ this.handleMessage(JSON.parse(event.data));
45
+ };
46
+
47
+ this.ws.onclose = () => {
48
+ console.log('[SyncManager] Disconnected from server');
49
+ this.isConnected = false;
50
+ this.attemptReconnect();
51
+ };
52
+
53
+ this.ws.onerror = (error) => {
54
+ console.error('[SyncManager] WebSocket error:', error);
55
+ reject(error);
56
+ };
57
+ } catch (err) {
58
+ console.error('[SyncManager] Failed to create WebSocket:', err);
59
+ reject(err);
60
+ }
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Handle incoming messages
66
+ */
67
+ handleMessage(message) {
68
+ const { type, sessionId, clientId } = message;
69
+
70
+ if (type === 'sync_connected') {
71
+ this.clientId = message.clientId;
72
+ console.log(`[SyncManager] Assigned client ID: ${this.clientId}`);
73
+ } else if (type === 'state_snapshot') {
74
+ // Received state after subscription
75
+ console.log(`[SyncManager] Received state snapshot for ${sessionId}`);
76
+ this.lastCheckpoint.set(sessionId, message.state.checkpoint);
77
+
78
+ const handlers = this.subscriptions.get(sessionId);
79
+ if (handlers?.onRecover) {
80
+ handlers.onRecover(message.state);
81
+ }
82
+ } else if (type === 'recovery_response') {
83
+ // Received full state recovery
84
+ console.log(`[SyncManager] Received recovery response for ${sessionId}`);
85
+ this.lastCheckpoint.set(sessionId, message.state.checkpoint);
86
+
87
+ const handlers = this.subscriptions.get(sessionId);
88
+ if (handlers?.onRecover) {
89
+ handlers.onRecover(message.state);
90
+ }
91
+ } else if (type === 'stream_update') {
92
+ // Real-time update from server
93
+ this.lastCheckpoint.set(sessionId, message.timestamp);
94
+
95
+ const handlers = this.subscriptions.get(sessionId);
96
+ if (handlers?.onUpdate) {
97
+ try {
98
+ handlers.onUpdate(message);
99
+ } catch (err) {
100
+ console.error(`[SyncManager] Error in update handler: ${err.message}`);
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Subscribe to session updates with callbacks
108
+ * @param {string} sessionId
109
+ * @param {Function} onUpdate - Called for each real-time update
110
+ * @param {Function} onRecover - Called with full state on subscribe/reconnect
111
+ */
112
+ subscribe(sessionId, onUpdate, onRecover) {
113
+ if (!this.subscriptions.has(sessionId)) {
114
+ this.subscriptions.set(sessionId, { onUpdate, onRecover });
115
+ }
116
+
117
+ if (this.isConnected && this.ws?.readyState === WebSocket.OPEN) {
118
+ this.ws.send(JSON.stringify({
119
+ type: 'subscribe',
120
+ sessionId
121
+ }));
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Unsubscribe from session
127
+ */
128
+ unsubscribe(sessionId) {
129
+ this.subscriptions.delete(sessionId);
130
+ this.lastCheckpoint.delete(sessionId);
131
+
132
+ if (this.isConnected && this.ws?.readyState === WebSocket.OPEN) {
133
+ this.ws.send(JSON.stringify({
134
+ type: 'unsubscribe',
135
+ sessionId
136
+ }));
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Request recovery from a specific checkpoint
142
+ * Called when client detects missing data
143
+ */
144
+ requestRecovery(sessionId) {
145
+ console.log(`[SyncManager] Requesting recovery for ${sessionId}`);
146
+
147
+ if (this.isConnected && this.ws?.readyState === WebSocket.OPEN) {
148
+ this.ws.send(JSON.stringify({
149
+ type: 'recovery_request',
150
+ sessionId
151
+ }));
152
+ } else {
153
+ // If not connected, recover when connection is restored
154
+ this.connect().then(() => {
155
+ this.ws.send(JSON.stringify({
156
+ type: 'recovery_request',
157
+ sessionId
158
+ }));
159
+ });
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Verify data consistency by querying server
165
+ */
166
+ async validateSession(sessionId) {
167
+ const baseUrl = window.__BASE_URL || '/gm';
168
+ try {
169
+ const response = await fetch(`${baseUrl}/api/sessions/${sessionId}/validate`);
170
+ const validation = await response.json();
171
+ return validation;
172
+ } catch (err) {
173
+ console.error(`[SyncManager] Validation failed: ${err.message}`);
174
+ return null;
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Fetch full state for recovery
180
+ */
181
+ async fetchSessionState(sessionId) {
182
+ const baseUrl = window.__BASE_URL || '/gm';
183
+ try {
184
+ const response = await fetch(`${baseUrl}/api/sessions/${sessionId}/state-recovery`);
185
+ if (!response.ok) return null;
186
+ return await response.json();
187
+ } catch (err) {
188
+ console.error(`[SyncManager] Failed to fetch session state: ${err.message}`);
189
+ return null;
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Automatic reconnection with exponential backoff
195
+ */
196
+ attemptReconnect() {
197
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
198
+ console.error('[SyncManager] Max reconnection attempts reached');
199
+ this.emit('reconnect_failed');
200
+ return;
201
+ }
202
+
203
+ this.reconnectAttempts++;
204
+ const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
205
+ console.log(`[SyncManager] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
206
+
207
+ setTimeout(() => {
208
+ this.connect().catch(err => {
209
+ console.error('[SyncManager] Reconnection failed:', err);
210
+ this.attemptReconnect();
211
+ });
212
+ }, delay);
213
+ }
214
+
215
+ /**
216
+ * Detect missing updates by checking sequence gaps
217
+ */
218
+ detectMissingUpdates(updates) {
219
+ const gaps = [];
220
+ for (let i = 0; i < updates.length - 1; i++) {
221
+ if (updates[i + 1].sequence !== updates[i].sequence + 1) {
222
+ gaps.push({
223
+ expected: updates[i].sequence + 1,
224
+ actual: updates[i + 1].sequence
225
+ });
226
+ }
227
+ }
228
+ return gaps;
229
+ }
230
+
231
+ /**
232
+ * Register event listener
233
+ */
234
+ on(event, callback) {
235
+ if (!this.handlers.has(event)) {
236
+ this.handlers.set(event, []);
237
+ }
238
+ this.handlers.get(event).push(callback);
239
+ }
240
+
241
+ /**
242
+ * Emit event
243
+ */
244
+ emit(event, data) {
245
+ const callbacks = this.handlers.get(event) || [];
246
+ for (const callback of callbacks) {
247
+ try {
248
+ callback(data);
249
+ } catch (err) {
250
+ console.error(`[SyncManager] Error in ${event} handler: ${err.message}`);
251
+ }
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Close connection gracefully
257
+ */
258
+ disconnect() {
259
+ this.subscriptions.clear();
260
+ this.lastCheckpoint.clear();
261
+ if (this.ws) {
262
+ this.ws.close();
263
+ this.ws = null;
264
+ }
265
+ }
266
+ }
267
+
268
+ // Export as global for browser use
269
+ if (typeof window !== 'undefined') {
270
+ window.SyncManager = SyncManager;
271
+ }
272
+
273
+ export default SyncManager;
@@ -0,0 +1,101 @@
1
+ import { queries } from './database.js';
2
+ import { StateValidator } from './state-validator.js';
3
+
4
+ export class StreamHandler {
5
+ constructor(sessionId, conversationId, broadcastFn) {
6
+ this.sessionId = sessionId;
7
+ this.conversationId = conversationId;
8
+ this.broadcastFn = broadcastFn;
9
+ this.updateCount = 0;
10
+ this.sequence = -1;
11
+ this.hasText = false;
12
+ this.hasBlocks = false;
13
+ this.blocks = [];
14
+ this.stateCheckpoint = StateValidator.getSessionState(sessionId);
15
+ }
16
+
17
+ handleUpdate(params, baseUrl) {
18
+ const u = params.update;
19
+ if (!u) return;
20
+
21
+ const kind = u.sessionUpdate;
22
+ if (kind === 'agent_message_chunk' && u.content?.text) {
23
+ this.hasText = true;
24
+ const update = {
25
+ type: 'text',
26
+ content: u.content.text,
27
+ timestamp: Date.now()
28
+ };
29
+ this.persistAndBroadcast('text', update, baseUrl);
30
+ } else if (kind === 'html_content' && u.content?.html) {
31
+ this.hasBlocks = true;
32
+ const update = {
33
+ type: 'html',
34
+ html: u.content.html,
35
+ title: u.content.title,
36
+ id: u.content.id,
37
+ timestamp: Date.now()
38
+ };
39
+ this.blocks.push({ type: 'html', html: u.content.html, title: u.content.title, id: u.content.id });
40
+ this.persistAndBroadcast('html', update, baseUrl);
41
+ } else if (kind === 'image_content' && u.content?.path) {
42
+ this.hasBlocks = true;
43
+ const imageUrl = baseUrl + '/api/image/' + encodeURIComponent(u.content.path);
44
+ const update = {
45
+ type: 'image',
46
+ path: u.content.path,
47
+ url: imageUrl,
48
+ title: u.content.title,
49
+ alt: u.content.alt,
50
+ timestamp: Date.now()
51
+ };
52
+ this.blocks.push({ type: 'image', path: u.content.path, url: imageUrl, title: u.content.title, alt: u.content.alt });
53
+ this.persistAndBroadcast('image', update, baseUrl);
54
+ }
55
+ }
56
+
57
+ persistAndBroadcast(updateType, update, baseUrl) {
58
+ try {
59
+ // CRITICAL: Database write MUST complete before broadcast
60
+ // This guarantees database is source of truth
61
+ const persistedUpdate = queries.createStreamUpdate(this.sessionId, this.conversationId, updateType, update);
62
+ this.sequence = persistedUpdate.sequence;
63
+ this.updateCount++;
64
+
65
+ // Validate consistency after write
66
+ const validation = StateValidator.validateSession(this.sessionId);
67
+ if (!validation.valid) {
68
+ console.error(`[StreamHandler] State validation failed after update:`, validation);
69
+ // Log but continue - database is still source of truth
70
+ }
71
+
72
+ // CRITICAL: Broadcast happens AFTER database write confirms
73
+ // This ensures clients see data that's already persisted
74
+ this.broadcastFn({
75
+ type: 'stream_update',
76
+ sessionId: this.sessionId,
77
+ conversationId: this.conversationId,
78
+ updateType,
79
+ update: persistedUpdate.content,
80
+ sequence: this.sequence,
81
+ persisted: true,
82
+ timestamp: persistedUpdate.created_at,
83
+ validation: validation.valid ? undefined : { error: validation.error }
84
+ });
85
+ } catch (err) {
86
+ console.error(`[StreamHandler] Error persisting update: ${err.message}`);
87
+ // On persistence failure, do NOT broadcast - maintain consistency
88
+ throw err;
89
+ }
90
+ }
91
+
92
+ getBlocks() {
93
+ return this.blocks;
94
+ }
95
+
96
+ getUpdateCount() {
97
+ return this.updateCount;
98
+ }
99
+ }
100
+
101
+ export default StreamHandler;
Binary file
@@ -1,284 +0,0 @@
1
- # Automatic Continuous Importing Feature
2
-
3
- ## Overview
4
- AgentGUI now automatically and continuously imports Claude Code conversations **without requiring any user action**. This ensures conversations are always available and up-to-date.
5
-
6
- ## How It Works
7
-
8
- ### Server-Side (Every 30 Seconds)
9
- ```
10
- Server startup
11
-
12
- [IMMEDIATE] Import Claude Code conversations (first run)
13
-
14
- [EVERY 30 SECONDS] Auto-import new conversations
15
-
16
- If new conversations found:
17
- • Add them to database
18
- • Broadcast 'conversations_updated' event to all connected clients
19
- • Log: "[AUTO-IMPORT] Imported X new Claude Code conversations"
20
- ```
21
-
22
- ### Frontend-Side
23
- ```
24
- Page loads
25
-
26
- [IMMEDIATE] Fetch conversations from API
27
-
28
- [EVERY 10 SECONDS] Fetch conversations again (as fallback)
29
-
30
- [ON SYNC EVENT] Receive 'conversations_updated' from server
31
-
32
- Immediately refresh conversation list
33
-
34
- Users see new conversations appear in real-time
35
- ```
36
-
37
- ## Key Features
38
-
39
- ✅ **Automatic**: No user action needed
40
- ✅ **Continuous**: Runs every 30 seconds (server) and 10 seconds (client)
41
- ✅ **Real-time**: New conversations appear immediately via WebSocket
42
- ✅ **No Duplicates**: Skips conversations already imported
43
- ✅ **Cross-tab**: Broadcasts via BroadcastChannel API
44
- ✅ **Resilient**: Fallback mechanism if WebSocket fails
45
- ✅ **Logging**: All imports logged for debugging
46
-
47
- ## Where Conversations Come From
48
-
49
- ### Automatically Discovered From:
50
- 1. **Claude Code Projects** (~/.claude/projects/)
51
- - Scans sessions-index.json files
52
- - Reads .jsonl message files
53
- - Imports with "[project] title" format
54
-
55
- 2. **Created in AgentGUI**
56
- - New conversations created via UI
57
- - Automatically stored in database
58
-
59
- ## Example Flow
60
-
61
- ### Scenario: User Uses Claude Code, Then Opens AgentGUI
62
-
63
- ```
64
- 11:00:00 - User creates conversation in Claude Code
65
- 11:00:15 - AgentGUI server detects new conversation in ~/.claude/projects/
66
- 11:00:20 - Server imports conversation automatically
67
- 11:00:20 - Server broadcasts 'conversations_updated' event
68
- 11:00:21 - All connected browser tabs receive update
69
- 11:00:21 - Users see new conversation appear in sidebar
70
- ```
71
-
72
- ### Scenario: Multiple Tabs Open
73
-
74
- ```
75
- Tab 1 opens AgentGUI
76
- Tab 2 opens AgentGUI (few seconds later)
77
-
78
- Tab 1 receives 'conversations_updated' from server
79
- Tab 1 uses BroadcastChannel to notify Tab 2
80
- Tab 2 also refreshes conversation list
81
- Both tabs show latest conversations in sync
82
- ```
83
-
84
- ## Server Logs
85
-
86
- You'll see logs like:
87
- ```
88
- [AUTO-IMPORT] Imported 2 new Claude Code conversations (42 already exist)
89
- [AUTO-IMPORT] Imported 1 new Claude Code conversation (43 already exist)
90
- [AUTO-IMPORT] (nothing new this cycle)
91
- ```
92
-
93
- ## Client Logs
94
-
95
- In browser console:
96
- ```
97
- [SYNC] Server imported 3 new conversations, refreshing...
98
- [DEBUG] Init: Auto-imported Claude Code conversations
99
- [DEBUG] Loaded conversations, total: 86
100
- ```
101
-
102
- ## Configuration
103
-
104
- ### Import Frequency (Server)
105
- Current: **30 seconds**
106
- Location: `server.js` line `setInterval(performAutoImport, 30000);`
107
-
108
- To change:
109
- ```javascript
110
- setInterval(performAutoImport, 60000); // 60 seconds
111
- setInterval(performAutoImport, 5000); // 5 seconds
112
- ```
113
-
114
- ### Refresh Frequency (Client)
115
- Current: **10 seconds**
116
- Location: `app.js` line `setInterval(() => { ... }, 10000);`
117
-
118
- To change:
119
- ```javascript
120
- }, 60000); // 60 seconds
121
- }, 5000); // 5 seconds
122
- ```
123
-
124
- ## Data Flow Diagram
125
-
126
- ```
127
- ┌─────────────────────────────────────────────────────┐
128
- │ Claude Code │
129
- │ ~/.claude/projects/*/sessions-index.json │
130
- └─────────────────┬───────────────────────────────────┘
131
- │ (monitors every 30s)
132
-
133
- ┌─────────────────────────────────────────────────────┐
134
- │ AgentGUI Server │
135
- │ • queries.importClaudeCodeConversations() │
136
- │ • Stores in ~/.gmgui/data.db │
137
- │ • Broadcasts via WebSocket │
138
- └──────┬──────────────────────────┬────────────────────┘
139
- │ │
140
- │ (WebSocket event) │ (HTTP API)
141
- ↓ ↓
142
- ┌─────────────────────────────────────────────────────┐
143
- │ Browser (Frontend) │
144
- │ • Listens on sync WebSocket │
145
- │ • Receives 'conversations_updated' event │
146
- │ • Calls fetchConversations() │
147
- │ • Calls renderChatHistory() │
148
- └──────┬──────────────────────────────────────────────┘
149
-
150
-
151
- ┌─────────────────────────────────────────────────────┐
152
- │ Chat Sidebar │
153
- │ • Displays conversation list │
154
- │ • User can click to view conversation │
155
- └─────────────────────────────────────────────────────┘
156
- ```
157
-
158
- ## Troubleshooting
159
-
160
- ### Conversations Not Appearing
161
-
162
- **Check 1: Server Auto-Import**
163
- ```bash
164
- # Look for these logs in server output
165
- grep "\[AUTO-IMPORT\]" server.log
166
-
167
- # If no logs, auto-import might not be running
168
- ```
169
-
170
- **Check 2: Claude Code Availability**
171
- ```bash
172
- # Check if Claude Code projects exist
173
- ls -la ~/.claude/projects/
174
-
175
- # Count projects with conversations
176
- find ~/.claude/projects -name "sessions-index.json" | wc -l
177
- ```
178
-
179
- **Check 3: Browser Sync**
180
- ```javascript
181
- // In browser console
182
- // Check if WebSocket is connected
183
- console.log('WebSocket state:', app.syncWs.ws?.readyState);
184
-
185
- // Try manual refresh
186
- await app.fetchConversations();
187
- app.renderChatHistory();
188
- ```
189
-
190
- **Check 4: Database**
191
- ```bash
192
- # Check total conversations in DB
193
- node -e "
194
- const DB = require('better-sqlite3');
195
- const db = new DB(process.env.HOME + '/.gmgui/data.db');
196
- const count = db.prepare('SELECT COUNT(*) as c FROM conversations').get();
197
- console.log('Database has:', count.c, 'conversations');
198
- db.close();
199
- "
200
- ```
201
-
202
- ### Too Many Import Logs
203
-
204
- If server logs are too verbose:
205
- 1. Increase `setInterval` time (30000 → 60000 or more)
206
- 2. Add log level filtering
207
-
208
- ### Conversations Take Too Long to Appear
209
-
210
- If new conversations take > 1 minute:
211
- 1. Check server auto-import interval (default 30s)
212
- 2. Check client refresh interval (default 10s)
213
- 3. Check network connectivity
214
- 4. Check browser console for errors
215
-
216
- ## Performance Considerations
217
-
218
- ### Import Impact
219
- - **Minimal**: Import skips existing conversations
220
- - **Fast**: Only processes new conversations
221
- - **Efficient**: Uses database transactions
222
-
223
- ### Client Impact
224
- - **WebSocket**: Real-time updates, low bandwidth
225
- - **Polling**: Every 10 seconds, minimal traffic
226
- - **Rendering**: Only updates when conversations change
227
-
228
- ## Security Notes
229
-
230
- - Only imports from user's local `.claude/projects/`
231
- - No external network access needed
232
- - All conversations stored locally
233
- - Respects filesystem permissions
234
-
235
- ## Future Enhancements
236
-
237
- Potential improvements:
238
- - Configurable import interval via UI
239
- - Import from multiple sources
240
- - Batch import optimization
241
- - Import history/logs viewer
242
- - Import statistics dashboard
243
- - Per-project import settings
244
-
245
- ## Testing
246
-
247
- ### Manual Test: Add Claude Code Conversation
248
- ```bash
249
- # 1. Use Claude Code (creates ~/.claude/projects/*/sessions-index.json)
250
- # 2. Wait up to 30 seconds
251
- # 3. Check browser - should see new conversation appear
252
- # 4. Confirm console logs show "[AUTO-IMPORT] Imported X..."
253
- ```
254
-
255
- ### Manual Test: Force Import
256
- ```javascript
257
- // In browser console
258
- await fetch('/gm/api/import/claude-code')
259
- .then(r => r.json())
260
- .then(d => console.log('Manual import result:', d));
261
-
262
- // Then refresh
263
- await app.fetchConversations();
264
- app.renderChatHistory();
265
- ```
266
-
267
- ## Related Files
268
-
269
- - `server.js` - Server auto-import implementation
270
- - `app.js` - Frontend sync handling
271
- - `database.js` - Query functions
272
- - `acp-launcher.js` - Claude Code connection
273
-
274
- ## Changelog
275
-
276
- ### Version 1.1.0 (Current)
277
- - ✅ Added automatic server-side importing every 30 seconds
278
- - ✅ Added WebSocket broadcast for instant updates
279
- - ✅ Added client-side sync event handler
280
- - ✅ Integrated with existing periodic sync
281
-
282
- ### Version 1.0.0 (Previous)
283
- - Manual import on demand via `/api/import/claude-code`
284
- - No automatic background importing