agentgui 1.0.65 → 1.0.66

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.
@@ -1,150 +0,0 @@
1
- import { queries } from './database.js';
2
- import crypto from 'crypto';
3
-
4
- export class StateValidator {
5
- /**
6
- * Validates data consistency by checking:
7
- * 1. Sequence numbers are consecutive (no gaps)
8
- * 2. Stream updates match database
9
- * 3. Final message matches aggregated stream updates
10
- */
11
- static validateSession(sessionId) {
12
- try {
13
- const session = queries.getSession(sessionId);
14
- if (!session) return { valid: false, error: 'Session not found' };
15
-
16
- const updates = queries.getSessionStreamUpdates(sessionId);
17
-
18
- // Check 1: Sequence continuity
19
- const sequenceGaps = [];
20
- for (let i = 0; i < updates.length - 1; i++) {
21
- if (updates[i + 1].sequence !== updates[i].sequence + 1) {
22
- sequenceGaps.push({ expected: updates[i].sequence + 1, actual: updates[i + 1].sequence });
23
- }
24
- }
25
-
26
- if (sequenceGaps.length > 0) {
27
- return {
28
- valid: false,
29
- error: 'Sequence gaps detected',
30
- gaps: sequenceGaps
31
- };
32
- }
33
-
34
- // Check 2: Stream update count matches
35
- const textUpdates = updates.filter(u => u.updateType === 'text');
36
- const htmlUpdates = updates.filter(u => u.updateType === 'html');
37
- const imageUpdates = updates.filter(u => u.updateType === 'image');
38
-
39
- // Check 3: Sequence starts at 0
40
- if (updates.length > 0 && updates[0].sequence !== 0) {
41
- return {
42
- valid: false,
43
- error: 'Sequence should start at 0',
44
- firstSequence: updates[0].sequence
45
- };
46
- }
47
-
48
- return {
49
- valid: true,
50
- sessionId,
51
- updateCount: updates.length,
52
- textCount: textUpdates.length,
53
- htmlCount: htmlUpdates.length,
54
- imageCount: imageUpdates.length,
55
- latestSequence: updates.length > 0 ? updates[updates.length - 1].sequence : -1,
56
- checkpoint: this.createChecksum(updates)
57
- };
58
- } catch (err) {
59
- return {
60
- valid: false,
61
- error: err.message
62
- };
63
- }
64
- }
65
-
66
- /**
67
- * Creates checksum of stream updates for integrity verification
68
- */
69
- static createChecksum(updates) {
70
- const data = updates
71
- .map(u => `${u.sequence}:${u.updateType}:${u.created_at}`)
72
- .join('|');
73
- return crypto.createHash('sha256').update(data).digest('hex');
74
- }
75
-
76
- /**
77
- * Verifies checksum hasn't changed (data integrity)
78
- */
79
- static verifyChecksum(updates, expectedChecksum) {
80
- return this.createChecksum(updates) === expectedChecksum;
81
- }
82
-
83
- /**
84
- * Gets current state for client recovery
85
- */
86
- static getSessionState(sessionId) {
87
- const session = queries.getSession(sessionId);
88
- if (!session) return null;
89
-
90
- const updates = queries.getSessionStreamUpdates(sessionId);
91
- const validation = this.validateSession(sessionId);
92
-
93
- return {
94
- session: {
95
- id: session.id,
96
- conversationId: session.conversationId,
97
- status: session.status,
98
- started_at: session.started_at,
99
- completed_at: session.completed_at
100
- },
101
- updates: updates.map(u => ({
102
- sequence: u.sequence,
103
- updateType: u.updateType,
104
- content: u.content,
105
- created_at: u.created_at
106
- })),
107
- validation,
108
- checkpoint: validation.checkpoint,
109
- recoveryPoint: {
110
- lastSequence: updates.length > 0 ? updates[updates.length - 1].sequence : -1,
111
- totalUpdates: updates.length,
112
- timestamp: Date.now()
113
- }
114
- };
115
- }
116
-
117
- /**
118
- * Validates incoming update against current state
119
- */
120
- static validateUpdate(sessionId, incomingUpdate, lastKnownSequence) {
121
- const updates = queries.getSessionStreamUpdates(sessionId);
122
- const maxSequence = updates.length > 0 ? updates[updates.length - 1].sequence : -1;
123
-
124
- // Check if this is the next expected sequence
125
- const expectedSequence = maxSequence + 1;
126
- if (incomingUpdate.sequence !== expectedSequence) {
127
- return {
128
- valid: false,
129
- error: 'Sequence out of order',
130
- expected: expectedSequence,
131
- received: incomingUpdate.sequence,
132
- action: 'FETCH_MISSING_UPDATES'
133
- };
134
- }
135
-
136
- // Check for duplicates (same sequence already exists)
137
- if (updates.some(u => u.sequence === incomingUpdate.sequence)) {
138
- return {
139
- valid: false,
140
- error: 'Duplicate update detected',
141
- sequence: incomingUpdate.sequence,
142
- action: 'IGNORE_DUPLICATE'
143
- };
144
- }
145
-
146
- return { valid: true, sequence: expectedSequence };
147
- }
148
- }
149
-
150
- export default StateValidator;
@@ -1,273 +0,0 @@
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;
package/stream-handler.js DELETED
@@ -1,106 +0,0 @@
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
- // CRITICAL: Broadcast happens AFTER database write confirms
66
- // This ensures clients see data that's already persisted
67
- // Broadcast immediately with zero delay
68
- this.broadcastFn({
69
- type: 'stream_update',
70
- sessionId: this.sessionId,
71
- conversationId: this.conversationId,
72
- updateType,
73
- update: persistedUpdate.content,
74
- sequence: this.sequence,
75
- persisted: true,
76
- timestamp: persistedUpdate.created_at
77
- });
78
-
79
- // Validate consistency asynchronously (don't block broadcast)
80
- setImmediate(() => {
81
- try {
82
- const validation = StateValidator.validateSession(this.sessionId);
83
- if (!validation.valid) {
84
- console.error(`[StreamHandler] State validation failed: ${validation.error}`);
85
- }
86
- } catch (validationErr) {
87
- console.error(`[StreamHandler] Validation error: ${validationErr.message}`);
88
- }
89
- });
90
- } catch (err) {
91
- console.error(`[StreamHandler] Error persisting update: ${err.message}`);
92
- // On persistence failure, do NOT broadcast - maintain consistency
93
- throw err;
94
- }
95
- }
96
-
97
- getBlocks() {
98
- return this.blocks;
99
- }
100
-
101
- getUpdateCount() {
102
- return this.updateCount;
103
- }
104
- }
105
-
106
- export default StreamHandler;