@dynamicu/chromedebug-mcp 2.2.0

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.
Files changed (95) hide show
  1. package/CLAUDE.md +344 -0
  2. package/LICENSE +21 -0
  3. package/README.md +250 -0
  4. package/chrome-extension/README.md +41 -0
  5. package/chrome-extension/background.js +3917 -0
  6. package/chrome-extension/chrome-session-manager.js +706 -0
  7. package/chrome-extension/content.css +181 -0
  8. package/chrome-extension/content.js +3022 -0
  9. package/chrome-extension/data-buffer.js +435 -0
  10. package/chrome-extension/dom-tracker.js +411 -0
  11. package/chrome-extension/extension-config.js +78 -0
  12. package/chrome-extension/firebase-client.js +278 -0
  13. package/chrome-extension/firebase-config.js +32 -0
  14. package/chrome-extension/firebase-config.module.js +22 -0
  15. package/chrome-extension/firebase-config.module.template.js +27 -0
  16. package/chrome-extension/firebase-config.template.js +36 -0
  17. package/chrome-extension/frame-capture.js +407 -0
  18. package/chrome-extension/icon128.png +1 -0
  19. package/chrome-extension/icon16.png +1 -0
  20. package/chrome-extension/icon48.png +1 -0
  21. package/chrome-extension/license-helper.js +181 -0
  22. package/chrome-extension/logger.js +23 -0
  23. package/chrome-extension/manifest.json +73 -0
  24. package/chrome-extension/network-tracker.js +510 -0
  25. package/chrome-extension/offscreen.html +10 -0
  26. package/chrome-extension/options.html +203 -0
  27. package/chrome-extension/options.js +282 -0
  28. package/chrome-extension/pako.min.js +2 -0
  29. package/chrome-extension/performance-monitor.js +533 -0
  30. package/chrome-extension/pii-redactor.js +405 -0
  31. package/chrome-extension/popup.html +532 -0
  32. package/chrome-extension/popup.js +2446 -0
  33. package/chrome-extension/upload-manager.js +323 -0
  34. package/chrome-extension/web-vitals.iife.js +1 -0
  35. package/config/api-keys.json +11 -0
  36. package/config/chrome-pilot-config.json +45 -0
  37. package/package.json +126 -0
  38. package/scripts/cleanup-processes.js +109 -0
  39. package/scripts/config-manager.js +280 -0
  40. package/scripts/generate-extension-config.js +53 -0
  41. package/scripts/setup-security.js +64 -0
  42. package/src/capture/architecture.js +426 -0
  43. package/src/capture/error-handling-tests.md +38 -0
  44. package/src/capture/error-handling-types.ts +360 -0
  45. package/src/capture/index.js +508 -0
  46. package/src/capture/interfaces.js +625 -0
  47. package/src/capture/memory-manager.js +713 -0
  48. package/src/capture/types.js +342 -0
  49. package/src/chrome-controller.js +2658 -0
  50. package/src/cli.js +19 -0
  51. package/src/config-loader.js +303 -0
  52. package/src/database.js +2178 -0
  53. package/src/firebase-license-manager.js +462 -0
  54. package/src/firebase-privacy-guard.js +397 -0
  55. package/src/http-server.js +1516 -0
  56. package/src/index-direct.js +157 -0
  57. package/src/index-modular.js +219 -0
  58. package/src/index-monolithic-backup.js +2230 -0
  59. package/src/index.js +305 -0
  60. package/src/legacy/chrome-controller-old.js +1406 -0
  61. package/src/legacy/index-express.js +625 -0
  62. package/src/legacy/index-old.js +977 -0
  63. package/src/legacy/routes.js +260 -0
  64. package/src/legacy/shared-storage.js +101 -0
  65. package/src/logger.js +10 -0
  66. package/src/mcp/handlers/chrome-tool-handler.js +306 -0
  67. package/src/mcp/handlers/element-tool-handler.js +51 -0
  68. package/src/mcp/handlers/frame-tool-handler.js +957 -0
  69. package/src/mcp/handlers/request-handler.js +104 -0
  70. package/src/mcp/handlers/workflow-tool-handler.js +636 -0
  71. package/src/mcp/server.js +68 -0
  72. package/src/mcp/tools/index.js +701 -0
  73. package/src/middleware/auth.js +371 -0
  74. package/src/middleware/security.js +267 -0
  75. package/src/port-discovery.js +258 -0
  76. package/src/routes/admin.js +182 -0
  77. package/src/services/browser-daemon.js +494 -0
  78. package/src/services/chrome-service.js +375 -0
  79. package/src/services/failover-manager.js +412 -0
  80. package/src/services/git-safety-service.js +675 -0
  81. package/src/services/heartbeat-manager.js +200 -0
  82. package/src/services/http-client.js +195 -0
  83. package/src/services/process-manager.js +318 -0
  84. package/src/services/process-tracker.js +574 -0
  85. package/src/services/profile-manager.js +449 -0
  86. package/src/services/project-manager.js +415 -0
  87. package/src/services/session-manager.js +497 -0
  88. package/src/services/session-registry.js +491 -0
  89. package/src/services/unified-session-manager.js +678 -0
  90. package/src/shared-storage-old.js +267 -0
  91. package/src/standalone-server.js +53 -0
  92. package/src/utils/extension-path.js +145 -0
  93. package/src/utils.js +187 -0
  94. package/src/validation/log-transformer.js +125 -0
  95. package/src/validation/schemas.js +391 -0
