camofox-browser 2.1.1 → 2.4.3

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 (102) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/README.md +310 -34
  3. package/dist/src/cli/commands/content.d.ts.map +1 -1
  4. package/dist/src/cli/commands/content.js +37 -0
  5. package/dist/src/cli/commands/content.js.map +1 -1
  6. package/dist/src/cli/commands/core.d.ts.map +1 -1
  7. package/dist/src/cli/commands/core.js +21 -4
  8. package/dist/src/cli/commands/core.js.map +1 -1
  9. package/dist/src/cli/commands/interaction.d.ts.map +1 -1
  10. package/dist/src/cli/commands/interaction.js +5 -14
  11. package/dist/src/cli/commands/interaction.js.map +1 -1
  12. package/dist/src/cli/commands/navigation.d.ts.map +1 -1
  13. package/dist/src/cli/commands/navigation.js +12 -6
  14. package/dist/src/cli/commands/navigation.js.map +1 -1
  15. package/dist/src/cli/commands/server.d.ts.map +1 -1
  16. package/dist/src/cli/commands/server.js +9 -3
  17. package/dist/src/cli/commands/server.js.map +1 -1
  18. package/dist/src/cli/commands/session.d.ts.map +1 -1
  19. package/dist/src/cli/commands/session.js +23 -5
  20. package/dist/src/cli/commands/session.js.map +1 -1
  21. package/dist/src/cli/server/manager.d.ts +1 -0
  22. package/dist/src/cli/server/manager.d.ts.map +1 -1
  23. package/dist/src/cli/server/manager.js +7 -12
  24. package/dist/src/cli/server/manager.js.map +1 -1
  25. package/dist/src/middleware/lifecycle-activity.d.ts +9 -0
  26. package/dist/src/middleware/lifecycle-activity.d.ts.map +1 -0
  27. package/dist/src/middleware/lifecycle-activity.js +21 -0
  28. package/dist/src/middleware/lifecycle-activity.js.map +1 -0
  29. package/dist/src/openapi/spec.d.ts +4 -0
  30. package/dist/src/openapi/spec.d.ts.map +1 -0
  31. package/dist/src/openapi/spec.js +730 -0
  32. package/dist/src/openapi/spec.js.map +1 -0
  33. package/dist/src/routes/core.d.ts.map +1 -1
  34. package/dist/src/routes/core.js +545 -58
  35. package/dist/src/routes/core.js.map +1 -1
  36. package/dist/src/routes/docs.d.ts +3 -0
  37. package/dist/src/routes/docs.d.ts.map +1 -0
  38. package/dist/src/routes/docs.js +23 -0
  39. package/dist/src/routes/docs.js.map +1 -0
  40. package/dist/src/routes/openclaw.d.ts.map +1 -1
  41. package/dist/src/routes/openclaw.js +317 -90
  42. package/dist/src/routes/openclaw.js.map +1 -1
  43. package/dist/src/server.js +55 -4
  44. package/dist/src/server.js.map +1 -1
  45. package/dist/src/services/context-pool.d.ts +21 -4
  46. package/dist/src/services/context-pool.d.ts.map +1 -1
  47. package/dist/src/services/context-pool.js +290 -71
  48. package/dist/src/services/context-pool.js.map +1 -1
  49. package/dist/src/services/download.d.ts +2 -0
  50. package/dist/src/services/download.d.ts.map +1 -1
  51. package/dist/src/services/download.js +110 -80
  52. package/dist/src/services/download.js.map +1 -1
  53. package/dist/src/services/lifecycle-controller.d.ts +40 -0
  54. package/dist/src/services/lifecycle-controller.d.ts.map +1 -0
  55. package/dist/src/services/lifecycle-controller.js +106 -0
  56. package/dist/src/services/lifecycle-controller.js.map +1 -0
  57. package/dist/src/services/resource-extractor.d.ts +1 -0
  58. package/dist/src/services/resource-extractor.d.ts.map +1 -1
  59. package/dist/src/services/resource-extractor.js +7 -0
  60. package/dist/src/services/resource-extractor.js.map +1 -1
  61. package/dist/src/services/session.d.ts +109 -4
  62. package/dist/src/services/session.d.ts.map +1 -1
  63. package/dist/src/services/session.js +622 -64
  64. package/dist/src/services/session.js.map +1 -1
  65. package/dist/src/services/structured-extractor.d.ts +39 -0
  66. package/dist/src/services/structured-extractor.d.ts.map +1 -0
  67. package/dist/src/services/structured-extractor.js +487 -0
  68. package/dist/src/services/structured-extractor.js.map +1 -0
  69. package/dist/src/services/tab.d.ts +30 -3
  70. package/dist/src/services/tab.d.ts.map +1 -1
  71. package/dist/src/services/tab.js +872 -124
  72. package/dist/src/services/tab.js.map +1 -1
  73. package/dist/src/services/tracing.d.ts +7 -0
  74. package/dist/src/services/tracing.d.ts.map +1 -1
  75. package/dist/src/services/tracing.js +200 -19
  76. package/dist/src/services/tracing.js.map +1 -1
  77. package/dist/src/services/vnc.d.ts.map +1 -1
  78. package/dist/src/services/vnc.js +5 -3
  79. package/dist/src/services/vnc.js.map +1 -1
  80. package/dist/src/services/youtube.js +1 -1
  81. package/dist/src/services/youtube.js.map +1 -1
  82. package/dist/src/types.d.ts +71 -1
  83. package/dist/src/types.d.ts.map +1 -1
  84. package/dist/src/utils/config.d.ts +79 -3
  85. package/dist/src/utils/config.d.ts.map +1 -1
  86. package/dist/src/utils/config.js +145 -3
  87. package/dist/src/utils/config.js.map +1 -1
  88. package/dist/src/utils/presets.d.ts.map +1 -1
  89. package/dist/src/utils/presets.js +3 -1
  90. package/dist/src/utils/presets.js.map +1 -1
  91. package/dist/src/utils/proxy-profiles.d.ts +18 -0
  92. package/dist/src/utils/proxy-profiles.d.ts.map +1 -0
  93. package/dist/src/utils/proxy-profiles.js +197 -0
  94. package/dist/src/utils/proxy-profiles.js.map +1 -0
  95. package/dist/src/utils/sidecar-version.d.ts +12 -0
  96. package/dist/src/utils/sidecar-version.d.ts.map +1 -0
  97. package/dist/src/utils/sidecar-version.js +63 -0
  98. package/dist/src/utils/sidecar-version.js.map +1 -0
  99. package/dist/tsconfig.tsbuildinfo +1 -1
  100. package/openclaw.plugin.json +39 -0
  101. package/package.json +16 -4
  102. package/plugin.ts +949 -0
