File size: 6,150 Bytes
391c43e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
/**
 * Debug Events State Management
 * Handles debug events persistence and state management with IndexedDB
 */

import { logger } from '@/lib/utils';

export interface DebugEvent {
  id: string;
  timestamp: number;
  event: string;
  data: any;
}

// Ephemeral events are only kept in memory for live viewing, not persisted to IndexedDB
// This prevents O(N²) memory growth from streaming token snapshots
const EPHEMERAL_EVENTS = new Set([
  'assistant_delta',
  'tool_param_delta',
  'reasoning_delta',
  'llm_request',
  'stream_raw_chunk'
]);

interface StoredDebugEventsState {
  id: string;
  projectId: string;
  events: DebugEvent[];
  lastUpdated: string;
}

export class DebugEventsStateManager {
  private eventsCache: Map<string, DebugEvent[]> = new Map();
  private storeName = 'debugEvents';
  private isInitialized = false;

  /**
   * Initialize by ensuring VFS database is ready
   */
  private async initDB(): Promise<void> {
    if (this.isInitialized) return;

    // Import vfs dynamically to avoid circular dependency
    const { vfs } = await import('@/lib/vfs');
    await vfs.init();
    this.isInitialized = true;
  }

  /**
   * Get shared database connection from VFS
   */
  private async getDB(): Promise<IDBDatabase> {
    const { vfs } = await import('@/lib/vfs');
    return vfs.getDatabase();
  }

  /**
   * Get debug events ID for a project
   */
  private getDebugEventsId(projectId: string): string {
    return `debug_events_${projectId}`;
  }

  /**
   * Load debug events from IndexedDB
   */
  async loadEvents(projectId: string): Promise<DebugEvent[]> {
    await this.initDB();
    const db = await this.getDB();

    const eventsId = this.getDebugEventsId(projectId);

    return new Promise((resolve, reject) => {
      const transaction = db.transaction([this.storeName], 'readonly');
      const store = transaction.objectStore(this.storeName);
      const request = store.get(eventsId);

      request.onsuccess = () => {
        const stored = request.result as StoredDebugEventsState;
        if (stored && stored.events) {
          this.eventsCache.set(projectId, stored.events);
          resolve(stored.events);
        } else {
          resolve([]);
        }
      };

      request.onerror = () => {
        logger.error('Failed to load debug events from DB');
        reject(request.error);
      };
    });
  }

  /**
   * Save debug events to IndexedDB
   */
  async saveEvents(projectId: string, events: DebugEvent[]): Promise<void> {
    await this.initDB();
    const db = await this.getDB();

    const storedState: StoredDebugEventsState = {
      id: this.getDebugEventsId(projectId),
      projectId,
      events,
      lastUpdated: new Date().toISOString()
    };

    return new Promise((resolve, reject) => {
      const transaction = db.transaction([this.storeName], 'readwrite');
      const store = transaction.objectStore(this.storeName);
      const request = store.put(storedState);

      request.onsuccess = () => {
        this.eventsCache.set(projectId, events);
        resolve();
      };

      request.onerror = () => {
        logger.error('Failed to save debug events to DB');
        reject(request.error);
      };
    });
  }

  /**
   * Append a new debug event
   * Ephemeral events (deltas) are only kept in memory, not persisted to IndexedDB
   */
  async appendEvent(projectId: string, event: DebugEvent): Promise<void> {
    let events = this.eventsCache.get(projectId);
    if (!events) {
      events = await this.loadEvents(projectId);
    }

    events.push(event);
    this.eventsCache.set(projectId, events);

    // Skip persisting ephemeral streaming events - they're only useful during live viewing
    // This prevents massive IndexedDB growth from streaming tokens
    if (EPHEMERAL_EVENTS.has(event.event)) {
      return;
    }

    // Save meaningful events to DB - await to prevent race conditions with rapid event appends
    await this.saveEvents(projectId, events);
  }

  /**
   * Clear all debug events for a project
   */
  async clearEvents(projectId: string): Promise<void> {
    this.eventsCache.delete(projectId);
    await this.saveEvents(projectId, []);

    logger.debug(`[DebugEventsState] Cleared debug events for project ${projectId}`);
  }

  /**
   * Truncate debug events to a specific set (used for retry)
   */
  async truncateEvents(projectId: string, events: DebugEvent[]): Promise<void> {
    this.eventsCache.set(projectId, events);
    await this.saveEvents(projectId, events);

    logger.debug(`[DebugEventsState] Truncated debug events for project ${projectId} to ${events.length} events`);
  }

  /**
   * Get debug events for a project (from cache or DB)
   */
  async getEvents(projectId: string): Promise<DebugEvent[]> {
    let events = this.eventsCache.get(projectId);
    if (!events) {
      events = await this.loadEvents(projectId);
    }
    return events;
  }

  /**
   * Delete all debug events for a project
   */
  async deleteProject(projectId: string): Promise<void> {
    await this.initDB();
    const db = await this.getDB();

    const eventsId = this.getDebugEventsId(projectId);

    return new Promise((resolve, reject) => {
      const transaction = db.transaction([this.storeName], 'readwrite');
      const store = transaction.objectStore(this.storeName);
      const request = store.delete(eventsId);

      request.onsuccess = () => {
        this.eventsCache.delete(projectId);
        resolve();
      };

      request.onerror = () => {
        logger.error('Failed to delete debug events from DB');
        reject(request.error);
      };
    });
  }

  /**
   * Unload debug events for a project from memory cache
   * Data remains in IndexedDB and can be reloaded on demand
   * This is called when leaving a project to free up memory
   */
  unloadProject(projectId: string): void {
    const hadCache = this.eventsCache.has(projectId);
    this.eventsCache.delete(projectId);

    if (hadCache) {
      logger.debug(`[DebugEventsState] Unloaded debug events cache for project ${projectId}`);
    }
  }
}

export const debugEventsState = new DebugEventsStateManager();