@@ -0,0 +1,706 @@
1
+ /**
2
+ * Chrome Extension Session Manager
3
+ *
4
+ * Manages recording sessions with lease-based state management to prevent
5
+ * stuck recordings and ensure reliable session lifecycle.
6
+ */
7
+
8
+ class ChromeExtensionSessionManager {
9
+ constructor() {
10
+ this.STORAGE_KEY = 'chrome_pilot_recording_state';
11
+ this.LEASE_DURATION = 5000; // 5 seconds
12
+ this.MAX_DURATION = 30 * 60 * 1000; // 30 minutes
13
+
14
+ // Recording-only timer properties
15
+ this.activeRecordingTimer = null; // Only active during recording
16
+ this.ACTIVE_LEASE_RENEWAL = 30000; // 30 seconds, only while recording
17
+
18
+ // In-memory state cache for synchronous access (v2.1.2: Race condition fix)
19
+ this._recordingStateCache = null;
20
+ }
21
+
22
+ /**
23
+ * Create a new recording session
24
+ * @param {Object} config - Session configuration
25
+ * @returns {Promise<Object>} Session creation result
26
+ */
27
+ async createSession(config = {}) {
28
+ const sessionConfig = this._mergeDefaultConfig(config);
29
+ const sessionId = this._generateSessionId();
30
+ const ownerId = this._generateOwnerId();
31
+ const currentTime = Date.now();
32
+
33
+ const recordingState = {
34
+ sessionId,
35
+ isRecording: false,
36
+ startTime: currentTime,
37
+ lastLeaseRenewal: currentTime,
38
+ leaseExpiresAt: currentTime + this.LEASE_DURATION,
39
+ maxDuration: sessionConfig.maxDuration,
40
+ frameCount: 0,
41
+ settings: sessionConfig,
42
+ status: 'active',
43
+ ownerId,
44
+ debugInfo: [{
45
+ event: 'session_created',
46
+ timestamp: currentTime,
47
+ data: { sessionId, ownerId }
48
+ }],
49
+ lastActivity: currentTime,
50
+ warningFlags: []
51
+ };
52
+
53
+ try {
54
+ await this._saveRecordingState(recordingState);
55
+ this._logDebug('Session created', { sessionId, ownerId });
56
+
57
+ return {
58
+ success: true,
59
+ sessionId,
60
+ ownerId,
61
+ state: recordingState
62
+ };
63
+ } catch (error) {
64
+ return this._createErrorResponse('STORAGE_ERROR', 'Failed to create session', error);
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Start recording for a session
70
+ * @param {string} sessionId - Session identifier
71
+ * @param {string} ownerId - Owner identifier
72
+ * @returns {Promise<Object>} Start result
73
+ */
74
+ async startRecording(sessionId, ownerId) {
75
+ try {
76
+ const state = await this._getRecordingState();
77
+
78
+ if (!state || state.sessionId !== sessionId) {
79
+ return this._createErrorResponse('INVALID_STATE', 'Session not found');
80
+ }
81
+
82
+ if (state.ownerId !== ownerId) {
83
+ return this._createErrorResponse('INVALID_STATE', 'Unauthorized access to session');
84
+ }
85
+
86
+ if (state.isRecording) {
87
+ return this._createErrorResponse('INVALID_STATE', 'Recording already in progress');
88
+ }
89
+
90
+ const currentTime = Date.now();
91
+ const updatedState = {
92
+ ...state,
93
+ isRecording: true,
94
+ startTime: currentTime,
95
+ lastLeaseRenewal: currentTime,
96
+ leaseExpiresAt: currentTime + this.LEASE_DURATION,
97
+ status: 'active',
98
+ lastActivity: currentTime,
99
+ debugInfo: [...state.debugInfo, {
100
+ event: 'recording_started',
101
+ timestamp: currentTime,
102
+ data: { sessionId }
103
+ }]
104
+ };
105
+
106
+ await this._saveRecordingState(updatedState);
107
+
108
+ // START polling ONLY for active recording
109
+ if (this.activeRecordingTimer) {
110
+ clearInterval(this.activeRecordingTimer);
111
+ }
112
+
113
+ this.activeRecordingTimer = setInterval(async () => {
114
+ // Renew lease for active recording
115
+ await this.renewLease({
116
+ sessionId,
117
+ ownerId,
118
+ requestTime: Date.now()
119
+ });
120
+ // Clean up any other expired sessions
121
+ await this.cleanupExpiredSessions();
122
+ }, this.ACTIVE_LEASE_RENEWAL);
123
+
124
+ this._logDebug('Recording started with timer', { sessionId });
125
+
126
+ return {
127
+ success: true,
128
+ sessionId,
129
+ state: updatedState
130
+ };
131
+ } catch (error) {
132
+ return this._createErrorResponse('STORAGE_ERROR', 'Failed to start recording', error);
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Renew lease for active session
138
+ * @param {Object} leaseRequest - Lease renewal request
139
+ * @returns {Promise<Object>} Lease response
140
+ */
141
+ async renewLease(leaseRequest) {
142
+ const { sessionId, ownerId, requestTime, renewalDuration = this.LEASE_DURATION } = leaseRequest;
143
+
144
+ try {
145
+ const state = await this._getRecordingState();
146
+
147
+ if (!state || state.sessionId !== sessionId) {
148
+ return {
149
+ success: false,
150
+ sessionId,
151
+ error: 'Session not found'
152
+ };
153
+ }
154
+
155
+ if (state.ownerId !== ownerId) {
156
+ return {
157
+ success: false,
158
+ sessionId,
159
+ error: 'Unauthorized lease renewal'
160
+ };
161
+ }
162
+
163
+ const currentTime = Date.now();
164
+ const sessionDuration = currentTime - state.startTime;
165
+
166
+ // Check if max duration exceeded
167
+ if (sessionDuration > state.maxDuration) {
168
+ await this._forceStopSession(sessionId, 'MAX_DURATION');
169
+ return {
170
+ success: false,
171
+ sessionId,
172
+ error: 'Maximum duration exceeded',
173
+ maxDurationExceeded: true
174
+ };
175
+ }
176
+
177
+ const newExpiration = currentTime + renewalDuration;
178
+ const updatedState = {
179
+ ...state,
180
+ lastLeaseRenewal: currentTime,
181
+ leaseExpiresAt: newExpiration,
182
+ lastActivity: currentTime,
183
+ debugInfo: [...state.debugInfo.slice(-20), { // Keep last 20 debug entries
184
+ event: 'lease_renewed',
185
+ timestamp: currentTime,
186
+ data: { newExpiration, sessionDuration }
187
+ }]
188
+ };
189
+
190
+ // v2.1.2: UPDATE STATE SYNCHRONOUSLY to fix race condition
191
+ // Cache the state immediately BEFORE async storage save
192
+ // This ensures isSessionValid() sees the updated expiration immediately
193
+ this._recordingStateCache = updatedState;
194
+
195
+ // Then persist to storage (async is fine, cache is already updated)
196
+ await this._saveRecordingState(updatedState);
197
+
198
+ return {
199
+ success: true,
200
+ sessionId,
201
+ newExpiration,
202
+ maxDurationExceeded: false
203
+ };
204
+ } catch (error) {
205
+ return {
206
+ success: false,
207
+ sessionId,
208
+ error: 'Failed to renew lease: ' + error.message
209
+ };
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Stop recording session
215
+ * @param {string} sessionId - Session identifier
216
+ * @param {string} ownerId - Owner identifier
217
+ * @returns {Promise<Object>} Stop result
218
+ */
219
+ async stopRecording(sessionId, ownerId) {
220
+ try {
221
+ const state = await this._getRecordingState();
222
+
223
+ if (!state || state.sessionId !== sessionId) {
224
+ return this._createErrorResponse('INVALID_STATE', 'Session not found');
225
+ }
226
+
227
+ if (state.ownerId !== ownerId) {
228
+ return this._createErrorResponse('INVALID_STATE', 'Unauthorized access to session');
229
+ }
230
+
231
+ const currentTime = Date.now();
232
+ const sessionDuration = currentTime - state.startTime;
233
+ const updatedState = {
234
+ ...state,
235
+ isRecording: false,
236
+ status: 'stopped',
237
+ lastActivity: currentTime,
238
+ debugInfo: [...state.debugInfo, {
239
+ event: 'recording_stopped',
240
+ timestamp: currentTime,
241
+ data: { sessionId, sessionDuration, frameCount: state.frameCount }
242
+ }]
243
+ };
244
+
245
+ await this._saveRecordingState(updatedState);
246
+
247
+ // STOP the polling immediately
248
+ if (this.activeRecordingTimer) {
249
+ clearInterval(this.activeRecordingTimer);
250
+ this.activeRecordingTimer = null;
251
+ }
252
+
253
+ this._logDebug('Recording stopped with timer cleared', { sessionId, sessionDuration });
254
+
255
+ return {
256
+ success: true,
257
+ sessionId,
258
+ sessionDuration,
259
+ frameCount: state.frameCount,
260
+ state: updatedState
261
+ };
262
+ } catch (error) {
263
+ return this._createErrorResponse('STORAGE_ERROR', 'Failed to stop recording', error);
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Force stop any active session (emergency cleanup)
269
+ * @param {string} reason - Reason for force stop
270
+ * @returns {Promise<Object>} Force stop result
271
+ */
272
+ async forceStopAll(reason = 'MANUAL_FORCE_STOP') {
273
+ try {
274
+ const state = await this._getRecordingState();
275
+
276
+ if (!state) {
277
+ return { success: true, message: 'No active session to stop' };
278
+ }
279
+
280
+ const result = await this._forceStopSession(state.sessionId, reason);
281
+ this._logDebug('Force stopped all sessions', { reason });
282
+
283
+ return result;
284
+ } catch (error) {
285
+ return this._createErrorResponse('STORAGE_ERROR', 'Failed to force stop', error);
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Get current session state
291
+ * @returns {Promise<Object|null>} Current recording state
292
+ */
293
+ async getCurrentState() {
294
+ try {
295
+ return await this._getRecordingState();
296
+ } catch (error) {
297
+ this._logError('Failed to get current state', error);
298
+ return null;
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Update frame count for session
304
+ * @param {string} sessionId - Session identifier
305
+ * @param {number} frameCount - New frame count
306
+ * @returns {Promise<boolean>} Update success
307
+ */
308
+ async updateFrameCount(sessionId, frameCount) {
309
+ try {
310
+ const state = await this._getRecordingState();
311
+
312
+ if (!state || state.sessionId !== sessionId) {
313
+ return false;
314
+ }
315
+
316
+ const updatedState = {
317
+ ...state,
318
+ frameCount,
319
+ lastActivity: Date.now()
320
+ };
321
+
322
+ await this._saveRecordingState(updatedState);
323
+ return true;
324
+ } catch (error) {
325
+ this._logError('Failed to update frame count', error);
326
+ return false;
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Check if session is valid and active
332
+ * @param {string} sessionId - Session identifier
333
+ * @returns {Promise<Object>} Validity check result
334
+ */
335
+ async isSessionValid(sessionId) {
336
+ try {
337
+ const state = await this._getRecordingState();
338
+
339
+ if (!state || state.sessionId !== sessionId) {
340
+ return { valid: false, reason: 'Session not found' };
341
+ }
342
+
343
+ const currentTime = Date.now();
344
+ const isExpired = currentTime > state.leaseExpiresAt;
345
+ const isOverDuration = (currentTime - state.startTime) > state.maxDuration;
346
+
347
+ if (isExpired) {
348
+ await this._forceStopSession(sessionId, 'LEASE_EXPIRED');
349
+ return { valid: false, reason: 'Lease expired' };
350
+ }
351
+
352
+ if (isOverDuration) {
353
+ await this._forceStopSession(sessionId, 'MAX_DURATION');
354
+ return { valid: false, reason: 'Maximum duration exceeded' };
355
+ }
356
+
357
+ return {
358
+ valid: true,
359
+ state,
360
+ timeRemaining: state.maxDuration - (currentTime - state.startTime),
361
+ leaseTimeRemaining: state.leaseExpiresAt - currentTime
362
+ };
363
+ } catch (error) {
364
+ this._logError('Failed to validate session', error);
365
+ return { valid: false, reason: 'Validation error' };
366
+ }
367
+ }
368
+
369
+ /**
370
+ * Recover stuck sessions from previous runs
371
+ * Called during extension initialization and Chrome startup
372
+ * @returns {Promise<Object>} Recovery result
373
+ */
374
+ async recoverSessions() {
375
+ try {
376
+ const state = await this._getRecordingState();
377
+
378
+ if (!state) {
379
+ this._logDebug('No sessions to recover');
380
+ return { recovered: 0, message: 'No sessions to recover' };
381
+ }
382
+
383
+ const currentTime = Date.now();
384
+ const isExpired = currentTime > state.leaseExpiresAt;
385
+ const isOverDuration = (currentTime - state.startTime) > state.maxDuration;
386
+ // A session is only "stuck" if it's active, not recording, AND hasn't had activity for a reasonable time
387
+ // Allow active sessions to be idle for up to 30 minutes without being considered stuck
388
+ const isStuck = !state.isRecording && state.status === 'active' &&
389
+ (currentTime - state.lastActivity) > (30 * 60 * 1000); // 30 minutes
390
+
391
+ // More conservative timeout: only clean up sessions that are truly abandoned
392
+ // - 24 hours of total inactivity for stopped sessions
393
+ // - 4 hours of inactivity for active sessions that aren't recording
394
+ const inactivityTime = currentTime - state.lastActivity;
395
+ const isTrulyAbandoned = state.status === 'stopped'
396
+ ? inactivityTime > (24 * 60 * 60 * 1000) // 24 hours for stopped sessions
397
+ : inactivityTime > (4 * 60 * 60 * 1000); // 4 hours for active sessions
398
+
399
+ if (isExpired || isOverDuration || isStuck || isTrulyAbandoned) {
400
+ let reason = 'RECOVERY_CLEANUP';
401
+ if (isExpired) reason = 'LEASE_EXPIRED';
402
+ else if (isOverDuration) reason = 'MAX_DURATION_EXCEEDED';
403
+ else if (isStuck) reason = 'STUCK_SESSION';
404
+ else if (isTrulyAbandoned) reason = 'ABANDONED_SESSION';
405
+
406
+ await this._forceStopSession(state.sessionId, reason);
407
+
408
+ this._logDebug('Recovered stuck session', {
409
+ sessionId: state.sessionId,
410
+ reason,
411
+ age: currentTime - state.startTime,
412
+ lastActivity: currentTime - state.lastActivity
413
+ });
414
+
415
+ return {
416
+ recovered: 1,
417
+ message: `Recovered stuck session: ${state.sessionId}`,
418
+ reason,
419
+ sessionId: state.sessionId
420
+ };
421
+ }
422
+
423
+ this._logDebug('Session is healthy, no recovery needed', {
424
+ sessionId: state.sessionId,
425
+ status: state.status,
426
+ isRecording: state.isRecording
427
+ });
428
+
429
+ return { recovered: 0, message: 'Session is healthy', sessionId: state.sessionId };
430
+ } catch (error) {
431
+ this._logError('Session recovery failed', error);
432
+ return {
433
+ recovered: 0,
434
+ error: error.message,
435
+ message: 'Recovery failed due to error'
436
+ };
437
+ }
438
+ }
439
+
440
+ // Private Methods
441
+
442
+ /**
443
+ * Clean up expired sessions (only called during recording)
444
+ */
445
+ async cleanupExpiredSessions() {
446
+ try {
447
+ const state = await this._getRecordingState();
448
+ if (!state) return;
449
+
450
+ // Skip if already stopped (prevents repeated messages)
451
+ if (state.status === 'stopped') {
452
+ // Optionally remove very old stopped sessions (>1 hour)
453
+ const hourAgo = Date.now() - 3600000;
454
+ if (state.lastActivity < hourAgo) {
455
+ await this._removeFromStorage();
456
+ }
457
+ return;
458
+ }
459
+
460
+ // Only process active expired sessions
461
+ const currentTime = Date.now();
462
+ if (currentTime > state.leaseExpiresAt) {
463
+ console.log('[ChromeSessionManager] Cleaning up expired session', {
464
+ sessionId: state.sessionId
465
+ });
466
+ await this._forceStopSession(state.sessionId, 'LEASE_EXPIRED');
467
+ }
468
+ } catch (error) {
469
+ this._logError('Cleanup failed', error);
470
+ }
471
+ }
472
+
473
+ /**
474
+ * Perform cleanup of expired sessions (legacy method, now calls cleanupExpiredSessions)
475
+ */
476
+ async _performCleanup() {
477
+ await this.cleanupExpiredSessions();
478
+ }
479
+
480
+ /**
481
+ * Force stop a session
482
+ */
483
+ async _forceStopSession(sessionId, reason) {
484
+ const currentTime = Date.now();
485
+
486
+ try {
487
+ const state = await this._getRecordingState();
488
+
489
+ if (state && state.sessionId === sessionId) {
490
+ const updatedState = {
491
+ ...state,
492
+ isRecording: false,
493
+ status: 'stopped',
494
+ lastActivity: currentTime,
495
+ debugInfo: [...state.debugInfo, {
496
+ event: 'force_stopped',
497
+ timestamp: currentTime,
498
+ data: { reason, sessionId }
499
+ }]
500
+ };
501
+
502
+ await this._saveRecordingState(updatedState);
503
+ }
504
+
505
+ return {
506
+ success: true,
507
+ sessionId,
508
+ reason,
509
+ stoppedAt: currentTime
510
+ };
511
+ } catch (error) {
512
+ return this._createErrorResponse('STORAGE_ERROR', 'Failed to force stop session', error);
513
+ }
514
+ }
515
+
516
+ /**
517
+ * Save recording state to Chrome storage
518
+ */
519
+ async _saveRecordingState(state) {
520
+ // v2.1.2: Update cache synchronously BEFORE async storage save
521
+ this._recordingStateCache = state;
522
+
523
+ return new Promise((resolve, reject) => {
524
+ chrome.storage.local.set({ [this.STORAGE_KEY]: state }, () => {
525
+ if (chrome.runtime.lastError) {
526
+ reject(new Error(chrome.runtime.lastError.message));
527
+ } else {
528
+ resolve();
529
+ }
530
+ });
531
+ });
532
+ }
533
+
534
+ /**
535
+ * Get recording state from Chrome storage
536
+ */
537
+ async _getRecordingState() {
538
+ // v2.1.2: Use cache first for synchronous lease checks (race condition fix)
539
+ if (this._recordingStateCache) {
540
+ return this._recordingStateCache;
541
+ }
542
+
543
+ // Otherwise fetch from storage
544
+ return new Promise((resolve, reject) => {
545
+ chrome.storage.local.get([this.STORAGE_KEY], (result) => {
546
+ if (chrome.runtime.lastError) {
547
+ reject(new Error(chrome.runtime.lastError.message));
548
+ } else {
549
+ const state = result[this.STORAGE_KEY] || null;
550
+ // Cache the state for future synchronous access
551
+ this._recordingStateCache = state;
552
+ resolve(state);
553
+ }
554
+ });
555
+ });
556
+ }
557
+
558
+ /**
559
+ * Remove recording state from Chrome storage
560
+ */
561
+ async _removeFromStorage() {
562
+ // v2.1.2: Clear cache synchronously
563
+ this._recordingStateCache = null;
564
+
565
+ return new Promise((resolve, reject) => {
566
+ chrome.storage.local.remove([this.STORAGE_KEY], () => {
567
+ if (chrome.runtime.lastError) {
568
+ reject(new Error(chrome.runtime.lastError.message));
569
+ } else {
570
+ resolve();
571
+ }
572
+ });
573
+ });
574
+ }
575
+
576
+ /**
577
+ * Merge default configuration
578
+ */
579
+ _mergeDefaultConfig(config) {
580
+ return {
581
+ maxDuration: this.MAX_DURATION,
582
+ leaseInterval: this.LEASE_DURATION,
583
+ activeLeaseRenewal: this.ACTIVE_LEASE_RENEWAL,
584
+ frameQuality: 0.3,
585
+ includeConsole: true,
586
+ captureAudio: false,
587
+ ...config
588
+ };
589
+ }
590
+
591
+ /**
592
+ * Generate unique session ID
593
+ */
594
+ _generateSessionId() {
595
+ return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
596
+ }
597
+
598
+ /**
599
+ * Generate unique owner ID
600
+ */
601
+ _generateOwnerId() {
602
+ return `owner_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
603
+ }
604
+
605
+ /**
606
+ * Create standardized error response
607
+ */
608
+ _createErrorResponse(type, message, originalError = null) {
609
+ return {
610
+ success: false,
611
+ error: {
612
+ type,
613
+ message,
614
+ timestamp: Date.now(),
615
+ recoverable: type !== 'STORAGE_ERROR',
616
+ originalError: originalError?.message
617
+ }
618
+ };
619
+ }
620
+
621
+ /**
622
+ * Debug logging
623
+ */
624
+ _logDebug(message, data = {}) {
625
+ console.log(`[ChromeSessionManager] ${message}`, data);
626
+ }
627
+
628
+ /**
629
+ * Error logging
630
+ */
631
+ _logError(message, error) {
632
+ console.error(`[ChromeSessionManager] ${message}`, error);
633
+ }
634
+
635
+ /**
636
+ * Check if session manager is initialized and healthy
637
+ * @returns {Promise<boolean>} True if initialized and ready
638
+ */
639
+ async isInitialized() {
640
+ try {
641
+ // Check if we can access storage
642
+ await this._getRecordingState();
643
+ return true;
644
+ } catch (error) {
645
+ this._logError('isInitialized check failed', error);
646
+ return false;
647
+ }
648
+ }
649
+
650
+ /**
651
+ * Check storage pressure and clean up if needed
652
+ */
653
+ async checkStoragePressure() {
654
+ const usage = await this.getStorageUsage();
655
+ if (usage > 0.7) { // 70% full
656
+ console.warn('[ChromeSessionManager] Storage pressure detected, cleaning up old sessions');
657
+ await this.aggressiveCleanup();
658
+ }
659
+ }
660
+
661
+ /**
662
+ * Get storage usage as a ratio (0-1)
663
+ */
664
+ async getStorageUsage() {
665
+ return new Promise(resolve => {
666
+ chrome.storage.local.getBytesInUse(bytes => {
667
+ const quota = chrome.storage.local.QUOTA_BYTES || 5242880; // 5MB default
668
+ resolve(bytes / quota);
669
+ });
670
+ });
671
+ }
672
+
673
+ /**
674
+ * Aggressive cleanup of old sessions when storage pressure is detected
675
+ */
676
+ async aggressiveCleanup() {
677
+ try {
678
+ const state = await this._getRecordingState();
679
+ if (state && state.status === 'stopped') {
680
+ // Remove stopped sessions older than 1 hour during storage pressure
681
+ const hourAgo = Date.now() - 3600000;
682
+ if (state.lastActivity < hourAgo) {
683
+ await this._removeFromStorage();
684
+ console.log('[ChromeSessionManager] Removed old stopped session due to storage pressure');
685
+ }
686
+ }
687
+ } catch (error) {
688
+ this._logError('Aggressive cleanup failed', error);
689
+ }
690
+ }
691
+
692
+ /**
693
+ * Cleanup resources
694
+ */
695
+ destroy() {
696
+ if (this.activeRecordingTimer) {
697
+ clearInterval(this.activeRecordingTimer);
698
+ this.activeRecordingTimer = null;
699
+ }
700
+ }
701
+ }
702
+
703
+ // Export for use in other parts of the extension
704
+ if (typeof module !== 'undefined' && module.exports) {
705
+ module.exports = ChromeExtensionSessionManager;
706
+ }