@@ -1,16 +1,43 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.MAX_TABS_PER_SESSION = exports.MAX_SESSIONS = exports.SESSION_TIMEOUT_MS = void 0;
4
7
  exports.__getUserConcurrencyStateForTests = __getUserConcurrencyStateForTests;
8
+ exports.__getSessionsMapForTests = __getSessionsMapForTests;
5
9
  exports.withUserLimit = withUserLimit;
10
+ exports.cleanupSessionsForUserId = cleanupSessionsForUserId;
6
11
  exports.normalizeUserId = normalizeUserId;
7
12
  exports.getSessionMapKey = getSessionMapKey;
13
+ exports.getEstablishedSessionProfile = getEstablishedSessionProfile;
14
+ exports.hasDefaultSessionProfileRuntime = hasDefaultSessionProfileRuntime;
15
+ exports.claimDefaultSessionProfileRuntime = claimDefaultSessionProfileRuntime;
16
+ exports.clearDefaultSessionProfileClaim = clearDefaultSessionProfileClaim;
17
+ exports.getSessionProfileLaunchSettings = getSessionProfileLaunchSettings;
18
+ exports.getCanonicalProfile = getCanonicalProfile;
19
+ exports.hasCanonicalProfile = hasCanonicalProfile;
20
+ exports.acquireFirstCreateMutex = acquireFirstCreateMutex;
21
+ exports.commitCanonicalProfile = commitCanonicalProfile;
22
+ exports.rollbackCanonicalMutex = rollbackCanonicalMutex;
23
+ exports.createCanonicalProfile = createCanonicalProfile;
24
+ exports.clearCanonicalProfile = clearCanonicalProfile;
25
+ exports.establishSessionProfile = establishSessionProfile;
26
+ exports.clearSessionProfile = clearSessionProfile;
27
+ exports.acquireSessionProfileCreateMutex = acquireSessionProfileCreateMutex;
28
+ exports.waitForSessionProfileCreate = waitForSessionProfileCreate;
29
+ exports.rollbackSessionProfileRuntime = rollbackSessionProfileRuntime;
8
30
  exports.getSessionsForUser = getSessionsForUser;
9
31
  exports.getAllSessions = getAllSessions;
10
32
  exports.countTotalTabsForSessions = countTotalTabsForSessions;
33
+ exports.getLifecycleSessionSnapshot = getLifecycleSessionSnapshot;
34
+ exports.getSessionsSnapshot = getSessionsSnapshot;
11
35
  exports.getTabGroup = getTabGroup;
12
36
  exports.unindexSessionTabs = unindexSessionTabs;
13
37
  exports.findTabById = findTabById;
38
+ exports.createStagedSession = createStagedSession;
39
+ exports.commitStagedFirstUse = commitStagedFirstUse;
40
+ exports.rollbackStagedFirstUse = rollbackStagedFirstUse;
14
41
  exports.getSession = getSession;
15
42
  exports.indexTab = indexTab;
16
43
  exports.unindexTab = unindexTab;
@@ -19,9 +46,12 @@ exports.closeSessionsForUser = closeSessionsForUser;
19
46
  exports.closeAllSessions = closeAllSessions;
20
47
  exports.startCleanupInterval = startCleanupInterval;
21
48
  exports.stopCleanupInterval = stopCleanupInterval;
49
+ exports.runLifecycleIdleCleanup = runLifecycleIdleCleanup;
50
+ const node_crypto_1 = __importDefault(require("node:crypto"));
22
51
  const logging_1 = require("../middleware/logging");
23
52
  const tab_1 = require("./tab");
24
53
  const config_1 = require("../utils/config");
54
+ const presets_1 = require("../utils/presets");
25
55
  const context_pool_1 = require("./context-pool");
26
56
  const download_1 = require("./download");
27
57
  const health_1 = require("./health");
@@ -31,12 +61,25 @@ const CONFIG = (0, config_1.loadConfig)();
31
61
  // userId -> { context, tabGroups: Map<sessionKey, Map<tabId, TabState>>, lastAccess }
32
62
  // Note: sessionKey was previously called listItemId - both are accepted for backward compatibility
33
63
  const sessions = new Map();
64
+ const sessionOwners = new Map();
34
65
  // sessionKey -> in-flight session creation promise
35
66
  // Avoids storing partially-initialized sessions (e.g., context: null cast) and dedupes concurrent creates.
36
67
  const launchingSessions = new Map();
68
+ const launchingSessionOwners = new Map();
69
+ const lifecycleIdleClosures = new Map();
37
70
  // tabId -> sessions map key
38
71
  // Persistent profiles are keyed only by userId, while tab endpoints only get tabId.
39
72
  const tabSessionIndex = new Map();
73
+ // Canonical per-user profile: stores resolved overrides from the first core POST /tabs.
74
+ // Survives passive context eviction; cleared only on explicit session close/cleanup.
75
+ const canonicalProfiles = new Map();
76
+ // Session profiles keyed by userId::sessionKey to track separate proxy/geo profiles per session
77
+ const sessionProfiles = new Map();
78
+ const defaultSessionProfileClaims = new Map();
79
+ // Per-user mutex covering the entire first-create lifecycle (establishment -> tab commit).
80
+ // Prevents sibling requests from observing provisional canonical state.
81
+ const firstCreateMutexes = new Map();
82
+ const sessionProfileCreateMutexes = new Map();
40
83
  const userConcurrency = new Map();
41
84
  function __getUserConcurrencyStateForTests(userId) {
42
85
  const key = String(userId).toLowerCase().trim();
@@ -45,6 +88,43 @@ function __getUserConcurrencyStateForTests(userId) {
45
88
  return null;
46
89
  return { active: state.active, queueLength: state.queue.length };
47
90
  }
91
+ function __getSessionsMapForTests() {
92
+ return sessions;
93
+ }
94
+ function beginLifecycleIdleClosure(userId) {
95
+ const key = normalizeUserId(userId);
96
+ let state = lifecycleIdleClosures.get(key);
97
+ if (!state) {
98
+ let resolve;
99
+ const promise = new Promise((r) => {
100
+ resolve = r;
101
+ });
102
+ state = { pending: 0, promise, resolve };
103
+ lifecycleIdleClosures.set(key, state);
104
+ }
105
+ state.pending++;
106
+ let released = false;
107
+ return () => {
108
+ if (released)
109
+ return;
110
+ released = true;
111
+ const current = lifecycleIdleClosures.get(key);
112
+ if (!current)
113
+ return;
114
+ current.pending--;
115
+ if (current.pending <= 0) {
116
+ lifecycleIdleClosures.delete(key);
117
+ current.resolve();
118
+ }
119
+ };
120
+ }
121
+ async function waitForLifecycleIdleClosure(userId) {
122
+ const key = normalizeUserId(userId);
123
+ const pending = lifecycleIdleClosures.get(key);
124
+ if (pending) {
125
+ await pending.promise;
126
+ }
127
+ }
48
128
  async function withUserLimit(userId, maxConcurrent, operation, operationTimeoutMs) {
49
129
  const key = String(userId).toLowerCase().trim();
50
130
  let state = userConcurrency.get(key);
@@ -96,52 +176,371 @@ async function withUserLimit(userId, maxConcurrent, operation, operationTimeoutM
96
176
  }
97
177
  }
98
178
  }
99
- function cleanupSessionsForUserId(userId, reason) {
100
- const prefix = normalizeUserId(userId);
179
+ function cleanupSessionsForUserId(userId, reason, clearCanonical = true, options = {}) {
180
+ const key = normalizeUserId(userId);
181
+ const allowInternalSessionKey = options.allowInternalSessionKey ?? true;
182
+ const ownerKeys = new Set();
101
183
  // If a session is currently being created, drop our reference so callers don't keep a stale placeholder.
102
- launchingSessions.delete(prefix);
103
- void (0, vnc_1.stopVnc)(prefix).catch(() => { });
104
- try {
105
- (0, download_1.cleanupUserDownloads)(prefix);
184
+ for (const launchKey of Array.from(launchingSessions.keys())) {
185
+ if (isLaunchingSessionKeyForCleanupKey(launchKey, key, allowInternalSessionKey)) {
186
+ ownerKeys.add(launchingSessionOwners.get(launchKey) ?? key);
187
+ launchingSessions.delete(launchKey);
188
+ launchingSessionOwners.delete(launchKey);
189
+ }
190
+ }
191
+ for (const [sessionKey, session] of Array.from(sessions.entries())) {
192
+ if (!isSessionMapKeyForCleanupKey(sessionKey, key, allowInternalSessionKey))
193
+ continue;
194
+ ownerKeys.add(sessionOwners.get(sessionKey) ?? sessionKey);
195
+ unindexSessionTabs(session);
196
+ sessions.delete(sessionKey);
197
+ sessionOwners.delete(sessionKey);
198
+ (0, logging_1.log)('info', 'session cleaned up', { userId: key, sessionKey, reason });
106
199
  }
107
- catch {
108
- // ignore cleanup errors
200
+ if (ownerKeys.size === 0)
201
+ ownerKeys.add(key);
202
+ for (const ownerKey of ownerKeys) {
203
+ void (0, vnc_1.stopVnc)(ownerKey).catch(() => { });
204
+ try {
205
+ (0, download_1.cleanupUserDownloads)(ownerKey);
206
+ }
207
+ catch {
208
+ // ignore cleanup errors
209
+ }
210
+ (0, tracing_1.cleanupTracing)(ownerKey);
211
+ clearDefaultSessionProfileClaimsForUser(ownerKey);
109
212
  }
110
- (0, tracing_1.cleanupTracing)(prefix);
111
- for (const [key, session] of sessions) {
112
- if (key === prefix || key.startsWith(prefix + ':')) {
113
- unindexSessionTabs(session);
114
- sessions.delete(key);
115
- (0, logging_1.log)('info', 'session cleaned up', { userId: key, reason });
213
+ if (clearCanonical) {
214
+ canonicalProfiles.delete(key);
215
+ clearDefaultSessionProfileClaimsForUser(key);
216
+ const mutex = firstCreateMutexes.get(key);
217
+ if (mutex) {
218
+ mutex.resolve(false);
219
+ firstCreateMutexes.delete(key);
220
+ }
221
+ // Also clear all session profiles for this user
222
+ const profileKeysToDelete = [];
223
+ for (const [profileKey, profile] of sessionProfiles.entries()) {
224
+ if (profile.userId === key) {
225
+ profileKeysToDelete.push(profileKey);
226
+ }
227
+ }
228
+ for (const profileKey of profileKeysToDelete) {
229
+ sessionProfiles.delete(profileKey);
230
+ }
231
+ for (const [profileKey, mutex] of Array.from(sessionProfileCreateMutexes.entries())) {
232
+ if (mutex.userId === key) {
233
+ mutex.resolve(false);
234
+ sessionProfileCreateMutexes.delete(profileKey);
235
+ }
116
236
  }
117
237
  }
118
- userConcurrency.delete(prefix);
238
+ userConcurrency.delete(key);
119
239
  }
120
240
  context_pool_1.contextPool.onEvict((userId) => {
121
- cleanupSessionsForUserId(userId, 'context_evicted');
241
+ cleanupSessionsForUserId(userId, 'context_evicted', false);
122
242
  // Note: the pool will close the context; session cleanup only removes dead Page references.
123
243
  });
124
- exports.SESSION_TIMEOUT_MS = Math.max(60000, Number.parseInt(process.env.CAMOFOX_SESSION_TIMEOUT || '', 10) || 1800000);
125
- exports.MAX_SESSIONS = Math.max(1, Number.parseInt(process.env.CAMOFOX_MAX_SESSIONS || '', 10) || 50);
126
- exports.MAX_TABS_PER_SESSION = Math.max(1, Number.parseInt(process.env.CAMOFOX_MAX_TABS || '', 10) || 10);
244
+ exports.SESSION_TIMEOUT_MS = CONFIG.sessionTimeoutMs;
245
+ exports.MAX_SESSIONS = CONFIG.maxSessions;
246
+ exports.MAX_TABS_PER_SESSION = CONFIG.maxTabsPerSession;
127
247
  function normalizeUserId(userId) {
128
248
  return String(userId);
129
249
  }
130
- function getSessionMapKey(userId, contextOverrides) {
131
- // Persistent profiles are keyed only by userId; overrides are applied on first launch.
132
- void contextOverrides;
133
- return normalizeUserId(userId);
250
+ function encodeKeyComponent(value) {
251
+ return Buffer.from(String(value), 'utf16le').toString('base64url');
252
+ }
253
+ function userSessionMapKey(userId) {
254
+ return `u:${encodeKeyComponent(normalizeUserId(userId))}`;
255
+ }
256
+ function sessionOverlayKey(userId, sessionKey) {
257
+ return `o:${encodeKeyComponent(normalizeUserId(userId))}:${encodeKeyComponent(sessionKey)}`;
258
+ }
259
+ function clearDefaultSessionProfileClaimsForUser(userId) {
260
+ const key = normalizeUserId(userId);
261
+ for (const [claimKey, claim] of Array.from(defaultSessionProfileClaims.entries())) {
262
+ if (claim.userId === key) {
263
+ defaultSessionProfileClaims.delete(claimKey);
264
+ }
265
+ }
266
+ }
267
+ function isSessionMapKeyForUser(sessionMapKey, userKey) {
268
+ return sessionMapKey === userSessionMapKey(userKey) || sessionOwners.get(sessionMapKey) === userKey;
269
+ }
270
+ function isSessionMapKeyForCleanupKey(sessionMapKey, cleanupKey, allowInternalSessionKey = true) {
271
+ return (allowInternalSessionKey && sessionMapKey === cleanupKey) || isSessionMapKeyForUser(sessionMapKey, cleanupKey);
272
+ }
273
+ function isLaunchingSessionKeyForCleanupKey(sessionMapKey, cleanupKey, allowInternalSessionKey = true) {
274
+ return ((allowInternalSessionKey && sessionMapKey === cleanupKey) ||
275
+ sessionMapKey === userSessionMapKey(cleanupKey) ||
276
+ launchingSessionOwners.get(sessionMapKey) === cleanupKey);
277
+ }
278
+ // Backward compatible version - takes contextOverrides instead of session profile
279
+ function getSessionMapKey(userId, contextOverridesOrSessionKey, profileSignature) {
280
+ // New signature: (userId, sessionKey, profileSignature)
281
+ if (typeof contextOverridesOrSessionKey === 'string') {
282
+ const sessionKey = contextOverridesOrSessionKey;
283
+ if (profileSignature) {
284
+ return `p:${encodeKeyComponent(normalizeUserId(userId))}:${encodeKeyComponent(sessionKey)}:${encodeKeyComponent(profileSignature)}`;
285
+ }
286
+ return `s:${encodeKeyComponent(normalizeUserId(userId))}:${encodeKeyComponent(sessionKey)}`;
287
+ }
288
+ // Old signature: (userId, contextOverrides) - backward compatibility
289
+ // This maintains the user-scoped behavior for existing routes
290
+ void contextOverridesOrSessionKey;
291
+ return userSessionMapKey(userId);
292
+ }
293
+ function getEstablishedSessionProfile(userId, sessionKey) {
294
+ return sessionProfiles.get(sessionOverlayKey(userId, sessionKey));
295
+ }
296
+ function hasDefaultSessionProfileRuntime(userId, sessionKey) {
297
+ const key = normalizeUserId(userId);
298
+ if (defaultSessionProfileClaims.has(sessionOverlayKey(key, sessionKey)))
299
+ return true;
300
+ const defaultSession = sessions.get(userSessionMapKey(key));
301
+ return defaultSession?.tabGroups.has(sessionKey) ?? false;
302
+ }
303
+ function claimDefaultSessionProfileRuntime(userId, sessionKey) {
304
+ if (getEstablishedSessionProfile(userId, sessionKey))
305
+ return false;
306
+ if (hasDefaultSessionProfileRuntime(userId, sessionKey))
307
+ return false;
308
+ const key = normalizeUserId(userId);
309
+ defaultSessionProfileClaims.set(sessionOverlayKey(key, sessionKey), { userId: key, sessionKey });
310
+ return true;
311
+ }
312
+ function clearDefaultSessionProfileClaim(userId, sessionKey) {
313
+ defaultSessionProfileClaims.delete(sessionOverlayKey(userId, sessionKey));
314
+ }
315
+ function contextOverridesFromProfile(profile) {
316
+ const overrides = {};
317
+ if (profile.locale !== undefined)
318
+ overrides.locale = profile.locale;
319
+ if (profile.timezoneId !== undefined)
320
+ overrides.timezoneId = profile.timezoneId;
321
+ if (profile.geolocation !== undefined)
322
+ overrides.geolocation = profile.geolocation;
323
+ if (profile.viewport !== undefined)
324
+ overrides.viewport = profile.viewport;
325
+ return Object.keys(overrides).length > 0 ? overrides : null;
326
+ }
327
+ function getSessionProfileLaunchSettings(userId, profileKey) {
328
+ const key = normalizeUserId(userId);
329
+ if (profileKey === userSessionMapKey(key)) {
330
+ const canonical = canonicalProfiles.get(key);
331
+ return canonical ? { contextOverrides: canonical.resolvedOverrides, proxy: null } : undefined;
332
+ }
333
+ for (const profile of sessionProfiles.values()) {
334
+ if (profile.userId !== key)
335
+ continue;
336
+ if (getSessionMapKey(key, profile.sessionKey, profile.signature) !== profileKey)
337
+ continue;
338
+ return {
339
+ contextOverrides: contextOverridesFromProfile(profile.resolvedProfile),
340
+ proxy: profile.resolvedProfile.proxy,
341
+ };
342
+ }
343
+ return undefined;
344
+ }
345
+ function resolveRuntimeSessionProfile(userId, sessionKey, contextOverrides) {
346
+ const userKey = normalizeUserId(userId);
347
+ if (!sessionKey) {
348
+ return {
349
+ sessionMapKey: userSessionMapKey(userKey),
350
+ profileKey: userSessionMapKey(userKey),
351
+ contextOverrides,
352
+ resolvedProxy: null,
353
+ };
354
+ }
355
+ const established = getEstablishedSessionProfile(userId, sessionKey);
356
+ if (!established) {
357
+ return {
358
+ sessionMapKey: userSessionMapKey(userKey),
359
+ profileKey: userSessionMapKey(userKey),
360
+ contextOverrides,
361
+ resolvedProxy: null,
362
+ };
363
+ }
364
+ const profileKey = getSessionMapKey(userId, sessionKey, established.signature);
365
+ return {
366
+ sessionMapKey: profileKey,
367
+ profileKey,
368
+ contextOverrides: contextOverridesFromProfile(established.resolvedProfile),
369
+ resolvedProxy: established.resolvedProfile.proxy,
370
+ };
371
+ }
372
+ function getCanonicalProfile(userId) {
373
+ return canonicalProfiles.get(normalizeUserId(userId));
374
+ }
375
+ function hasCanonicalProfile(userId) {
376
+ return canonicalProfiles.has(normalizeUserId(userId));
377
+ }
378
+ /**
379
+ * Try to acquire the first-create mutex for a user.
380
+ * Returns { acquired: true } if we are the first creator (mutex acquired).
381
+ * Returns { acquired: false, wait: Promise<boolean> } if another request is first-creating.
382
+ * The promise resolves to true (committed) or false (rolled back).
383
+ * If canonical already exists (committed), returns { acquired: false, wait: resolved-true }.
384
+ */
385
+ function acquireFirstCreateMutex(userId) {
386
+ const key = normalizeUserId(userId);
387
+ if (canonicalProfiles.has(key)) {
388
+ return { acquired: false, wait: Promise.resolve(true) };
389
+ }
390
+ const existing = firstCreateMutexes.get(key);
391
+ if (existing) {
392
+ return { acquired: false, wait: existing.promise };
393
+ }
394
+ let resolve;
395
+ const promise = new Promise((r) => {
396
+ resolve = r;
397
+ });
398
+ firstCreateMutexes.set(key, { promise, resolve });
399
+ return { acquired: true };
400
+ }
401
+ /**
402
+ * Commit: store the canonical profile and release the mutex (signaling success to waiters).
403
+ */
404
+ function commitCanonicalProfile(userId, resolved) {
405
+ const key = normalizeUserId(userId);
406
+ const profile = {
407
+ resolvedOverrides: resolved,
408
+ hash: (0, presets_1.contextHash)(resolved),
409
+ establishedAt: Date.now(),
410
+ };
411
+ canonicalProfiles.set(key, profile);
412
+ const mutex = firstCreateMutexes.get(key);
413
+ if (mutex) {
414
+ mutex.resolve(true);
415
+ firstCreateMutexes.delete(key);
416
+ }
417
+ (0, logging_1.log)('info', 'canonical profile committed', { userId: key, hash: profile.hash });
418
+ return profile;
419
+ }
420
+ /**
421
+ * Rollback: release the mutex (signaling failure to waiters). No canonical is stored.
422
+ */
423
+ function rollbackCanonicalMutex(userId) {
424
+ const key = normalizeUserId(userId);
425
+ const mutex = firstCreateMutexes.get(key);
426
+ if (mutex) {
427
+ mutex.resolve(false);
428
+ firstCreateMutexes.delete(key);
429
+ }
430
+ }
431
+ /**
432
+ * Create a CanonicalProfile object without storing it (for hash comparison during first-create).
433
+ */
434
+ function createCanonicalProfile(resolved) {
435
+ return {
436
+ resolvedOverrides: resolved,
437
+ hash: (0, presets_1.contextHash)(resolved),
438
+ establishedAt: Date.now(),
439
+ };
440
+ }
441
+ function clearCanonicalProfile(userId) {
442
+ const key = normalizeUserId(userId);
443
+ canonicalProfiles.delete(key);
444
+ const mutex = firstCreateMutexes.get(key);
445
+ if (mutex) {
446
+ mutex.resolve(false);
447
+ firstCreateMutexes.delete(key);
448
+ }
449
+ }
450
+ /**
451
+ * Store or validate a session profile for a specific userId + sessionKey combination.
452
+ * Returns the established profile if successful.
453
+ * Throws if a conflicting profile is already established for the same userId + sessionKey.
454
+ */
455
+ function establishSessionProfile(userId, sessionKey, profile) {
456
+ const key = sessionOverlayKey(userId, sessionKey);
457
+ const existing = sessionProfiles.get(key);
458
+ if (existing) {
459
+ if (existing.signature !== profile.signature) {
460
+ throw new Error('Session profile conflict');
461
+ }
462
+ return existing;
463
+ }
464
+ const established = {
465
+ userId: normalizeUserId(userId),
466
+ sessionKey,
467
+ signature: profile.signature,
468
+ resolvedProfile: profile,
469
+ establishedAt: Date.now(),
470
+ };
471
+ sessionProfiles.set(key, established);
472
+ (0, logging_1.log)('info', 'session profile established', {
473
+ userId: established.userId,
474
+ sessionKey,
475
+ signature: profile.signature,
476
+ });
477
+ return established;
478
+ }
479
+ function clearSessionProfile(userId, sessionKey) {
480
+ const key = sessionOverlayKey(userId, sessionKey);
481
+ sessionProfiles.delete(key);
482
+ }
483
+ function acquireSessionProfileCreateMutex(userId, sessionKey, signature) {
484
+ const key = sessionOverlayKey(userId, sessionKey);
485
+ const existing = sessionProfileCreateMutexes.get(key);
486
+ if (existing) {
487
+ return { acquired: false, wait: existing.promise };
488
+ }
489
+ let resolve;
490
+ const promise = new Promise((r) => {
491
+ resolve = r;
492
+ });
493
+ const mutex = {
494
+ userId: normalizeUserId(userId),
495
+ sessionKey,
496
+ signature,
497
+ promise,
498
+ resolve,
499
+ };
500
+ sessionProfileCreateMutexes.set(key, mutex);
501
+ let released = false;
502
+ return {
503
+ acquired: true,
504
+ release: (committed) => {
505
+ if (released)
506
+ return;
507
+ released = true;
508
+ const current = sessionProfileCreateMutexes.get(key);
509
+ if (current === mutex) {
510
+ current.resolve(committed);
511
+ sessionProfileCreateMutexes.delete(key);
512
+ }
513
+ },
514
+ };
515
+ }
516
+ async function waitForSessionProfileCreate(userId, sessionKey) {
517
+ const pending = sessionProfileCreateMutexes.get(sessionOverlayKey(userId, sessionKey));
518
+ if (pending) {
519
+ await pending.promise.catch(() => false);
520
+ }
521
+ }
522
+ async function rollbackSessionProfileRuntime(userId, sessionKey, profileSignature) {
523
+ const overlayKey = sessionOverlayKey(userId, sessionKey);
524
+ const existingProfile = sessionProfiles.get(overlayKey);
525
+ if (existingProfile?.signature === profileSignature) {
526
+ sessionProfiles.delete(overlayKey);
527
+ }
528
+ const sessionMapKey = getSessionMapKey(userId, sessionKey, profileSignature);
529
+ const session = sessions.get(sessionMapKey);
530
+ if (session) {
531
+ unindexSessionTabs(session);
532
+ sessions.delete(sessionMapKey);
533
+ }
534
+ sessionOwners.delete(sessionMapKey);
535
+ launchingSessions.delete(sessionMapKey);
536
+ launchingSessionOwners.delete(sessionMapKey);
537
+ await context_pool_1.contextPool.closeContext(sessionMapKey).catch(() => { });
134
538
  }
135
539
  function getSessionsForUser(userId) {
136
540
  if (userId === undefined || userId === null)
137
541
  return [];
138
- const prefix = normalizeUserId(userId);
139
- const out = [];
140
- for (const [key, session] of sessions) {
141
- if (key === prefix || key.startsWith(prefix + ':'))
142
- out.push([key, session]);
143
- }
144
- return out;
542
+ const key = normalizeUserId(userId);
543
+ return Array.from(sessions.entries()).filter(([sessionKey]) => isSessionMapKeyForUser(sessionKey, key));
145
544
  }
146
545
  function getAllSessions() {
147
546
  return sessions;
@@ -155,6 +554,16 @@ function countTotalTabsForSessions(sessionsForUser) {
155
554
  }
156
555
  return totalTabs;
157
556
  }
557
+ function getLifecycleSessionSnapshot() {
558
+ return {
559
+ liveSessions: sessions.size,
560
+ liveTabs: countTotalTabsForSessions(),
561
+ stagedCreates: launchingSessions.size,
562
+ };
563
+ }
564
+ function getSessionsSnapshot() {
565
+ return new Map(sessions);
566
+ }
158
567
  function getTabGroup(session, sessionKey) {
159
568
  let group = session.tabGroups.get(sessionKey);
160
569
  if (!group) {
@@ -187,10 +596,10 @@ function unindexSessionTabs(session) {
187
596
  function findTabById(tabId, userId) {
188
597
  if (userId === undefined || userId === null)
189
598
  return null;
190
- const prefix = normalizeUserId(userId);
599
+ const key = normalizeUserId(userId);
191
600
  const indexedKey = tabSessionIndex.get(tabId);
192
601
  if (indexedKey) {
193
- if (!(indexedKey === prefix || indexedKey.startsWith(prefix + ':'))) {
602
+ if (!isSessionMapKeyForUser(indexedKey, key)) {
194
603
  return null;
195
604
  }
196
605
  const session = sessions.get(indexedKey);
@@ -201,64 +610,125 @@ function findTabById(tabId, userId) {
201
610
  }
202
611
  tabSessionIndex.delete(tabId);
203
612
  }
204
- for (const [sessionKey, session] of sessions) {
205
- if (!(sessionKey === prefix || sessionKey.startsWith(prefix + ':')))
206
- continue;
207
- const found = findTab(session, tabId);
208
- if (found) {
209
- tabSessionIndex.set(tabId, sessionKey);
210
- return { sessionKey, session, ...found };
211
- }
613
+ const defaultSessionKey = userSessionMapKey(key);
614
+ const session = sessions.get(defaultSessionKey);
615
+ if (!session)
616
+ return null;
617
+ const found = findTab(session, tabId);
618
+ if (found) {
619
+ tabSessionIndex.set(tabId, defaultSessionKey);
620
+ return { sessionKey: defaultSessionKey, session, ...found };
212
621
  }
213
622
  return null;
214
623
  }
215
- async function getSession(userId, contextOverrides) {
216
- const key = getSessionMapKey(userId, contextOverrides);
217
- let session = sessions.get(key);
624
+ function buildBrowserContextOptions(contextOverrides, hasSessionProxy = false) {
218
625
  const resolved = contextOverrides || {};
219
626
  const contextOptions = {
220
627
  viewport: resolved.viewport || { width: 1280, height: 720 },
221
628
  permissions: ['geolocation'],
222
629
  };
223
630
  const hasOverrides = !!(contextOverrides &&
224
- (contextOverrides.locale !== undefined || contextOverrides.timezoneId !== undefined || contextOverrides.geolocation !== undefined));
631
+ (contextOverrides.locale !== undefined ||
632
+ contextOverrides.timezoneId !== undefined ||
633
+ contextOverrides.geolocation !== undefined));
225
634
  // With proxy+geoip, camoufox auto-configures locale/timezone/geo from proxy IP.
226
635
  // If caller explicitly supplies overrides, apply them even when proxy is active.
227
- if (!CONFIG.proxy.host || hasOverrides) {
636
+ if ((!CONFIG.proxy.host && !hasSessionProxy) || hasOverrides) {
228
637
  contextOptions.locale = resolved.locale || 'en-US';
229
638
  contextOptions.timezoneId = resolved.timezoneId || 'America/Los_Angeles';
230
639
  contextOptions.geolocation = resolved.geolocation || { latitude: 37.7749, longitude: -122.4194 };
231
640
  }
641
+ return contextOptions;
642
+ }
643
+ async function createStagedSession(userId, contextOverrides, sessionKey) {
644
+ const runtimeProfile = resolveRuntimeSessionProfile(userId, sessionKey, contextOverrides);
645
+ if (context_pool_1.contextPool.size() >= exports.MAX_SESSIONS) {
646
+ throw new Error('Maximum concurrent sessions reached');
647
+ }
648
+ const generation = node_crypto_1.default.randomUUID();
649
+ const contextOptions = buildBrowserContextOptions(runtimeProfile.contextOverrides, !!runtimeProfile.resolvedProxy);
650
+ const entry = await context_pool_1.contextPool.ensureContext(runtimeProfile.profileKey, normalizeUserId(userId), contextOptions, runtimeProfile.resolvedProxy, true, generation);
651
+ const session = {
652
+ context: entry.context,
653
+ tabGroups: new Map(),
654
+ lastAccess: Date.now(),
655
+ };
656
+ return { session, contextEntry: entry, generation };
657
+ }
658
+ function commitStagedFirstUse(userId, session, contextOverrides, tabInfo, generation) {
659
+ const key = normalizeUserId(userId);
660
+ const entry = context_pool_1.contextPool.getEntry(tabInfo.sessionMapKey);
661
+ if (!entry || entry.stagedGeneration !== generation)
662
+ return false;
663
+ if (!firstCreateMutexes.has(key) || canonicalProfiles.has(key)) {
664
+ return false;
665
+ }
666
+ session.lastAccess = Date.now();
667
+ const group = getTabGroup(session, tabInfo.sessionKey);
668
+ group.set(tabInfo.tabId, tabInfo.tabState);
669
+ sessions.set(tabInfo.sessionMapKey, session);
670
+ sessionOwners.set(tabInfo.sessionMapKey, key);
671
+ entry.staged = false;
672
+ entry.stagedGeneration = undefined;
673
+ indexTab(tabInfo.tabId, tabInfo.sessionMapKey);
674
+ commitCanonicalProfile(userId, contextOverrides);
675
+ return true;
676
+ }
677
+ async function rollbackStagedFirstUse(userId, generation) {
678
+ const key = normalizeUserId(userId);
679
+ try {
680
+ try {
681
+ (0, download_1.cleanupUserDownloads)(key);
682
+ }
683
+ catch {
684
+ // ignore cleanup errors
685
+ }
686
+ await context_pool_1.contextPool.closeStagedContextByUserId(key, generation);
687
+ }
688
+ finally {
689
+ rollbackCanonicalMutex(userId);
690
+ }
691
+ }
692
+ async function getSession(userId, contextOverrides, sessionKey) {
693
+ const key = normalizeUserId(userId);
694
+ await waitForLifecycleIdleClosure(key);
695
+ const runtimeProfile = resolveRuntimeSessionProfile(userId, sessionKey, contextOverrides);
696
+ let session = sessions.get(runtimeProfile.sessionMapKey);
697
+ const contextOptions = buildBrowserContextOptions(runtimeProfile.contextOverrides, !!runtimeProfile.resolvedProxy);
232
698
  if (!session) {
233
- const existingLaunch = launchingSessions.get(key);
699
+ const existingLaunch = launchingSessions.get(runtimeProfile.sessionMapKey);
234
700
  if (existingLaunch) {
235
701
  session = await existingLaunch;
236
702
  session.lastAccess = Date.now();
237
703
  return session;
238
704
  }
239
- if (sessions.size + launchingSessions.size >= exports.MAX_SESSIONS) {
705
+ if (context_pool_1.contextPool.size() >= exports.MAX_SESSIONS) {
240
706
  throw new Error('Maximum concurrent sessions reached');
241
707
  }
242
708
  const launchPromise = (async () => {
243
- const entry = await context_pool_1.contextPool.ensureContext(normalizeUserId(userId), contextOptions);
709
+ const entry = await context_pool_1.contextPool.ensureContext(runtimeProfile.profileKey, key, contextOptions, runtimeProfile.resolvedProxy);
244
710
  const created = { context: entry.context, tabGroups: new Map(), lastAccess: Date.now() };
245
- sessions.set(key, created);
246
- (0, logging_1.log)('info', 'session created', { userId: key });
711
+ sessions.set(runtimeProfile.sessionMapKey, created);
712
+ sessionOwners.set(runtimeProfile.sessionMapKey, key);
713
+ (0, logging_1.log)('info', 'session created', { userId: key, sessionMapKey: runtimeProfile.sessionMapKey });
247
714
  return created;
248
715
  })();
249
- launchingSessions.set(key, launchPromise);
716
+ launchingSessionOwners.set(runtimeProfile.sessionMapKey, key);
717
+ launchingSessions.set(runtimeProfile.sessionMapKey, launchPromise);
250
718
  try {
251
719
  session = await launchPromise;
252
720
  }
253
721
  finally {
254
- launchingSessions.delete(key);
722
+ launchingSessions.delete(runtimeProfile.sessionMapKey);
723
+ launchingSessionOwners.delete(runtimeProfile.sessionMapKey);
255
724
  }
256
725
  }
257
726
  else {
258
727
  // Re-resolve context on each access; ContextPool de-dupes launches and detects unexpected closes.
259
- const entry = await context_pool_1.contextPool.ensureContext(normalizeUserId(userId), contextOptions);
728
+ const entry = await context_pool_1.contextPool.ensureContext(runtimeProfile.profileKey, key, contextOptions, runtimeProfile.resolvedProxy);
260
729
  session.context = entry.context;
261
730
  session.lastAccess = Date.now();
731
+ sessionOwners.set(runtimeProfile.sessionMapKey, key);
262
732
  }
263
733
  // For newly created sessions, lastAccess/context are already set.
264
734
  session.lastAccess = Date.now();
@@ -273,30 +743,59 @@ function unindexTab(tabId) {
273
743
  }
274
744
  function clearAllState() {
275
745
  sessions.clear();
746
+ sessionOwners.clear();
747
+ launchingSessions.clear();
748
+ launchingSessionOwners.clear();
276
749
  tabSessionIndex.clear();
750
+ canonicalProfiles.clear();
751
+ sessionProfiles.clear();
752
+ defaultSessionProfileClaims.clear();
753
+ for (const [, mutex] of sessionProfileCreateMutexes)
754
+ mutex.resolve(false);
755
+ sessionProfileCreateMutexes.clear();
756
+ for (const [, state] of lifecycleIdleClosures)
757
+ state.resolve();
758
+ lifecycleIdleClosures.clear();
759
+ for (const [, mutex] of firstCreateMutexes)
760
+ mutex.resolve(false);
761
+ firstCreateMutexes.clear();
277
762
  (0, tab_1.clearAllTabLocks)();
278
763
  userConcurrency.clear();
279
764
  }
280
- async function closeSessionsForUser(userId) {
281
- const prefix = userId;
282
- await context_pool_1.contextPool.closeContext(prefix).catch(() => { });
283
- cleanupSessionsForUserId(prefix, 'explicit_close');
765
+ async function closeSessionsForUser(userId, options = {}) {
766
+ const key = normalizeUserId(userId);
767
+ await context_pool_1.contextPool.closeStagedContextByUserId(key).catch(() => { });
768
+ await context_pool_1.contextPool.closeContextByUserId(key).catch(() => { });
769
+ cleanupSessionsForUserId(key, 'explicit_close', options.clearProfiles ?? true, { allowInternalSessionKey: false });
284
770
  }
285
771
  async function closeAllSessions() {
286
772
  await context_pool_1.contextPool.closeAll().catch(() => { });
287
- for (const [userId, session] of sessions) {
288
- void (0, vnc_1.stopVnc)(userId).catch(() => { });
773
+ for (const [sessionKey, session] of sessions) {
774
+ const ownerUserId = sessionOwners.get(sessionKey) ?? sessionKey;
775
+ void (0, vnc_1.stopVnc)(ownerUserId).catch(() => { });
289
776
  unindexSessionTabs(session);
290
- sessions.delete(userId);
291
- (0, tracing_1.cleanupTracing)(userId);
777
+ sessions.delete(sessionKey);
778
+ sessionOwners.delete(sessionKey);
779
+ (0, tracing_1.cleanupTracing)(ownerUserId);
292
780
  try {
293
- (0, download_1.cleanupUserDownloads)(userId);
781
+ (0, download_1.cleanupUserDownloads)(ownerUserId);
294
782
  }
295
783
  catch {
296
784
  // ignore
297
785
  }
298
786
  }
299
787
  launchingSessions.clear();
788
+ launchingSessionOwners.clear();
789
+ sessionOwners.clear();
790
+ canonicalProfiles.clear();
791
+ sessionProfiles.clear();
792
+ defaultSessionProfileClaims.clear();
793
+ for (const [, mutex] of sessionProfileCreateMutexes)
794
+ mutex.resolve(false);
795
+ sessionProfileCreateMutexes.clear();
796
+ for (const [, mutex] of firstCreateMutexes)
797
+ mutex.resolve(false);
798
+ firstCreateMutexes.clear();
300
799
  }
301
800
  let cleanupInterval = null;
302
801
  function startCleanupInterval() {
@@ -306,12 +805,15 @@ function startCleanupInterval() {
306
805
  const now = Date.now();
307
806
  for (const [sessionKey, session] of sessions) {
308
807
  if (now - session.lastAccess > exports.SESSION_TIMEOUT_MS) {
808
+ const ownerUserId = sessionOwners.get(sessionKey) ?? sessionKey;
309
809
  // Persistent profile is preserved on disk; closing the context frees resources.
310
810
  context_pool_1.contextPool.closeContext(sessionKey).catch(() => { });
311
811
  unindexSessionTabs(session);
812
+ clearDefaultSessionProfileClaimsForUser(ownerUserId);
312
813
  sessions.delete(sessionKey);
313
- (0, tracing_1.cleanupTracing)(sessionKey);
314
- (0, logging_1.log)('info', 'session expired', { userId: sessionKey });
814
+ sessionOwners.delete(sessionKey);
815
+ (0, tracing_1.cleanupTracing)(ownerUserId);
816
+ (0, logging_1.log)('info', 'session expired', { userId: ownerUserId, sessionKey });
315
817
  }
316
818
  }
317
819
  }, 60_000);
@@ -323,4 +825,60 @@ function stopCleanupInterval() {
323
825
  cleanupInterval = null;
324
826
  }
325
827
  }
828
+ /**
829
+ * Stage 1 idle cleanup: close runtime state (contexts, session data) for users with no tabs.
830
+ * Does NOT clear stored session profiles - they survive idle cleanup.
831
+ * Does NOT exit the daemon - that is Stage 2 (Task 4).
832
+ * @param sessionSnapshot Snapshot of sessions map taken before cleanup started
833
+ * @param contextSnapshot Snapshot of context pool entries taken before cleanup started
834
+ * @param cleanupStartedMs Timestamp when cleanup was triggered (used to avoid closing newly-created contexts)
835
+ */
836
+ async function runLifecycleIdleCleanup(sessionSnapshot, contextSnapshot, cleanupStartedMs) {
837
+ const closedUsers = [];
838
+ // Use the provided snapshots to avoid race with new context creation
839
+ const sessionKeysToCleanup = new Set();
840
+ for (const [sessionKey, session] of sessionSnapshot.entries()) {
841
+ const tabCount = countTotalTabsForSessions([[sessionKey, session]]);
842
+ // Only cleanup sessions with zero tabs
843
+ if (tabCount === 0) {
844
+ sessionKeysToCleanup.add(sessionKey);
845
+ }
846
+ }
847
+ // Close only the specific contexts from the snapshot that were created before cleanup started
848
+ const contextsToClose = [];
849
+ for (const [profileKey, entry] of contextSnapshot.entries()) {
850
+ // Skip staged, launching, or newly-created contexts
851
+ if (sessionKeysToCleanup.has(profileKey) && !entry.staged && !entry.launching && entry.createdAt < cleanupStartedMs) {
852
+ contextsToClose.push({ profileKey, createdAt: entry.createdAt, lastAccess: entry.lastAccess });
853
+ }
854
+ }
855
+ // Close the specific contexts from the snapshot
856
+ const actuallyClosedSessionKeys = new Set();
857
+ for (const { profileKey, createdAt, lastAccess } of contextsToClose) {
858
+ const entry = contextSnapshot.get(profileKey);
859
+ const releaseIdleClosure = entry ? beginLifecycleIdleClosure(entry.userId) : null;
860
+ try {
861
+ await context_pool_1.contextPool.closeContextIfMatches(profileKey, createdAt, lastAccess);
862
+ // Verify the context was actually closed (not skipped due to reuse)
863
+ const stillExists = context_pool_1.contextPool.getEntry(profileKey);
864
+ if (!stillExists) {
865
+ // Context was actually closed, mark this exact session/profile key for cleanup.
866
+ actuallyClosedSessionKeys.add(profileKey);
867
+ }
868
+ }
869
+ catch (err) {
870
+ const message = err instanceof Error ? err.message : String(err);
871
+ (0, logging_1.log)('error', 'idle cleanup failed to close context', { profileKey, error: message });
872
+ }
873
+ finally {
874
+ releaseIdleClosure?.();
875
+ }
876
+ }
877
+ // Clean up session data ONLY for session/profile keys whose contexts were actually closed.
878
+ for (const sessionKey of actuallyClosedSessionKeys) {
879
+ cleanupSessionsForUserId(sessionKey, 'idle_cleanup', false);
880
+ closedUsers.push(sessionKey);
881
+ }
882
+ return { closedUsers };
883
+ }
326
884
  //# sourceMappingURL=session.js.map