opensteer 0.6.12 → 0.6.13

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.
@@ -25,1069 +25,2655 @@ import {
25
25
  } from "./chunk-3H5RRIMZ.js";
26
26
 
27
27
  // src/browser/persistent-profile.ts
28
- import { createHash, randomUUID } from "crypto";
29
- import { execFile } from "child_process";
30
- import { createReadStream, existsSync } from "fs";
28
+ import { createHash, randomUUID as randomUUID3 } from "crypto";
29
+ import { execFile as execFile2 } from "child_process";
30
+ import { createReadStream, existsSync as existsSync3 } from "fs";
31
31
  import {
32
32
  cp,
33
33
  copyFile,
34
- mkdir,
34
+ mkdir as mkdir3,
35
35
  mkdtemp,
36
- readdir,
37
- readFile,
38
- rename,
39
- rm,
36
+ readdir as readdir2,
37
+ readFile as readFile4,
38
+ rename as rename3,
39
+ rm as rm3,
40
40
  stat,
41
- writeFile
41
+ writeFile as writeFile3
42
42
  } from "fs/promises";
43
43
  import { homedir, tmpdir } from "os";
44
- import { basename, dirname, join, relative, sep } from "path";
44
+ import { basename as basename3, dirname as dirname4, join as join4, relative, sep } from "path";
45
+ import { promisify as promisify2 } from "util";
46
+
47
+ // src/browser/persistent-profile-coordination.ts
48
+ import { basename, dirname as dirname2, join as join2 } from "path";
49
+
50
+ // src/browser/dir-lock.ts
51
+ import { randomUUID } from "crypto";
52
+ import { existsSync } from "fs";
53
+ import { mkdir, readFile as readFile2, rename, rm, writeFile } from "fs/promises";
54
+ import { dirname, join } from "path";
55
+
56
+ // src/browser/process-owner.ts
57
+ import { execFile } from "child_process";
58
+ import { readFile } from "fs/promises";
45
59
  import { promisify } from "util";
46
60
  var execFileAsync = promisify(execFile);
47
- var OPENSTEER_META_FILE = ".opensteer-meta.json";
48
- var OPENSTEER_RUNTIME_META_FILE = ".opensteer-runtime.json";
49
- var LOCK_OWNER_FILE = "owner.json";
50
- var LOCK_RECLAIMER_DIR = "reclaimer";
51
61
  var PROCESS_STARTED_AT_MS = Math.floor(Date.now() - process.uptime() * 1e3);
52
62
  var PROCESS_START_TIME_TOLERANCE_MS = 1e3;
53
- var PROFILE_LOCK_RETRY_DELAY_MS = 50;
63
+ var PROCESS_LIST_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
54
64
  var PS_COMMAND_ENV = { ...process.env, LC_ALL: "C" };
55
65
  var LINUX_STAT_START_TIME_FIELD_INDEX = 19;
56
- var CHROME_SINGLETON_ENTRIES = /* @__PURE__ */ new Set([
57
- "SingletonCookie",
58
- "SingletonLock",
59
- "SingletonSocket",
60
- "DevToolsActivePort",
61
- "lockfile"
62
- ]);
63
- var COPY_SKIP_ENTRIES = /* @__PURE__ */ new Set([
64
- ...CHROME_SINGLETON_ENTRIES,
65
- OPENSTEER_META_FILE,
66
- OPENSTEER_RUNTIME_META_FILE
67
- ]);
68
- var SKIPPED_ROOT_DIRECTORIES = /* @__PURE__ */ new Set([
69
- "Crash Reports",
70
- "Crashpad",
71
- "BrowserMetrics",
72
- "GrShaderCache",
73
- "ShaderCache",
74
- "GraphiteDawnCache",
75
- "component_crx_cache",
76
- "Crowd Deny",
77
- "hyphen-data",
78
- "OnDeviceHeadSuggestModel",
79
- "OptimizationGuidePredictionModels",
80
- "Segmentation Platform",
81
- "SmartCardDeviceNames",
82
- "WidevineCdm",
83
- "pnacl"
84
- ]);
85
- var CURRENT_PROCESS_LOCK_OWNER = {
66
+ var CURRENT_PROCESS_OWNER = {
86
67
  pid: process.pid,
87
68
  processStartedAtMs: PROCESS_STARTED_AT_MS
88
69
  };
89
70
  var linuxClockTicksPerSecondPromise = null;
90
- async function getOrCreatePersistentProfile(sourceUserDataDir, profileDirectory, profilesRootDir = defaultPersistentProfilesRootDir()) {
91
- const resolvedSourceUserDataDir = expandHome(sourceUserDataDir);
92
- const targetUserDataDir = join(
93
- expandHome(profilesRootDir),
94
- buildPersistentProfileKey(resolvedSourceUserDataDir, profileDirectory)
95
- );
96
- const sourceProfileDir = join(resolvedSourceUserDataDir, profileDirectory);
97
- const metadata = buildPersistentProfileMetadata(
98
- resolvedSourceUserDataDir,
99
- profileDirectory
100
- );
101
- await mkdir(dirname(targetUserDataDir), { recursive: true });
102
- return await withPersistentProfileLock(targetUserDataDir, async () => {
103
- await cleanOrphanedOwnedDirs(
104
- dirname(targetUserDataDir),
105
- buildPersistentProfileTempDirNamePrefix(targetUserDataDir)
106
- );
107
- if (!existsSync(sourceProfileDir)) {
108
- throw new Error(
109
- `Chrome profile "${profileDirectory}" was not found in "${resolvedSourceUserDataDir}".`
110
- );
111
- }
112
- const created = await createPersistentProfileClone(
113
- resolvedSourceUserDataDir,
114
- sourceProfileDir,
115
- targetUserDataDir,
116
- profileDirectory,
117
- metadata
118
- );
119
- await ensurePersistentProfileMetadata(targetUserDataDir, metadata);
120
- return {
121
- created,
122
- userDataDir: targetUserDataDir
123
- };
124
- });
125
- }
126
- async function clearPersistentProfileSingletons(userDataDir) {
127
- await Promise.all(
128
- [...CHROME_SINGLETON_ENTRIES].map(
129
- (entry) => rm(join(userDataDir, entry), {
130
- force: true,
131
- recursive: true
132
- }).catch(() => void 0)
133
- )
134
- );
135
- }
136
- async function createIsolatedRuntimeProfile(sourceUserDataDir, runtimesRootDir = defaultRuntimeProfilesRootDir()) {
137
- const resolvedSourceUserDataDir = expandHome(sourceUserDataDir);
138
- const runtimeRootDir = expandHome(runtimesRootDir);
139
- await mkdir(runtimeRootDir, { recursive: true });
140
- return await withPersistentProfileLock(
141
- resolvedSourceUserDataDir,
142
- async () => {
143
- const sourceMetadata = await readPersistentProfileMetadata(
144
- resolvedSourceUserDataDir
145
- );
146
- await cleanOrphanedOwnedDirs(
147
- runtimeRootDir,
148
- buildRuntimeProfileDirNamePrefix(resolvedSourceUserDataDir)
149
- );
150
- const runtimeUserDataDir = await mkdtemp(
151
- buildRuntimeProfileDirPrefix(
152
- runtimeRootDir,
153
- resolvedSourceUserDataDir
154
- )
155
- );
156
- try {
157
- await copyUserDataDirSnapshot(
158
- resolvedSourceUserDataDir,
159
- runtimeUserDataDir
160
- );
161
- await writeRuntimeProfileMetadata(
162
- runtimeUserDataDir,
163
- await buildRuntimeProfileMetadata(
164
- runtimeUserDataDir,
165
- sourceMetadata?.profileDirectory ?? null
166
- )
167
- );
168
- return {
169
- persistentUserDataDir: resolvedSourceUserDataDir,
170
- userDataDir: runtimeUserDataDir
171
- };
172
- } catch (error) {
173
- await rm(runtimeUserDataDir, {
174
- recursive: true,
175
- force: true
176
- }).catch(() => void 0);
177
- throw error;
178
- }
179
- }
180
- );
181
- }
182
- async function persistIsolatedRuntimeProfile(runtimeUserDataDir, persistentUserDataDir) {
183
- const resolvedRuntimeUserDataDir = expandHome(runtimeUserDataDir);
184
- const resolvedPersistentUserDataDir = expandHome(persistentUserDataDir);
185
- await withPersistentProfileLock(resolvedPersistentUserDataDir, async () => {
186
- await mkdir(dirname(resolvedPersistentUserDataDir), { recursive: true });
187
- await cleanOrphanedOwnedDirs(
188
- dirname(resolvedPersistentUserDataDir),
189
- buildPersistentProfileTempDirNamePrefix(resolvedPersistentUserDataDir)
190
- );
191
- const metadata = await requirePersistentProfileMetadata(
192
- resolvedPersistentUserDataDir
193
- );
194
- const runtimeMetadata = await requireRuntimeProfileMetadata(
195
- resolvedRuntimeUserDataDir,
196
- metadata.profileDirectory
197
- );
198
- await mergePersistentProfileSnapshot(
199
- resolvedRuntimeUserDataDir,
200
- resolvedPersistentUserDataDir,
201
- metadata,
202
- runtimeMetadata
203
- );
204
- });
205
- await rm(resolvedRuntimeUserDataDir, {
206
- recursive: true,
207
- force: true
208
- });
209
- }
210
- function buildPersistentProfileKey(sourceUserDataDir, profileDirectory) {
211
- const hash = createHash("sha256").update(`${sourceUserDataDir}\0${profileDirectory}`).digest("hex").slice(0, 16);
212
- const sourceLabel = sanitizePathSegment(basename(sourceUserDataDir) || "user-data");
213
- const profileLabel = sanitizePathSegment(profileDirectory || "Default");
214
- return `${sourceLabel}-${profileLabel}-${hash}`;
71
+ function parseProcessOwner(value) {
72
+ if (!value || typeof value !== "object") {
73
+ return null;
74
+ }
75
+ const parsed = value;
76
+ const pid = Number(parsed.pid);
77
+ const processStartedAtMs = Number(parsed.processStartedAtMs);
78
+ if (!Number.isInteger(pid) || pid <= 0) {
79
+ return null;
80
+ }
81
+ if (!Number.isInteger(processStartedAtMs) || processStartedAtMs <= 0) {
82
+ return null;
83
+ }
84
+ return {
85
+ pid,
86
+ processStartedAtMs
87
+ };
215
88
  }
216
- function defaultPersistentProfilesRootDir() {
217
- return join(homedir(), ".opensteer", "real-browser-profiles");
89
+ function processOwnersEqual(left, right) {
90
+ if (!left || !right) {
91
+ return left === right;
92
+ }
93
+ return left.pid === right.pid && left.processStartedAtMs === right.processStartedAtMs;
218
94
  }
219
- function defaultRuntimeProfilesRootDir() {
220
- return join(tmpdir(), "opensteer-real-browser-runtimes");
95
+ async function getProcessLiveness(owner) {
96
+ if (owner.pid === process.pid && hasMatchingProcessStartTime(
97
+ owner.processStartedAtMs,
98
+ PROCESS_STARTED_AT_MS
99
+ )) {
100
+ return "live";
101
+ }
102
+ const startedAtMs = await readProcessStartedAtMs(owner.pid);
103
+ if (typeof startedAtMs === "number") {
104
+ return hasMatchingProcessStartTime(
105
+ owner.processStartedAtMs,
106
+ startedAtMs
107
+ ) ? "live" : "dead";
108
+ }
109
+ return isProcessRunning(owner.pid) ? "unknown" : "dead";
221
110
  }
222
- function sanitizePathSegment(value) {
223
- const sanitized = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/-+/g, "-");
224
- return sanitized.replace(/^-|-$/g, "") || "profile";
111
+ function isProcessRunning(pid) {
112
+ try {
113
+ process.kill(pid, 0);
114
+ return true;
115
+ } catch (error) {
116
+ const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
117
+ return code !== "ESRCH";
118
+ }
225
119
  }
226
- function isProfileDirectory(userDataDir, entry) {
227
- return existsSync(join(userDataDir, entry, "Preferences"));
120
+ async function readProcessOwner(pid) {
121
+ const processStartedAtMs = await readProcessStartedAtMs(pid);
122
+ if (processStartedAtMs === null) {
123
+ return null;
124
+ }
125
+ return {
126
+ pid,
127
+ processStartedAtMs
128
+ };
228
129
  }
229
- async function copyUserDataDirSnapshot(sourceUserDataDir, targetUserDataDir) {
230
- await cp(sourceUserDataDir, targetUserDataDir, {
231
- recursive: true,
232
- filter: (candidatePath) => shouldCopyRuntimeSnapshotEntry(sourceUserDataDir, candidatePath)
233
- });
130
+ function hasMatchingProcessStartTime(expectedStartedAtMs, actualStartedAtMs) {
131
+ return Math.abs(expectedStartedAtMs - actualStartedAtMs) <= PROCESS_START_TIME_TOLERANCE_MS;
234
132
  }
235
- function shouldCopyRuntimeSnapshotEntry(userDataDir, candidatePath) {
236
- const candidateRelativePath = relative(userDataDir, candidatePath);
237
- if (!candidateRelativePath) {
238
- return true;
133
+ async function readProcessStartedAtMs(pid) {
134
+ if (pid <= 0) {
135
+ return null;
239
136
  }
240
- const segments = candidateRelativePath.split(sep).filter(Boolean);
241
- if (segments.length !== 1) {
242
- return true;
137
+ if (process.platform === "linux") {
138
+ return await readLinuxProcessStartedAtMs(pid);
243
139
  }
244
- return !COPY_SKIP_ENTRIES.has(segments[0]);
140
+ if (process.platform === "win32") {
141
+ return await readWindowsProcessStartedAtMs(pid);
142
+ }
143
+ return await readPsProcessStartedAtMs(pid);
245
144
  }
246
- async function copyRootLevelEntries(sourceUserDataDir, targetUserDataDir, targetProfileDirectory) {
247
- let entries;
145
+ async function readLinuxProcessStartedAtMs(pid) {
146
+ let statRaw;
248
147
  try {
249
- entries = await readdir(sourceUserDataDir);
148
+ statRaw = await readFile(`/proc/${pid}/stat`, "utf8");
250
149
  } catch {
251
- return;
150
+ return null;
252
151
  }
253
- const copyTasks = [];
254
- for (const entry of entries) {
255
- if (COPY_SKIP_ENTRIES.has(entry)) continue;
256
- if (entry === targetProfileDirectory) continue;
257
- const sourcePath = join(sourceUserDataDir, entry);
258
- const targetPath = join(targetUserDataDir, entry);
259
- if (existsSync(targetPath)) continue;
260
- let entryStat;
261
- try {
262
- entryStat = await stat(sourcePath);
263
- } catch {
264
- continue;
265
- }
266
- if (entryStat.isFile()) {
267
- copyTasks.push(copyFile(sourcePath, targetPath).catch(() => void 0));
268
- } else if (entryStat.isDirectory()) {
269
- if (isProfileDirectory(sourceUserDataDir, entry)) continue;
270
- if (SKIPPED_ROOT_DIRECTORIES.has(entry)) continue;
271
- copyTasks.push(
272
- cp(sourcePath, targetPath, { recursive: true }).catch(
273
- () => void 0
274
- )
275
- );
276
- }
152
+ const startTicks = parseLinuxProcessStartTicks(statRaw);
153
+ if (startTicks === null) {
154
+ return null;
277
155
  }
278
- await Promise.all(copyTasks);
279
- }
280
- async function writePersistentProfileMetadata(userDataDir, metadata) {
281
- await writeFile(
282
- join(userDataDir, OPENSTEER_META_FILE),
283
- JSON.stringify(metadata, null, 2)
156
+ const [bootTimeMs, clockTicksPerSecond] = await Promise.all([
157
+ readLinuxBootTimeMs(),
158
+ readLinuxClockTicksPerSecond()
159
+ ]);
160
+ if (bootTimeMs === null || clockTicksPerSecond === null) {
161
+ return null;
162
+ }
163
+ return Math.floor(
164
+ bootTimeMs + startTicks * 1e3 / clockTicksPerSecond
284
165
  );
285
166
  }
286
- function buildPersistentProfileMetadata(sourceUserDataDir, profileDirectory) {
287
- return {
288
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
289
- profileDirectory,
290
- source: sourceUserDataDir
291
- };
292
- }
293
- async function createPersistentProfileClone(sourceUserDataDir, sourceProfileDir, targetUserDataDir, profileDirectory, metadata) {
294
- if (existsSync(targetUserDataDir)) {
295
- return false;
167
+ function parseLinuxProcessStartTicks(statRaw) {
168
+ const closingParenIndex = statRaw.lastIndexOf(")");
169
+ if (closingParenIndex === -1) {
170
+ return null;
296
171
  }
297
- const tempUserDataDir = await mkdtemp(
298
- buildPersistentProfileTempDirPrefix(targetUserDataDir)
299
- );
300
- let published = false;
172
+ const fields = statRaw.slice(closingParenIndex + 2).trim().split(/\s+/);
173
+ const startTicks = Number(fields[LINUX_STAT_START_TIME_FIELD_INDEX]);
174
+ return Number.isFinite(startTicks) && startTicks >= 0 ? startTicks : null;
175
+ }
176
+ async function readLinuxBootTimeMs() {
301
177
  try {
302
- await materializePersistentProfileSnapshot(
303
- sourceUserDataDir,
304
- sourceProfileDir,
305
- tempUserDataDir,
306
- profileDirectory,
307
- metadata
178
+ const statRaw = await readFile("/proc/stat", "utf8");
179
+ const bootTimeLine = statRaw.split("\n").find((line) => line.startsWith("btime "));
180
+ if (!bootTimeLine) {
181
+ return null;
182
+ }
183
+ const bootTimeSeconds = Number.parseInt(
184
+ bootTimeLine.slice("btime ".length),
185
+ 10
308
186
  );
309
- try {
310
- await rename(tempUserDataDir, targetUserDataDir);
311
- } catch (error) {
312
- if (wasDirPublishedByAnotherProcess(error, targetUserDataDir)) {
313
- return false;
187
+ return Number.isFinite(bootTimeSeconds) ? bootTimeSeconds * 1e3 : null;
188
+ } catch {
189
+ return null;
190
+ }
191
+ }
192
+ async function readLinuxClockTicksPerSecond() {
193
+ if (!linuxClockTicksPerSecondPromise) {
194
+ linuxClockTicksPerSecondPromise = execFileAsync("getconf", ["CLK_TCK"], {
195
+ encoding: "utf8",
196
+ maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES
197
+ }).then(({ stdout }) => {
198
+ const value = Number.parseInt(stdout.trim(), 10);
199
+ return Number.isFinite(value) && value > 0 ? value : null;
200
+ }).catch(() => null);
201
+ }
202
+ return await linuxClockTicksPerSecondPromise;
203
+ }
204
+ async function readWindowsProcessStartedAtMs(pid) {
205
+ try {
206
+ const { stdout } = await execFileAsync(
207
+ "powershell.exe",
208
+ [
209
+ "-NoProfile",
210
+ "-Command",
211
+ `(Get-Process -Id ${pid}).StartTime.ToUniversalTime().ToString("o")`
212
+ ],
213
+ {
214
+ encoding: "utf8",
215
+ maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES
314
216
  }
315
- throw error;
217
+ );
218
+ const isoTimestamp = stdout.trim();
219
+ if (!isoTimestamp) {
220
+ return null;
316
221
  }
317
- published = true;
318
- return true;
222
+ const startedAtMs = Date.parse(isoTimestamp);
223
+ return Number.isFinite(startedAtMs) ? startedAtMs : null;
224
+ } catch {
225
+ return null;
226
+ }
227
+ }
228
+ async function readPsProcessStartedAtMs(pid) {
229
+ try {
230
+ const { stdout } = await execFileAsync(
231
+ "ps",
232
+ ["-o", "lstart=", "-p", String(pid)],
233
+ {
234
+ encoding: "utf8",
235
+ env: PS_COMMAND_ENV,
236
+ maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES
237
+ }
238
+ );
239
+ const startedAt = stdout.trim();
240
+ if (!startedAt) {
241
+ return null;
242
+ }
243
+ const startedAtMs = Date.parse(startedAt.replace(/\s+/g, " "));
244
+ return Number.isFinite(startedAtMs) ? startedAtMs : null;
245
+ } catch {
246
+ return null;
247
+ }
248
+ }
249
+
250
+ // src/browser/dir-lock.ts
251
+ var LOCK_OWNER_FILE = "owner.json";
252
+ var LOCK_RECLAIMER_DIR = "reclaimer";
253
+ var LOCK_RETRY_DELAY_MS = 50;
254
+ async function withDirLock(lockDirPath, action) {
255
+ const releaseLock = await acquireDirLock(lockDirPath);
256
+ try {
257
+ return await action();
319
258
  } finally {
320
- if (!published) {
321
- await rm(tempUserDataDir, {
259
+ await releaseLock();
260
+ }
261
+ }
262
+ async function acquireDirLock(lockDirPath) {
263
+ while (true) {
264
+ const releaseLock = await tryAcquireDirLock(lockDirPath);
265
+ if (releaseLock) {
266
+ return releaseLock;
267
+ }
268
+ await sleep(LOCK_RETRY_DELAY_MS);
269
+ }
270
+ }
271
+ async function tryAcquireDirLock(lockDirPath) {
272
+ await mkdir(dirname(lockDirPath), { recursive: true });
273
+ while (true) {
274
+ const tempLockDirPath = `${lockDirPath}-${process.pid}-${CURRENT_PROCESS_OWNER.processStartedAtMs}-${randomUUID()}`;
275
+ try {
276
+ await mkdir(tempLockDirPath);
277
+ await writeLockOwner(tempLockDirPath, CURRENT_PROCESS_OWNER);
278
+ try {
279
+ await rename(tempLockDirPath, lockDirPath);
280
+ break;
281
+ } catch (error) {
282
+ if (!wasDirPublishedByAnotherProcess(error, lockDirPath)) {
283
+ throw error;
284
+ }
285
+ }
286
+ } finally {
287
+ await rm(tempLockDirPath, {
322
288
  recursive: true,
323
289
  force: true
324
290
  }).catch(() => void 0);
325
291
  }
292
+ const owner = await readLockOwner(lockDirPath);
293
+ if ((!owner || await getProcessLiveness(owner) === "dead") && await tryReclaimStaleLock(lockDirPath, owner)) {
294
+ continue;
295
+ }
296
+ return null;
326
297
  }
298
+ return async () => {
299
+ await rm(lockDirPath, {
300
+ recursive: true,
301
+ force: true
302
+ }).catch(() => void 0);
303
+ };
327
304
  }
328
- async function materializePersistentProfileSnapshot(sourceUserDataDir, sourceProfileDir, targetUserDataDir, profileDirectory, metadata) {
329
- if (!existsSync(sourceProfileDir)) {
330
- throw new Error(
331
- `Chrome profile "${profileDirectory}" was not found in "${sourceUserDataDir}".`
332
- );
305
+ async function isDirLockHeld(lockDirPath) {
306
+ if (!existsSync(lockDirPath)) {
307
+ return false;
333
308
  }
334
- await cp(sourceProfileDir, join(targetUserDataDir, profileDirectory), {
335
- recursive: true
336
- });
337
- await copyRootLevelEntries(sourceUserDataDir, targetUserDataDir, profileDirectory);
338
- await writePersistentProfileMetadata(targetUserDataDir, metadata);
309
+ const owner = await readLockOwner(lockDirPath);
310
+ if ((!owner || await getProcessLiveness(owner) === "dead") && await tryReclaimStaleLock(lockDirPath, owner)) {
311
+ return false;
312
+ }
313
+ return existsSync(lockDirPath);
339
314
  }
340
- async function mergePersistentProfileSnapshot(runtimeUserDataDir, persistentUserDataDir, metadata, runtimeMetadata) {
341
- const tempUserDataDir = await mkdtemp(
342
- buildPersistentProfileTempDirPrefix(persistentUserDataDir)
315
+ function getErrorCode(error) {
316
+ return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
317
+ }
318
+ function wasDirPublishedByAnotherProcess(error, targetDirPath) {
319
+ const code = getErrorCode(error);
320
+ return existsSync(targetDirPath) && (code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM");
321
+ }
322
+ async function writeLockOwner(lockDirPath, owner) {
323
+ await writeFile(join(lockDirPath, LOCK_OWNER_FILE), JSON.stringify(owner));
324
+ }
325
+ async function readLockOwner(lockDirPath) {
326
+ return await readLockParticipant(join(lockDirPath, LOCK_OWNER_FILE));
327
+ }
328
+ async function readLockParticipant(filePath) {
329
+ return (await readLockParticipantRecord(filePath)).owner;
330
+ }
331
+ async function readLockParticipantRecord(filePath) {
332
+ try {
333
+ const raw = await readFile2(filePath, "utf8");
334
+ const owner = parseProcessOwner(JSON.parse(raw));
335
+ return {
336
+ exists: true,
337
+ owner
338
+ };
339
+ } catch (error) {
340
+ return {
341
+ exists: getErrorCode(error) !== "ENOENT",
342
+ owner: null
343
+ };
344
+ }
345
+ }
346
+ async function readLockReclaimerRecord(lockDirPath) {
347
+ return await readLockParticipantRecord(
348
+ join(buildLockReclaimerDirPath(lockDirPath), LOCK_OWNER_FILE)
343
349
  );
344
- let published = false;
350
+ }
351
+ async function tryReclaimStaleLock(lockDirPath, expectedOwner) {
352
+ if (!await tryAcquireLockReclaimer(lockDirPath)) {
353
+ return false;
354
+ }
355
+ let reclaimed = false;
345
356
  try {
346
- const baseEntries = deserializeSnapshotManifestEntries(
347
- runtimeMetadata.baseEntries
348
- );
349
- const currentEntries = await collectPersistentSnapshotEntries(
350
- persistentUserDataDir,
351
- metadata.profileDirectory
352
- );
353
- const runtimeEntries = await collectPersistentSnapshotEntries(
354
- runtimeUserDataDir,
355
- metadata.profileDirectory
356
- );
357
- const mergedEntries = resolveMergedSnapshotEntries(
358
- baseEntries,
359
- currentEntries,
360
- runtimeEntries
361
- );
362
- await materializeMergedPersistentProfileSnapshot(
363
- tempUserDataDir,
364
- currentEntries,
365
- runtimeEntries,
366
- mergedEntries
367
- );
368
- await writePersistentProfileMetadata(tempUserDataDir, metadata);
369
- await replaceProfileDirectory(persistentUserDataDir, tempUserDataDir);
370
- published = true;
357
+ const owner = await readLockOwner(lockDirPath);
358
+ if (!processOwnersEqual(owner, expectedOwner)) {
359
+ return false;
360
+ }
361
+ if (owner && await getProcessLiveness(owner) !== "dead") {
362
+ return false;
363
+ }
364
+ await rm(lockDirPath, {
365
+ recursive: true,
366
+ force: true
367
+ }).catch(() => void 0);
368
+ reclaimed = !existsSync(lockDirPath);
369
+ return reclaimed;
371
370
  } finally {
372
- if (!published) {
373
- await rm(tempUserDataDir, {
371
+ if (!reclaimed) {
372
+ await rm(buildLockReclaimerDirPath(lockDirPath), {
374
373
  recursive: true,
375
374
  force: true
376
375
  }).catch(() => void 0);
377
376
  }
378
377
  }
379
378
  }
380
- async function buildRuntimeProfileMetadata(runtimeUserDataDir, profileDirectory) {
381
- if (!profileDirectory) {
382
- return {
383
- baseEntries: {},
384
- profileDirectory: null
385
- };
379
+ async function tryAcquireLockReclaimer(lockDirPath) {
380
+ const reclaimerDirPath = buildLockReclaimerDirPath(lockDirPath);
381
+ while (true) {
382
+ const tempReclaimerDirPath = `${reclaimerDirPath}-${process.pid}-${CURRENT_PROCESS_OWNER.processStartedAtMs}-${randomUUID()}`;
383
+ try {
384
+ await mkdir(tempReclaimerDirPath);
385
+ await writeLockOwner(tempReclaimerDirPath, CURRENT_PROCESS_OWNER);
386
+ try {
387
+ await rename(tempReclaimerDirPath, reclaimerDirPath);
388
+ return true;
389
+ } catch (error) {
390
+ if (getErrorCode(error) === "ENOENT") {
391
+ return false;
392
+ }
393
+ if (!wasDirPublishedByAnotherProcess(error, reclaimerDirPath)) {
394
+ throw error;
395
+ }
396
+ }
397
+ } catch (error) {
398
+ const code = getErrorCode(error);
399
+ if (code === "ENOENT") {
400
+ return false;
401
+ }
402
+ throw error;
403
+ } finally {
404
+ await rm(tempReclaimerDirPath, {
405
+ recursive: true,
406
+ force: true
407
+ }).catch(() => void 0);
408
+ }
409
+ const reclaimerRecord = await readLockReclaimerRecord(lockDirPath);
410
+ if (!reclaimerRecord.exists || !reclaimerRecord.owner) {
411
+ return false;
412
+ }
413
+ if (await getProcessLiveness(reclaimerRecord.owner) !== "dead") {
414
+ return false;
415
+ }
416
+ await rm(reclaimerDirPath, {
417
+ recursive: true,
418
+ force: true
419
+ }).catch(() => void 0);
386
420
  }
387
- const baseEntries = await collectPersistentSnapshotEntries(
388
- runtimeUserDataDir,
389
- profileDirectory
421
+ }
422
+ function buildLockReclaimerDirPath(lockDirPath) {
423
+ return join(lockDirPath, LOCK_RECLAIMER_DIR);
424
+ }
425
+ async function sleep(ms) {
426
+ await new Promise((resolve) => setTimeout(resolve, ms));
427
+ }
428
+
429
+ // src/browser/persistent-profile-coordination.ts
430
+ var PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS = 50;
431
+ async function withPersistentProfileControlLock(targetUserDataDir, action) {
432
+ return await withDirLock(
433
+ buildPersistentProfileControlLockDirPath(targetUserDataDir),
434
+ action
390
435
  );
391
- return {
392
- baseEntries: serializeSnapshotManifestEntries(baseEntries),
393
- profileDirectory
394
- };
395
436
  }
396
- async function writeRuntimeProfileMetadata(userDataDir, metadata) {
397
- await writeFile(
398
- join(userDataDir, OPENSTEER_RUNTIME_META_FILE),
399
- JSON.stringify(metadata, null, 2)
437
+ async function acquirePersistentProfileWriteLock(targetUserDataDir) {
438
+ const controlLockDirPath = buildPersistentProfileControlLockDirPath(targetUserDataDir);
439
+ const writeLockDirPath = buildPersistentProfileWriteLockDirPath(
440
+ targetUserDataDir
441
+ );
442
+ while (true) {
443
+ let releaseWriteLock = null;
444
+ const releaseControlLock = await acquireDirLock(controlLockDirPath);
445
+ try {
446
+ releaseWriteLock = await tryAcquireDirLock(writeLockDirPath);
447
+ } finally {
448
+ await releaseControlLock();
449
+ }
450
+ if (releaseWriteLock) {
451
+ return releaseWriteLock;
452
+ }
453
+ await sleep2(PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS);
454
+ }
455
+ }
456
+ async function isPersistentProfileWriteLocked(targetUserDataDir) {
457
+ return await isDirLockHeld(
458
+ buildPersistentProfileWriteLockDirPath(targetUserDataDir)
459
+ );
460
+ }
461
+ function buildPersistentProfileWriteLockDirPath(targetUserDataDir) {
462
+ return join2(dirname2(targetUserDataDir), `${basename(targetUserDataDir)}.lock`);
463
+ }
464
+ function buildPersistentProfileControlLockDirPath(targetUserDataDir) {
465
+ return join2(
466
+ dirname2(targetUserDataDir),
467
+ `${basename(targetUserDataDir)}.control.lock`
468
+ );
469
+ }
470
+ async function sleep2(ms) {
471
+ await new Promise((resolve) => setTimeout(resolve, ms));
472
+ }
473
+
474
+ // src/browser/shared-real-browser-session-state.ts
475
+ import { randomUUID as randomUUID2 } from "crypto";
476
+ import { existsSync as existsSync2 } from "fs";
477
+ import { mkdir as mkdir2, readFile as readFile3, readdir, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
478
+ import { basename as basename2, dirname as dirname3, join as join3 } from "path";
479
+ var SHARED_SESSION_METADATA_FILE = "session.json";
480
+ var SHARED_SESSION_CLIENTS_DIR = "clients";
481
+ var SHARED_SESSION_RETRY_DELAY_MS = 50;
482
+ var SHARED_SESSION_METADATA_TEMP_FILE_PREFIX = `${SHARED_SESSION_METADATA_FILE}.`;
483
+ var SHARED_SESSION_METADATA_TEMP_FILE_SUFFIX = ".tmp";
484
+ function buildSharedSessionDirPath(persistentUserDataDir) {
485
+ return join3(
486
+ dirname3(persistentUserDataDir),
487
+ `${basename2(persistentUserDataDir)}.session`
488
+ );
489
+ }
490
+ function buildSharedSessionLockPath(persistentUserDataDir) {
491
+ return `${buildSharedSessionDirPath(persistentUserDataDir)}.lock`;
492
+ }
493
+ function buildSharedSessionClientsDirPath(persistentUserDataDir) {
494
+ return join3(
495
+ buildSharedSessionDirPath(persistentUserDataDir),
496
+ SHARED_SESSION_CLIENTS_DIR
497
+ );
498
+ }
499
+ function buildSharedSessionClientPath(persistentUserDataDir, clientId) {
500
+ return join3(
501
+ buildSharedSessionClientsDirPath(persistentUserDataDir),
502
+ `${clientId}.json`
503
+ );
504
+ }
505
+ async function readSharedSessionMetadata(persistentUserDataDir) {
506
+ return (await readSharedSessionMetadataRecord(persistentUserDataDir)).metadata;
507
+ }
508
+ async function writeSharedSessionMetadata(persistentUserDataDir, metadata) {
509
+ const sessionDirPath = buildSharedSessionDirPath(persistentUserDataDir);
510
+ const metadataPath = buildSharedSessionMetadataPath(persistentUserDataDir);
511
+ const tempPath = buildSharedSessionMetadataTempPath(sessionDirPath);
512
+ await mkdir2(sessionDirPath, { recursive: true });
513
+ try {
514
+ await writeFile2(tempPath, JSON.stringify(metadata, null, 2));
515
+ await rename2(tempPath, metadataPath);
516
+ } finally {
517
+ await rm2(tempPath, { force: true }).catch(() => void 0);
518
+ }
519
+ }
520
+ async function hasLiveSharedRealBrowserSession(persistentUserDataDir) {
521
+ const sessionDirPath = buildSharedSessionDirPath(persistentUserDataDir);
522
+ const metadataRecord = await readSharedSessionMetadataRecord(
523
+ persistentUserDataDir
524
+ );
525
+ if (!metadataRecord.exists) {
526
+ return await hasLiveSharedSessionPublisherOrClients(sessionDirPath);
527
+ }
528
+ if (!metadataRecord.metadata) {
529
+ return true;
530
+ }
531
+ if (await getProcessLiveness(metadataRecord.metadata.browserOwner) === "dead") {
532
+ await rm2(sessionDirPath, {
533
+ force: true,
534
+ recursive: true
535
+ }).catch(() => void 0);
536
+ return false;
537
+ }
538
+ return true;
539
+ }
540
+ async function waitForSharedRealBrowserSessionToDrain(persistentUserDataDir) {
541
+ while (true) {
542
+ if (!await hasLiveSharedRealBrowserSession(persistentUserDataDir)) {
543
+ return;
544
+ }
545
+ await sleep3(SHARED_SESSION_RETRY_DELAY_MS);
546
+ }
547
+ }
548
+ async function readSharedSessionMetadataRecord(persistentUserDataDir) {
549
+ try {
550
+ const raw = await readFile3(
551
+ buildSharedSessionMetadataPath(persistentUserDataDir),
552
+ "utf8"
553
+ );
554
+ return {
555
+ exists: true,
556
+ metadata: parseSharedSessionMetadata(JSON.parse(raw))
557
+ };
558
+ } catch (error) {
559
+ return {
560
+ exists: getErrorCode2(error) !== "ENOENT",
561
+ metadata: null
562
+ };
563
+ }
564
+ }
565
+ async function hasLiveSharedSessionPublisherOrClients(sessionDirPath) {
566
+ if (!existsSync2(sessionDirPath)) {
567
+ return false;
568
+ }
569
+ let entries;
570
+ try {
571
+ entries = await readDirNames(sessionDirPath);
572
+ } catch (error) {
573
+ return getErrorCode2(error) !== "ENOENT";
574
+ }
575
+ let hasUnknownEntries = false;
576
+ for (const entry of entries) {
577
+ if (entry === SHARED_SESSION_METADATA_FILE) {
578
+ return true;
579
+ }
580
+ if (entry === SHARED_SESSION_CLIENTS_DIR) {
581
+ if (await hasDirectoryEntries(join3(sessionDirPath, entry))) {
582
+ return true;
583
+ }
584
+ continue;
585
+ }
586
+ const owner = parseSharedSessionMetadataTempOwner(entry);
587
+ if (!owner) {
588
+ if (isSharedSessionMetadataTempFile(entry)) {
589
+ continue;
590
+ }
591
+ hasUnknownEntries = true;
592
+ continue;
593
+ }
594
+ if (await getProcessLiveness(owner) !== "dead") {
595
+ return true;
596
+ }
597
+ }
598
+ if (hasUnknownEntries) {
599
+ return true;
600
+ }
601
+ await rm2(sessionDirPath, {
602
+ force: true,
603
+ recursive: true
604
+ }).catch(() => void 0);
605
+ return false;
606
+ }
607
+ function buildSharedSessionMetadataPath(persistentUserDataDir) {
608
+ return join3(
609
+ buildSharedSessionDirPath(persistentUserDataDir),
610
+ SHARED_SESSION_METADATA_FILE
611
+ );
612
+ }
613
+ function buildSharedSessionMetadataTempPath(sessionDirPath) {
614
+ return join3(
615
+ sessionDirPath,
616
+ [
617
+ SHARED_SESSION_METADATA_FILE,
618
+ CURRENT_PROCESS_OWNER.pid,
619
+ CURRENT_PROCESS_OWNER.processStartedAtMs,
620
+ randomUUID2(),
621
+ "tmp"
622
+ ].join(".")
623
+ );
624
+ }
625
+ function parseSharedSessionMetadata(value) {
626
+ if (!value || typeof value !== "object") {
627
+ return null;
628
+ }
629
+ const parsed = value;
630
+ const browserOwner = parseProcessOwner(parsed.browserOwner);
631
+ const stateOwner = parseProcessOwner(parsed.stateOwner);
632
+ const state = parsed.state === "launching" || parsed.state === "ready" || parsed.state === "closing" ? parsed.state : null;
633
+ if (!browserOwner || !stateOwner || typeof parsed.createdAt !== "string" || typeof parsed.debugPort !== "number" || typeof parsed.executablePath !== "string" || typeof parsed.headless !== "boolean" || typeof parsed.persistentUserDataDir !== "string" || typeof parsed.profileDirectory !== "string" || typeof parsed.sessionId !== "string" || !state) {
634
+ return null;
635
+ }
636
+ return {
637
+ browserOwner,
638
+ createdAt: parsed.createdAt,
639
+ debugPort: parsed.debugPort,
640
+ executablePath: parsed.executablePath,
641
+ headless: parsed.headless,
642
+ persistentUserDataDir: parsed.persistentUserDataDir,
643
+ profileDirectory: parsed.profileDirectory,
644
+ sessionId: parsed.sessionId,
645
+ state,
646
+ stateOwner
647
+ };
648
+ }
649
+ function parseSharedSessionMetadataTempOwner(entryName) {
650
+ if (!isSharedSessionMetadataTempFile(entryName)) {
651
+ return null;
652
+ }
653
+ const segments = entryName.split(".");
654
+ if (segments.length < 5) {
655
+ return null;
656
+ }
657
+ return parseProcessOwner({
658
+ pid: Number.parseInt(segments[2] ?? "", 10),
659
+ processStartedAtMs: Number.parseInt(segments[3] ?? "", 10)
660
+ });
661
+ }
662
+ function isSharedSessionMetadataTempFile(entryName) {
663
+ return entryName.startsWith(SHARED_SESSION_METADATA_TEMP_FILE_PREFIX) && entryName.endsWith(SHARED_SESSION_METADATA_TEMP_FILE_SUFFIX);
664
+ }
665
+ function getErrorCode2(error) {
666
+ return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
667
+ }
668
+ async function hasDirectoryEntries(dirPath) {
669
+ try {
670
+ return (await readDirNames(dirPath)).length > 0;
671
+ } catch (error) {
672
+ return getErrorCode2(error) !== "ENOENT";
673
+ }
674
+ }
675
+ async function readDirNames(dirPath) {
676
+ return await readdir(dirPath, { encoding: "utf8" });
677
+ }
678
+ async function sleep3(ms) {
679
+ await new Promise((resolve) => setTimeout(resolve, ms));
680
+ }
681
+
682
+ // src/browser/persistent-profile.ts
683
+ var execFileAsync2 = promisify2(execFile2);
684
+ var OPENSTEER_META_FILE = ".opensteer-meta.json";
685
+ var OPENSTEER_RUNTIME_META_FILE = ".opensteer-runtime.json";
686
+ var OPENSTEER_RUNTIME_CREATING_FILE = ".opensteer-runtime-creating.json";
687
+ var PROCESS_LIST_MAX_BUFFER_BYTES2 = 16 * 1024 * 1024;
688
+ var PS_COMMAND_ENV2 = { ...process.env, LC_ALL: "C" };
689
+ var CHROME_SINGLETON_ENTRIES = /* @__PURE__ */ new Set([
690
+ "SingletonCookie",
691
+ "SingletonLock",
692
+ "SingletonSocket",
693
+ "DevToolsActivePort",
694
+ "lockfile"
695
+ ]);
696
+ var COPY_SKIP_ENTRIES = /* @__PURE__ */ new Set([
697
+ ...CHROME_SINGLETON_ENTRIES,
698
+ OPENSTEER_META_FILE,
699
+ OPENSTEER_RUNTIME_META_FILE,
700
+ OPENSTEER_RUNTIME_CREATING_FILE
701
+ ]);
702
+ var SKIPPED_ROOT_DIRECTORIES = /* @__PURE__ */ new Set([
703
+ "Crash Reports",
704
+ "Crashpad",
705
+ "BrowserMetrics",
706
+ "GrShaderCache",
707
+ "ShaderCache",
708
+ "GraphiteDawnCache",
709
+ "component_crx_cache",
710
+ "Crowd Deny",
711
+ "hyphen-data",
712
+ "OnDeviceHeadSuggestModel",
713
+ "OptimizationGuidePredictionModels",
714
+ "Segmentation Platform",
715
+ "SmartCardDeviceNames",
716
+ "WidevineCdm",
717
+ "pnacl"
718
+ ]);
719
+ async function getOrCreatePersistentProfile(sourceUserDataDir, profileDirectory, profilesRootDir = defaultPersistentProfilesRootDir()) {
720
+ const resolvedSourceUserDataDir = expandHome(sourceUserDataDir);
721
+ const targetUserDataDir = join4(
722
+ expandHome(profilesRootDir),
723
+ buildPersistentProfileKey(resolvedSourceUserDataDir, profileDirectory)
724
+ );
725
+ const sourceProfileDir = join4(resolvedSourceUserDataDir, profileDirectory);
726
+ const metadata = buildPersistentProfileMetadata(
727
+ resolvedSourceUserDataDir,
728
+ profileDirectory
729
+ );
730
+ await mkdir3(dirname4(targetUserDataDir), { recursive: true });
731
+ if (await isHealthyPersistentProfile(
732
+ targetUserDataDir,
733
+ resolvedSourceUserDataDir,
734
+ profileDirectory
735
+ ) && !await isPersistentProfileWriteLocked(targetUserDataDir)) {
736
+ return {
737
+ created: false,
738
+ userDataDir: targetUserDataDir
739
+ };
740
+ }
741
+ return await withPersistentProfileWriteAccess(targetUserDataDir, async () => {
742
+ await recoverPersistentProfileBackup(targetUserDataDir);
743
+ await cleanOrphanedOwnedDirs(
744
+ dirname4(targetUserDataDir),
745
+ buildPersistentProfileTempDirNamePrefix(targetUserDataDir)
746
+ );
747
+ if (!existsSync3(sourceProfileDir)) {
748
+ throw new Error(
749
+ `Chrome profile "${profileDirectory}" was not found in "${resolvedSourceUserDataDir}".`
750
+ );
751
+ }
752
+ const created = await createPersistentProfileClone(
753
+ resolvedSourceUserDataDir,
754
+ sourceProfileDir,
755
+ targetUserDataDir,
756
+ profileDirectory,
757
+ metadata
758
+ );
759
+ await ensurePersistentProfileMetadata(targetUserDataDir, metadata);
760
+ return {
761
+ created,
762
+ userDataDir: targetUserDataDir
763
+ };
764
+ });
765
+ }
766
+ async function clearPersistentProfileSingletons(userDataDir) {
767
+ await Promise.all(
768
+ [...CHROME_SINGLETON_ENTRIES].map(
769
+ (entry) => rm3(join4(userDataDir, entry), {
770
+ force: true,
771
+ recursive: true
772
+ }).catch(() => void 0)
773
+ )
774
+ );
775
+ }
776
+ async function createIsolatedRuntimeProfile(sourceUserDataDir, runtimesRootDir = defaultRuntimeProfilesRootDir()) {
777
+ const resolvedSourceUserDataDir = expandHome(sourceUserDataDir);
778
+ const runtimeRootDir = expandHome(runtimesRootDir);
779
+ await mkdir3(runtimeRootDir, { recursive: true });
780
+ const sourceMetadata = await requirePersistentProfileMetadata(
781
+ resolvedSourceUserDataDir
782
+ );
783
+ const runtimeProfile = await reserveRuntimeProfileCreation(
784
+ resolvedSourceUserDataDir,
785
+ runtimeRootDir,
786
+ sourceMetadata.profileDirectory
787
+ );
788
+ try {
789
+ await cleanOrphanedRuntimeProfileDirs(
790
+ runtimeRootDir,
791
+ buildRuntimeProfileDirNamePrefix(resolvedSourceUserDataDir)
792
+ );
793
+ await copyUserDataDirSnapshot(
794
+ resolvedSourceUserDataDir,
795
+ runtimeProfile.userDataDir
796
+ );
797
+ const currentSourceMetadata = await readPersistentProfileMetadata(
798
+ resolvedSourceUserDataDir
799
+ );
800
+ await writeRuntimeProfileMetadata(
801
+ runtimeProfile.userDataDir,
802
+ await buildRuntimeProfileMetadata(
803
+ runtimeProfile.userDataDir,
804
+ resolvedSourceUserDataDir,
805
+ currentSourceMetadata?.profileDirectory ?? sourceMetadata.profileDirectory
806
+ )
807
+ );
808
+ await clearRuntimeProfileCreationState(
809
+ runtimeProfile.userDataDir,
810
+ resolvedSourceUserDataDir
811
+ );
812
+ return {
813
+ persistentUserDataDir: resolvedSourceUserDataDir,
814
+ userDataDir: runtimeProfile.userDataDir
815
+ };
816
+ } catch (error) {
817
+ await clearRuntimeProfileCreationState(
818
+ runtimeProfile.userDataDir,
819
+ resolvedSourceUserDataDir
820
+ );
821
+ await rm3(runtimeProfile.userDataDir, {
822
+ recursive: true,
823
+ force: true
824
+ }).catch(() => void 0);
825
+ throw error;
826
+ }
827
+ }
828
+ async function persistIsolatedRuntimeProfile(runtimeUserDataDir, persistentUserDataDir) {
829
+ const resolvedRuntimeUserDataDir = expandHome(runtimeUserDataDir);
830
+ const resolvedPersistentUserDataDir = expandHome(persistentUserDataDir);
831
+ let claimedRuntimeUserDataDir = null;
832
+ try {
833
+ await withPersistentProfileWriteAccess(
834
+ resolvedPersistentUserDataDir,
835
+ async () => {
836
+ await mkdir3(dirname4(resolvedPersistentUserDataDir), {
837
+ recursive: true
838
+ });
839
+ await recoverPersistentProfileBackup(resolvedPersistentUserDataDir);
840
+ await cleanOrphanedOwnedDirs(
841
+ dirname4(resolvedPersistentUserDataDir),
842
+ buildPersistentProfileTempDirNamePrefix(
843
+ resolvedPersistentUserDataDir
844
+ )
845
+ );
846
+ const metadata = await requirePersistentProfileMetadata(
847
+ resolvedPersistentUserDataDir
848
+ );
849
+ claimedRuntimeUserDataDir = await claimRuntimeProfileForPersist(
850
+ resolvedRuntimeUserDataDir
851
+ );
852
+ const runtimeMetadata = await requireRuntimeProfileMetadata(
853
+ claimedRuntimeUserDataDir,
854
+ resolvedPersistentUserDataDir,
855
+ metadata.profileDirectory,
856
+ resolvedRuntimeUserDataDir
857
+ );
858
+ await mergePersistentProfileSnapshot(
859
+ claimedRuntimeUserDataDir,
860
+ resolvedPersistentUserDataDir,
861
+ metadata,
862
+ runtimeMetadata
863
+ );
864
+ }
865
+ );
866
+ } catch (error) {
867
+ if (claimedRuntimeUserDataDir) {
868
+ try {
869
+ await restoreClaimedRuntimeProfile(
870
+ claimedRuntimeUserDataDir,
871
+ resolvedRuntimeUserDataDir
872
+ );
873
+ } catch (restoreError) {
874
+ throw new AggregateError(
875
+ [error, restoreError],
876
+ `Failed to restore runtime profile "${resolvedRuntimeUserDataDir}" after persistence failed.`
877
+ );
878
+ }
879
+ }
880
+ throw error;
881
+ }
882
+ if (claimedRuntimeUserDataDir) {
883
+ await rm3(claimedRuntimeUserDataDir, {
884
+ recursive: true,
885
+ force: true
886
+ }).catch(() => void 0);
887
+ }
888
+ }
889
+ function buildPersistentProfileKey(sourceUserDataDir, profileDirectory) {
890
+ const hash = createHash("sha256").update(`${sourceUserDataDir}\0${profileDirectory}`).digest("hex").slice(0, 16);
891
+ const sourceLabel = sanitizePathSegment(basename3(sourceUserDataDir) || "user-data");
892
+ const profileLabel = sanitizePathSegment(profileDirectory || "Default");
893
+ return `${sourceLabel}-${profileLabel}-${hash}`;
894
+ }
895
+ function defaultPersistentProfilesRootDir() {
896
+ return join4(homedir(), ".opensteer", "real-browser-profiles");
897
+ }
898
+ function defaultRuntimeProfilesRootDir() {
899
+ return join4(tmpdir(), "opensteer-real-browser-runtimes");
900
+ }
901
+ function sanitizePathSegment(value) {
902
+ const sanitized = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/-+/g, "-");
903
+ return sanitized.replace(/^-|-$/g, "") || "profile";
904
+ }
905
+ function isProfileDirectory(userDataDir, entry) {
906
+ return existsSync3(join4(userDataDir, entry, "Preferences"));
907
+ }
908
+ async function copyUserDataDirSnapshot(sourceUserDataDir, targetUserDataDir) {
909
+ await cp(sourceUserDataDir, targetUserDataDir, {
910
+ recursive: true,
911
+ filter: (candidatePath) => shouldCopyRuntimeSnapshotEntry(sourceUserDataDir, candidatePath)
912
+ });
913
+ }
914
+ function shouldCopyRuntimeSnapshotEntry(userDataDir, candidatePath) {
915
+ const candidateRelativePath = relative(userDataDir, candidatePath);
916
+ if (!candidateRelativePath) {
917
+ return true;
918
+ }
919
+ const segments = candidateRelativePath.split(sep).filter(Boolean);
920
+ if (segments.length !== 1) {
921
+ return true;
922
+ }
923
+ return !COPY_SKIP_ENTRIES.has(segments[0]);
924
+ }
925
+ async function copyRootLevelEntries(sourceUserDataDir, targetUserDataDir, targetProfileDirectory) {
926
+ let entries;
927
+ try {
928
+ entries = await readdir2(sourceUserDataDir);
929
+ } catch {
930
+ return;
931
+ }
932
+ const copyTasks = [];
933
+ for (const entry of entries) {
934
+ if (COPY_SKIP_ENTRIES.has(entry)) continue;
935
+ if (entry === targetProfileDirectory) continue;
936
+ const sourcePath = join4(sourceUserDataDir, entry);
937
+ const targetPath = join4(targetUserDataDir, entry);
938
+ if (existsSync3(targetPath)) continue;
939
+ let entryStat;
940
+ try {
941
+ entryStat = await stat(sourcePath);
942
+ } catch {
943
+ continue;
944
+ }
945
+ if (entryStat.isFile()) {
946
+ copyTasks.push(copyFile(sourcePath, targetPath).catch(() => void 0));
947
+ } else if (entryStat.isDirectory()) {
948
+ if (isProfileDirectory(sourceUserDataDir, entry)) continue;
949
+ if (SKIPPED_ROOT_DIRECTORIES.has(entry)) continue;
950
+ copyTasks.push(
951
+ cp(sourcePath, targetPath, { recursive: true }).catch(
952
+ () => void 0
953
+ )
954
+ );
955
+ }
956
+ }
957
+ await Promise.all(copyTasks);
958
+ }
959
+ async function writePersistentProfileMetadata(userDataDir, metadata) {
960
+ await writeFile3(
961
+ join4(userDataDir, OPENSTEER_META_FILE),
962
+ JSON.stringify(metadata, null, 2)
963
+ );
964
+ }
965
+ function buildPersistentProfileMetadata(sourceUserDataDir, profileDirectory) {
966
+ return {
967
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
968
+ profileDirectory,
969
+ source: sourceUserDataDir
970
+ };
971
+ }
972
+ async function createPersistentProfileClone(sourceUserDataDir, sourceProfileDir, targetUserDataDir, profileDirectory, metadata) {
973
+ if (existsSync3(targetUserDataDir)) {
974
+ return false;
975
+ }
976
+ const tempUserDataDir = await mkdtemp(
977
+ buildPersistentProfileTempDirPrefix(targetUserDataDir)
978
+ );
979
+ let published = false;
980
+ try {
981
+ await materializePersistentProfileSnapshot(
982
+ sourceUserDataDir,
983
+ sourceProfileDir,
984
+ tempUserDataDir,
985
+ profileDirectory,
986
+ metadata
987
+ );
988
+ try {
989
+ await rename3(tempUserDataDir, targetUserDataDir);
990
+ } catch (error) {
991
+ if (wasDirPublishedByAnotherProcess2(error, targetUserDataDir)) {
992
+ return false;
993
+ }
994
+ throw error;
995
+ }
996
+ published = true;
997
+ return true;
998
+ } finally {
999
+ if (!published) {
1000
+ await rm3(tempUserDataDir, {
1001
+ recursive: true,
1002
+ force: true
1003
+ }).catch(() => void 0);
1004
+ }
1005
+ }
1006
+ }
1007
+ async function materializePersistentProfileSnapshot(sourceUserDataDir, sourceProfileDir, targetUserDataDir, profileDirectory, metadata) {
1008
+ if (!existsSync3(sourceProfileDir)) {
1009
+ throw new Error(
1010
+ `Chrome profile "${profileDirectory}" was not found in "${sourceUserDataDir}".`
1011
+ );
1012
+ }
1013
+ await cp(sourceProfileDir, join4(targetUserDataDir, profileDirectory), {
1014
+ recursive: true
1015
+ });
1016
+ await copyRootLevelEntries(sourceUserDataDir, targetUserDataDir, profileDirectory);
1017
+ await writePersistentProfileMetadata(targetUserDataDir, metadata);
1018
+ }
1019
+ async function mergePersistentProfileSnapshot(runtimeUserDataDir, persistentUserDataDir, metadata, runtimeMetadata) {
1020
+ const tempUserDataDir = await mkdtemp(
1021
+ buildPersistentProfileTempDirPrefix(persistentUserDataDir)
1022
+ );
1023
+ let published = false;
1024
+ try {
1025
+ const baseEntries = deserializeSnapshotManifestEntries(
1026
+ runtimeMetadata.baseEntries
1027
+ );
1028
+ const currentEntries = await collectPersistentSnapshotEntries(
1029
+ persistentUserDataDir,
1030
+ metadata.profileDirectory
1031
+ );
1032
+ const runtimeEntries = await collectPersistentSnapshotEntries(
1033
+ runtimeUserDataDir,
1034
+ metadata.profileDirectory
1035
+ );
1036
+ const mergedEntries = resolveMergedSnapshotEntries(
1037
+ baseEntries,
1038
+ currentEntries,
1039
+ runtimeEntries
1040
+ );
1041
+ await materializeMergedPersistentProfileSnapshot(
1042
+ tempUserDataDir,
1043
+ currentEntries,
1044
+ runtimeEntries,
1045
+ mergedEntries
1046
+ );
1047
+ await writePersistentProfileMetadata(tempUserDataDir, metadata);
1048
+ await replaceProfileDirectory(persistentUserDataDir, tempUserDataDir);
1049
+ published = true;
1050
+ } finally {
1051
+ if (!published) {
1052
+ await rm3(tempUserDataDir, {
1053
+ recursive: true,
1054
+ force: true
1055
+ }).catch(() => void 0);
1056
+ }
1057
+ }
1058
+ }
1059
+ async function buildRuntimeProfileMetadata(runtimeUserDataDir, persistentUserDataDir, profileDirectory) {
1060
+ const baseEntries = profileDirectory ? serializeSnapshotManifestEntries(
1061
+ await collectPersistentSnapshotEntries(
1062
+ runtimeUserDataDir,
1063
+ profileDirectory
1064
+ )
1065
+ ) : {};
1066
+ return {
1067
+ baseEntries,
1068
+ creator: CURRENT_PROCESS_OWNER,
1069
+ persistentUserDataDir,
1070
+ profileDirectory
1071
+ };
1072
+ }
1073
+ async function writeRuntimeProfileMetadata(userDataDir, metadata) {
1074
+ await writeFile3(
1075
+ join4(userDataDir, OPENSTEER_RUNTIME_META_FILE),
1076
+ JSON.stringify(metadata, null, 2)
1077
+ );
1078
+ }
1079
+ async function writeRuntimeProfileCreationMarker(userDataDir, marker) {
1080
+ await writeFile3(
1081
+ join4(userDataDir, OPENSTEER_RUNTIME_CREATING_FILE),
1082
+ JSON.stringify(marker, null, 2)
1083
+ );
1084
+ }
1085
+ async function readRuntimeProfileMetadata(userDataDir) {
1086
+ try {
1087
+ const raw = await readFile4(
1088
+ join4(userDataDir, OPENSTEER_RUNTIME_META_FILE),
1089
+ "utf8"
1090
+ );
1091
+ const parsed = JSON.parse(raw);
1092
+ const creator = parseProcessOwner(parsed.creator);
1093
+ const persistentUserDataDir = typeof parsed.persistentUserDataDir === "string" ? parsed.persistentUserDataDir : void 0;
1094
+ const profileDirectory = parsed.profileDirectory === null ? null : typeof parsed.profileDirectory === "string" ? parsed.profileDirectory : void 0;
1095
+ if (!creator || persistentUserDataDir === void 0 || profileDirectory === void 0 || typeof parsed.baseEntries !== "object" || parsed.baseEntries === null || Array.isArray(parsed.baseEntries)) {
1096
+ return null;
1097
+ }
1098
+ const baseEntries = deserializeSnapshotManifestEntries(
1099
+ parsed.baseEntries
1100
+ );
1101
+ return {
1102
+ baseEntries: Object.fromEntries(baseEntries),
1103
+ creator,
1104
+ persistentUserDataDir,
1105
+ profileDirectory
1106
+ };
1107
+ } catch {
1108
+ return null;
1109
+ }
1110
+ }
1111
+ async function readRuntimeProfileCreationMarker(userDataDir) {
1112
+ try {
1113
+ const raw = await readFile4(
1114
+ join4(userDataDir, OPENSTEER_RUNTIME_CREATING_FILE),
1115
+ "utf8"
1116
+ );
1117
+ return parseRuntimeProfileCreationMarker(JSON.parse(raw));
1118
+ } catch {
1119
+ return null;
1120
+ }
1121
+ }
1122
+ async function requireRuntimeProfileMetadata(userDataDir, expectedPersistentUserDataDir, expectedProfileDirectory, displayUserDataDir = userDataDir) {
1123
+ const metadata = await readRuntimeProfileMetadata(userDataDir);
1124
+ if (!metadata) {
1125
+ throw new Error(
1126
+ `Runtime profile metadata was not found for "${displayUserDataDir}".`
1127
+ );
1128
+ }
1129
+ if (metadata.profileDirectory !== expectedProfileDirectory) {
1130
+ throw new Error(
1131
+ `Runtime profile "${displayUserDataDir}" was created for profile "${metadata.profileDirectory ?? "unknown"}", expected "${expectedProfileDirectory}".`
1132
+ );
1133
+ }
1134
+ if (metadata.persistentUserDataDir !== expectedPersistentUserDataDir) {
1135
+ throw new Error(
1136
+ `Runtime profile "${displayUserDataDir}" does not belong to persistent profile "${expectedPersistentUserDataDir}".`
1137
+ );
1138
+ }
1139
+ return metadata;
1140
+ }
1141
+ async function collectPersistentSnapshotEntries(userDataDir, profileDirectory) {
1142
+ let rootEntries;
1143
+ try {
1144
+ rootEntries = await readdir2(userDataDir, {
1145
+ encoding: "utf8",
1146
+ withFileTypes: true
1147
+ });
1148
+ } catch {
1149
+ return /* @__PURE__ */ new Map();
1150
+ }
1151
+ rootEntries.sort((left, right) => left.name.localeCompare(right.name));
1152
+ const collected = /* @__PURE__ */ new Map();
1153
+ for (const entry of rootEntries) {
1154
+ if (!shouldIncludePersistentRootEntry(
1155
+ userDataDir,
1156
+ profileDirectory,
1157
+ entry.name
1158
+ )) {
1159
+ continue;
1160
+ }
1161
+ await collectSnapshotEntry(
1162
+ join4(userDataDir, entry.name),
1163
+ entry.name,
1164
+ collected
1165
+ );
1166
+ }
1167
+ return collected;
1168
+ }
1169
+ function shouldIncludePersistentRootEntry(userDataDir, profileDirectory, entry) {
1170
+ if (entry === profileDirectory) {
1171
+ return true;
1172
+ }
1173
+ if (COPY_SKIP_ENTRIES.has(entry)) {
1174
+ return false;
1175
+ }
1176
+ if (SKIPPED_ROOT_DIRECTORIES.has(entry)) {
1177
+ return false;
1178
+ }
1179
+ return !isProfileDirectory(userDataDir, entry);
1180
+ }
1181
+ async function collectSnapshotEntry(sourcePath, relativePath, collected) {
1182
+ let entryStat;
1183
+ try {
1184
+ entryStat = await stat(sourcePath);
1185
+ } catch {
1186
+ return;
1187
+ }
1188
+ if (entryStat.isDirectory()) {
1189
+ collected.set(relativePath, {
1190
+ kind: "directory",
1191
+ hash: null,
1192
+ sourcePath
1193
+ });
1194
+ let children;
1195
+ try {
1196
+ children = await readdir2(sourcePath, {
1197
+ encoding: "utf8",
1198
+ withFileTypes: true
1199
+ });
1200
+ } catch {
1201
+ return;
1202
+ }
1203
+ children.sort((left, right) => left.name.localeCompare(right.name));
1204
+ for (const child of children) {
1205
+ await collectSnapshotEntry(
1206
+ join4(sourcePath, child.name),
1207
+ join4(relativePath, child.name),
1208
+ collected
1209
+ );
1210
+ }
1211
+ return;
1212
+ }
1213
+ if (entryStat.isFile()) {
1214
+ collected.set(relativePath, {
1215
+ kind: "file",
1216
+ hash: await hashSnapshotFile(sourcePath, relativePath),
1217
+ sourcePath
1218
+ });
1219
+ }
1220
+ }
1221
+ function serializeSnapshotManifestEntries(entries) {
1222
+ return Object.fromEntries(
1223
+ [...entries.entries()].sort(([leftPath], [rightPath]) => leftPath.localeCompare(rightPath)).map(([relativePath, entry]) => [
1224
+ relativePath,
1225
+ {
1226
+ kind: entry.kind,
1227
+ hash: entry.hash
1228
+ }
1229
+ ])
1230
+ );
1231
+ }
1232
+ function deserializeSnapshotManifestEntries(entries) {
1233
+ const manifestEntries = /* @__PURE__ */ new Map();
1234
+ for (const [relativePath, entry] of Object.entries(entries)) {
1235
+ if (!entry || entry.kind !== "directory" && entry.kind !== "file" || !(entry.hash === null || typeof entry.hash === "string")) {
1236
+ throw new Error(
1237
+ `Runtime profile metadata for "${relativePath}" is invalid.`
1238
+ );
1239
+ }
1240
+ manifestEntries.set(relativePath, {
1241
+ kind: entry.kind,
1242
+ hash: entry.hash
1243
+ });
1244
+ }
1245
+ return manifestEntries;
1246
+ }
1247
+ function resolveMergedSnapshotEntries(baseEntries, currentEntries, runtimeEntries) {
1248
+ const mergedEntries = /* @__PURE__ */ new Map();
1249
+ const relativePaths = /* @__PURE__ */ new Set([
1250
+ ...baseEntries.keys(),
1251
+ ...currentEntries.keys(),
1252
+ ...runtimeEntries.keys()
1253
+ ]);
1254
+ for (const relativePath of [...relativePaths].sort(compareSnapshotPaths)) {
1255
+ mergedEntries.set(
1256
+ relativePath,
1257
+ resolveMergedSnapshotEntrySelection(
1258
+ relativePath,
1259
+ baseEntries.get(relativePath) ?? null,
1260
+ currentEntries.get(relativePath) ?? null,
1261
+ runtimeEntries.get(relativePath) ?? null
1262
+ )
1263
+ );
1264
+ }
1265
+ return mergedEntries;
1266
+ }
1267
+ function resolveMergedSnapshotEntrySelection(relativePath, baseEntry, currentEntry, runtimeEntry) {
1268
+ if (snapshotEntriesEqual(runtimeEntry, baseEntry)) {
1269
+ return currentEntry ? "current" : null;
1270
+ }
1271
+ if (snapshotEntriesEqual(currentEntry, baseEntry)) {
1272
+ return runtimeEntry ? "runtime" : null;
1273
+ }
1274
+ if (!baseEntry) {
1275
+ if (!currentEntry) {
1276
+ return runtimeEntry ? "runtime" : null;
1277
+ }
1278
+ if (!runtimeEntry) {
1279
+ return "current";
1280
+ }
1281
+ if (snapshotEntriesEqual(currentEntry, runtimeEntry)) {
1282
+ return "current";
1283
+ }
1284
+ throw new Error(
1285
+ `Concurrent runtime updates changed "${relativePath}" differently; refusing to overwrite the persistent profile.`
1286
+ );
1287
+ }
1288
+ if (!currentEntry && !runtimeEntry) {
1289
+ return null;
1290
+ }
1291
+ if (snapshotEntriesEqual(currentEntry, runtimeEntry)) {
1292
+ return currentEntry ? "current" : null;
1293
+ }
1294
+ throw new Error(
1295
+ `Concurrent runtime updates changed "${relativePath}" differently; refusing to overwrite the persistent profile.`
1296
+ );
1297
+ }
1298
+ function snapshotEntriesEqual(left, right) {
1299
+ if (!left || !right) {
1300
+ return left === right;
1301
+ }
1302
+ return left.kind === right.kind && left.hash === right.hash;
1303
+ }
1304
+ async function materializeMergedPersistentProfileSnapshot(targetUserDataDir, currentEntries, runtimeEntries, mergedEntries) {
1305
+ const selectedEntries = [...mergedEntries.entries()].filter(([, selection]) => selection !== null).sort(
1306
+ ([leftPath], [rightPath]) => compareSnapshotPaths(leftPath, rightPath)
1307
+ );
1308
+ for (const [relativePath, selection] of selectedEntries) {
1309
+ const entry = (selection === "current" ? currentEntries.get(relativePath) : runtimeEntries.get(relativePath)) ?? null;
1310
+ if (!entry) {
1311
+ continue;
1312
+ }
1313
+ const targetPath = join4(targetUserDataDir, relativePath);
1314
+ if (entry.kind === "directory") {
1315
+ await mkdir3(targetPath, { recursive: true });
1316
+ continue;
1317
+ }
1318
+ await mkdir3(dirname4(targetPath), { recursive: true });
1319
+ await copyFile(entry.sourcePath, targetPath);
1320
+ }
1321
+ }
1322
+ function compareSnapshotPaths(left, right) {
1323
+ const leftDepth = left.split(sep).length;
1324
+ const rightDepth = right.split(sep).length;
1325
+ if (leftDepth !== rightDepth) {
1326
+ return leftDepth - rightDepth;
1327
+ }
1328
+ return left.localeCompare(right);
1329
+ }
1330
+ async function hashSnapshotFile(filePath, relativePath) {
1331
+ const normalizedJson = await readNormalizedSnapshotJson(filePath, relativePath);
1332
+ if (normalizedJson !== null) {
1333
+ return createHash("sha256").update(JSON.stringify(normalizedJson)).digest("hex");
1334
+ }
1335
+ return await hashFile(filePath);
1336
+ }
1337
+ async function readNormalizedSnapshotJson(filePath, relativePath) {
1338
+ const normalizer = SNAPSHOT_JSON_NORMALIZERS.get(relativePath);
1339
+ if (!normalizer) {
1340
+ return null;
1341
+ }
1342
+ try {
1343
+ const parsed = JSON.parse(await readFile4(filePath, "utf8"));
1344
+ return normalizer(parsed);
1345
+ } catch {
1346
+ return null;
1347
+ }
1348
+ }
1349
+ var SNAPSHOT_JSON_NORMALIZERS = /* @__PURE__ */ new Map([["Local State", normalizeLocalStateSnapshotJson]]);
1350
+ function normalizeLocalStateSnapshotJson(value) {
1351
+ if (!isJsonRecord(value)) {
1352
+ return value;
1353
+ }
1354
+ const { user_experience_metrics: _ignored, ...rest } = value;
1355
+ return rest;
1356
+ }
1357
+ function isJsonRecord(value) {
1358
+ return !!value && typeof value === "object" && !Array.isArray(value);
1359
+ }
1360
+ async function hashFile(filePath) {
1361
+ return new Promise((resolve, reject) => {
1362
+ const hash = createHash("sha256");
1363
+ const stream = createReadStream(filePath);
1364
+ stream.on("data", (chunk) => {
1365
+ hash.update(chunk);
1366
+ });
1367
+ stream.on("error", reject);
1368
+ stream.on("end", () => {
1369
+ resolve(hash.digest("hex"));
1370
+ });
1371
+ });
1372
+ }
1373
+ async function ensurePersistentProfileMetadata(userDataDir, metadata) {
1374
+ if (existsSync3(join4(userDataDir, OPENSTEER_META_FILE))) {
1375
+ return;
1376
+ }
1377
+ await writePersistentProfileMetadata(userDataDir, metadata);
1378
+ }
1379
+ async function recoverPersistentProfileBackup(targetUserDataDir) {
1380
+ const backupDirPaths = await listPersistentProfileBackupDirs(targetUserDataDir);
1381
+ if (backupDirPaths.length === 0) {
1382
+ return;
1383
+ }
1384
+ if (!existsSync3(targetUserDataDir)) {
1385
+ const [latestBackupDirPath, ...staleBackupDirPaths] = backupDirPaths;
1386
+ await rename3(latestBackupDirPath, targetUserDataDir);
1387
+ await Promise.all(
1388
+ staleBackupDirPaths.map(
1389
+ (backupDirPath) => rm3(backupDirPath, {
1390
+ recursive: true,
1391
+ force: true
1392
+ }).catch(() => void 0)
1393
+ )
1394
+ );
1395
+ return;
1396
+ }
1397
+ await Promise.all(
1398
+ backupDirPaths.map(
1399
+ (backupDirPath) => rm3(backupDirPath, {
1400
+ recursive: true,
1401
+ force: true
1402
+ }).catch(() => void 0)
1403
+ )
1404
+ );
1405
+ }
1406
+ async function listPersistentProfileBackupDirs(targetUserDataDir) {
1407
+ const profilesDir = dirname4(targetUserDataDir);
1408
+ let entries;
1409
+ try {
1410
+ entries = await readdir2(profilesDir, {
1411
+ encoding: "utf8",
1412
+ withFileTypes: true
1413
+ });
1414
+ } catch {
1415
+ return [];
1416
+ }
1417
+ const backupDirNamePrefix = buildPersistentProfileBackupDirNamePrefix(targetUserDataDir);
1418
+ return entries.filter(
1419
+ (entry) => entry.isDirectory() && entry.name.startsWith(backupDirNamePrefix)
1420
+ ).map((entry) => join4(profilesDir, entry.name)).sort((leftPath, rightPath) => rightPath.localeCompare(leftPath));
1421
+ }
1422
+ async function readPersistentProfileMetadata(userDataDir) {
1423
+ try {
1424
+ const raw = await readFile4(join4(userDataDir, OPENSTEER_META_FILE), "utf8");
1425
+ const parsed = JSON.parse(raw);
1426
+ if (typeof parsed.createdAt !== "string" || typeof parsed.profileDirectory !== "string" || typeof parsed.source !== "string") {
1427
+ return null;
1428
+ }
1429
+ return {
1430
+ createdAt: parsed.createdAt,
1431
+ profileDirectory: parsed.profileDirectory,
1432
+ source: parsed.source
1433
+ };
1434
+ } catch {
1435
+ return null;
1436
+ }
1437
+ }
1438
+ async function requirePersistentProfileMetadata(userDataDir) {
1439
+ const metadata = await readPersistentProfileMetadata(userDataDir);
1440
+ if (!metadata) {
1441
+ throw new Error(
1442
+ `Persistent profile metadata was not found for "${userDataDir}".`
1443
+ );
1444
+ }
1445
+ return metadata;
1446
+ }
1447
+ async function isHealthyPersistentProfile(userDataDir, expectedSourceUserDataDir, expectedProfileDirectory) {
1448
+ if (!existsSync3(userDataDir) || !existsSync3(join4(userDataDir, expectedProfileDirectory))) {
1449
+ return false;
1450
+ }
1451
+ const metadata = await readPersistentProfileMetadata(userDataDir);
1452
+ return metadata?.source === expectedSourceUserDataDir && metadata.profileDirectory === expectedProfileDirectory;
1453
+ }
1454
+ function wasDirPublishedByAnotherProcess2(error, targetDirPath) {
1455
+ const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
1456
+ return existsSync3(targetDirPath) && (code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM");
1457
+ }
1458
+ async function replaceProfileDirectory(targetUserDataDir, replacementUserDataDir) {
1459
+ if (!existsSync3(targetUserDataDir)) {
1460
+ await rename3(replacementUserDataDir, targetUserDataDir);
1461
+ return;
1462
+ }
1463
+ const backupUserDataDir = buildPersistentProfileBackupDirPath(targetUserDataDir);
1464
+ let targetMovedToBackup = false;
1465
+ let replacementPublished = false;
1466
+ try {
1467
+ await rename3(targetUserDataDir, backupUserDataDir);
1468
+ targetMovedToBackup = true;
1469
+ await rename3(replacementUserDataDir, targetUserDataDir);
1470
+ replacementPublished = true;
1471
+ } catch (error) {
1472
+ if (targetMovedToBackup && !existsSync3(targetUserDataDir)) {
1473
+ await rename3(backupUserDataDir, targetUserDataDir).catch(() => void 0);
1474
+ }
1475
+ throw error;
1476
+ } finally {
1477
+ if (replacementPublished && targetMovedToBackup && existsSync3(backupUserDataDir)) {
1478
+ await rm3(backupUserDataDir, {
1479
+ recursive: true,
1480
+ force: true
1481
+ }).catch(() => void 0);
1482
+ }
1483
+ }
1484
+ }
1485
+ async function withPersistentProfileWriteAccess(targetUserDataDir, action) {
1486
+ const releaseWriteLock = await acquirePersistentProfileWriteLock(
1487
+ targetUserDataDir
1488
+ );
1489
+ try {
1490
+ await waitForRuntimeProfileCreationsToDrain(targetUserDataDir);
1491
+ await waitForSharedRealBrowserSessionToDrain(targetUserDataDir);
1492
+ return await action();
1493
+ } finally {
1494
+ await releaseWriteLock();
1495
+ }
1496
+ }
1497
+ function buildPersistentProfileTempDirPrefix(targetUserDataDir) {
1498
+ return join4(
1499
+ dirname4(targetUserDataDir),
1500
+ `${buildPersistentProfileTempDirNamePrefix(targetUserDataDir)}${process.pid}-${CURRENT_PROCESS_OWNER.processStartedAtMs}-`
1501
+ );
1502
+ }
1503
+ function buildPersistentProfileTempDirNamePrefix(targetUserDataDir) {
1504
+ return `${basename3(targetUserDataDir)}-tmp-`;
1505
+ }
1506
+ function buildPersistentProfileBackupDirPath(targetUserDataDir) {
1507
+ return join4(
1508
+ dirname4(targetUserDataDir),
1509
+ `${buildPersistentProfileBackupDirNamePrefix(targetUserDataDir)}${Date.now()}-${process.pid}-${CURRENT_PROCESS_OWNER.processStartedAtMs}-${randomUUID3()}`
1510
+ );
1511
+ }
1512
+ function buildPersistentProfileBackupDirNamePrefix(targetUserDataDir) {
1513
+ return `${basename3(targetUserDataDir)}-backup-`;
1514
+ }
1515
+ function buildRuntimeProfileCreationRegistryDirPath(persistentUserDataDir) {
1516
+ return join4(
1517
+ dirname4(persistentUserDataDir),
1518
+ `${basename3(persistentUserDataDir)}.creating`
1519
+ );
1520
+ }
1521
+ function buildRuntimeProfileCreationRegistrationPath(persistentUserDataDir, runtimeUserDataDir) {
1522
+ const key = createHash("sha256").update(runtimeUserDataDir).digest("hex").slice(0, 16);
1523
+ return join4(
1524
+ buildRuntimeProfileCreationRegistryDirPath(persistentUserDataDir),
1525
+ `${key}.json`
1526
+ );
1527
+ }
1528
+ function buildRuntimeProfileKey(sourceUserDataDir) {
1529
+ const hash = createHash("sha256").update(sourceUserDataDir).digest("hex").slice(0, 16);
1530
+ return `${sanitizePathSegment(basename3(sourceUserDataDir) || "profile")}-${hash}`;
1531
+ }
1532
+ function buildRuntimeProfileDirNamePrefix(sourceUserDataDir) {
1533
+ return `${buildRuntimeProfileKey(sourceUserDataDir)}-runtime-`;
1534
+ }
1535
+ function buildRuntimeProfileDirPrefix(runtimesRootDir, sourceUserDataDir) {
1536
+ return join4(
1537
+ runtimesRootDir,
1538
+ buildRuntimeProfileDirNamePrefix(sourceUserDataDir)
1539
+ );
1540
+ }
1541
+ async function reserveRuntimeProfileCreation(persistentUserDataDir, runtimeRootDir, profileDirectory) {
1542
+ while (true) {
1543
+ let runtimeUserDataDir = null;
1544
+ await withPersistentProfileControlLock(
1545
+ persistentUserDataDir,
1546
+ async () => {
1547
+ if (await isPersistentProfileWriteLocked(persistentUserDataDir)) {
1548
+ return;
1549
+ }
1550
+ if (await hasLiveSharedRealBrowserSession(persistentUserDataDir)) {
1551
+ return;
1552
+ }
1553
+ const createdRuntimeUserDataDir = await mkdtemp(
1554
+ buildRuntimeProfileDirPrefix(
1555
+ runtimeRootDir,
1556
+ persistentUserDataDir
1557
+ )
1558
+ );
1559
+ runtimeUserDataDir = createdRuntimeUserDataDir;
1560
+ const marker = {
1561
+ creator: CURRENT_PROCESS_OWNER,
1562
+ persistentUserDataDir,
1563
+ profileDirectory,
1564
+ runtimeUserDataDir: createdRuntimeUserDataDir
1565
+ };
1566
+ await writeRuntimeProfileCreationMarker(
1567
+ createdRuntimeUserDataDir,
1568
+ marker
1569
+ );
1570
+ await writeRuntimeProfileCreationRegistration(marker);
1571
+ }
1572
+ );
1573
+ if (runtimeUserDataDir) {
1574
+ return {
1575
+ persistentUserDataDir,
1576
+ userDataDir: runtimeUserDataDir
1577
+ };
1578
+ }
1579
+ await sleep4(PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS);
1580
+ }
1581
+ }
1582
+ async function clearRuntimeProfileCreationState(runtimeUserDataDir, persistentUserDataDir) {
1583
+ await Promise.all([
1584
+ rm3(join4(runtimeUserDataDir, OPENSTEER_RUNTIME_CREATING_FILE), {
1585
+ force: true
1586
+ }).catch(() => void 0),
1587
+ rm3(
1588
+ buildRuntimeProfileCreationRegistrationPath(
1589
+ persistentUserDataDir,
1590
+ runtimeUserDataDir
1591
+ ),
1592
+ {
1593
+ force: true
1594
+ }
1595
+ ).catch(() => void 0)
1596
+ ]);
1597
+ }
1598
+ async function writeRuntimeProfileCreationRegistration(marker) {
1599
+ const registryDirPath = buildRuntimeProfileCreationRegistryDirPath(
1600
+ marker.persistentUserDataDir
1601
+ );
1602
+ await mkdir3(registryDirPath, { recursive: true });
1603
+ await writeFile3(
1604
+ buildRuntimeProfileCreationRegistrationPath(
1605
+ marker.persistentUserDataDir,
1606
+ marker.runtimeUserDataDir
1607
+ ),
1608
+ JSON.stringify(marker, null, 2)
1609
+ );
1610
+ }
1611
+ async function listRuntimeProfileCreationRegistrations(persistentUserDataDir) {
1612
+ const registryDirPath = buildRuntimeProfileCreationRegistryDirPath(
1613
+ persistentUserDataDir
1614
+ );
1615
+ let entries;
1616
+ try {
1617
+ entries = await readdir2(registryDirPath, {
1618
+ encoding: "utf8",
1619
+ withFileTypes: true
1620
+ });
1621
+ } catch {
1622
+ return [];
1623
+ }
1624
+ return await Promise.all(
1625
+ entries.filter((entry) => entry.isFile()).map(async (entry) => {
1626
+ const filePath = join4(registryDirPath, entry.name);
1627
+ return {
1628
+ filePath,
1629
+ marker: await readRuntimeProfileCreationRegistration(filePath)
1630
+ };
1631
+ })
1632
+ );
1633
+ }
1634
+ async function readRuntimeProfileCreationRegistration(filePath) {
1635
+ try {
1636
+ const raw = await readFile4(filePath, "utf8");
1637
+ return parseRuntimeProfileCreationMarker(JSON.parse(raw));
1638
+ } catch {
1639
+ return null;
1640
+ }
1641
+ }
1642
+ async function cleanOrphanedRuntimeProfileDirs(rootDir, runtimeDirNamePrefix) {
1643
+ let entries;
1644
+ try {
1645
+ entries = await readdir2(rootDir, {
1646
+ encoding: "utf8",
1647
+ withFileTypes: true
1648
+ });
1649
+ } catch {
1650
+ return;
1651
+ }
1652
+ const liveProcessCommandLines = await listProcessCommandLines();
1653
+ await Promise.all(
1654
+ entries.map(async (entry) => {
1655
+ if (!entry.isDirectory() || !entry.name.startsWith(runtimeDirNamePrefix)) {
1656
+ return;
1657
+ }
1658
+ const runtimeDirPath = join4(rootDir, entry.name);
1659
+ const creationMarker = await readRuntimeProfileCreationMarker(
1660
+ runtimeDirPath
1661
+ );
1662
+ if (await isRuntimeProfileDirInUse(
1663
+ runtimeDirPath,
1664
+ liveProcessCommandLines
1665
+ )) {
1666
+ return;
1667
+ }
1668
+ await rm3(runtimeDirPath, {
1669
+ recursive: true,
1670
+ force: true
1671
+ }).catch(() => void 0);
1672
+ if (creationMarker) {
1673
+ await clearRuntimeProfileCreationState(
1674
+ runtimeDirPath,
1675
+ creationMarker.persistentUserDataDir
1676
+ );
1677
+ }
1678
+ })
1679
+ );
1680
+ }
1681
+ async function cleanOrphanedOwnedDirs(rootDir, ownedDirNamePrefix) {
1682
+ let entries;
1683
+ try {
1684
+ entries = await readdir2(rootDir, {
1685
+ encoding: "utf8",
1686
+ withFileTypes: true
1687
+ });
1688
+ } catch {
1689
+ return;
1690
+ }
1691
+ await Promise.all(
1692
+ entries.map(async (entry) => {
1693
+ if (!entry.isDirectory() || !entry.name.startsWith(ownedDirNamePrefix)) {
1694
+ return;
1695
+ }
1696
+ if (await isOwnedDirByLiveProcess(entry.name, ownedDirNamePrefix)) {
1697
+ return;
1698
+ }
1699
+ await rm3(join4(rootDir, entry.name), {
1700
+ recursive: true,
1701
+ force: true
1702
+ }).catch(() => void 0);
1703
+ })
1704
+ );
1705
+ }
1706
+ async function isOwnedDirByLiveProcess(ownedDirName, ownedDirPrefix) {
1707
+ const owner = parseOwnedDirOwner(ownedDirName, ownedDirPrefix);
1708
+ return owner ? await getProcessLiveness(owner) !== "dead" : false;
1709
+ }
1710
+ async function isRuntimeProfileDirInUse(runtimeDirPath, liveProcessCommandLines) {
1711
+ const creationMarker = await readRuntimeProfileCreationMarker(runtimeDirPath);
1712
+ if (creationMarker && await getProcessLiveness(creationMarker.creator) !== "dead") {
1713
+ return true;
1714
+ }
1715
+ const metadata = await readRuntimeProfileMetadata(runtimeDirPath);
1716
+ if (metadata && await getProcessLiveness(metadata.creator) !== "dead") {
1717
+ return true;
1718
+ }
1719
+ return liveProcessCommandLines.some(
1720
+ (commandLine) => commandLineIncludesUserDataDir(commandLine, runtimeDirPath)
400
1721
  );
401
1722
  }
402
- async function readRuntimeProfileMetadata(userDataDir) {
403
- try {
404
- const raw = await readFile(
405
- join(userDataDir, OPENSTEER_RUNTIME_META_FILE),
406
- "utf8"
407
- );
408
- const parsed = JSON.parse(raw);
409
- const profileDirectory = parsed.profileDirectory === null ? null : typeof parsed.profileDirectory === "string" ? parsed.profileDirectory : void 0;
410
- if (profileDirectory === void 0 || typeof parsed.baseEntries !== "object" || parsed.baseEntries === null || Array.isArray(parsed.baseEntries)) {
411
- return null;
1723
+ async function claimRuntimeProfileForPersist(runtimeUserDataDir) {
1724
+ while (true) {
1725
+ await waitForRuntimeProfileProcessesToDrain(runtimeUserDataDir);
1726
+ const claimedRuntimeUserDataDir = buildClaimedRuntimeProfileDirPath(runtimeUserDataDir);
1727
+ try {
1728
+ await rename3(runtimeUserDataDir, claimedRuntimeUserDataDir);
1729
+ } catch (error) {
1730
+ const code = getErrorCode3(error);
1731
+ if (code === "ENOENT") {
1732
+ throw new Error(
1733
+ `Runtime profile "${runtimeUserDataDir}" was not found.`
1734
+ );
1735
+ }
1736
+ if (code === "EACCES" || code === "EBUSY" || code === "EPERM") {
1737
+ await sleep4(PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS);
1738
+ continue;
1739
+ }
1740
+ throw error;
412
1741
  }
413
- const baseEntries = deserializeSnapshotManifestEntries(
414
- parsed.baseEntries
415
- );
416
- return {
417
- baseEntries: Object.fromEntries(baseEntries),
418
- profileDirectory
419
- };
420
- } catch {
421
- return null;
1742
+ if (!await hasLiveProcessUsingUserDataDir(runtimeUserDataDir)) {
1743
+ return claimedRuntimeUserDataDir;
1744
+ }
1745
+ await rename3(
1746
+ claimedRuntimeUserDataDir,
1747
+ runtimeUserDataDir
1748
+ ).catch(() => void 0);
1749
+ await sleep4(PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS);
422
1750
  }
423
1751
  }
424
- async function requireRuntimeProfileMetadata(userDataDir, expectedProfileDirectory) {
425
- const metadata = await readRuntimeProfileMetadata(userDataDir);
426
- if (!metadata) {
427
- throw new Error(
428
- `Runtime profile metadata was not found for "${userDataDir}".`
429
- );
1752
+ async function restoreClaimedRuntimeProfile(claimedRuntimeUserDataDir, runtimeUserDataDir) {
1753
+ if (!existsSync3(claimedRuntimeUserDataDir)) {
1754
+ return;
430
1755
  }
431
- if (metadata.profileDirectory !== expectedProfileDirectory) {
1756
+ if (existsSync3(runtimeUserDataDir)) {
432
1757
  throw new Error(
433
- `Runtime profile "${userDataDir}" was created for profile "${metadata.profileDirectory ?? "unknown"}", expected "${expectedProfileDirectory}".`
1758
+ `Runtime profile "${runtimeUserDataDir}" was recreated before the failed persist could restore it from "${claimedRuntimeUserDataDir}".`
434
1759
  );
435
1760
  }
436
- return metadata;
1761
+ await rename3(claimedRuntimeUserDataDir, runtimeUserDataDir);
437
1762
  }
438
- async function collectPersistentSnapshotEntries(userDataDir, profileDirectory) {
439
- let rootEntries;
440
- try {
441
- rootEntries = await readdir(userDataDir, {
442
- encoding: "utf8",
443
- withFileTypes: true
444
- });
445
- } catch {
446
- return /* @__PURE__ */ new Map();
1763
+ function buildClaimedRuntimeProfileDirPath(runtimeUserDataDir) {
1764
+ return join4(
1765
+ dirname4(runtimeUserDataDir),
1766
+ `${basename3(runtimeUserDataDir)}-persisting-${process.pid}-${CURRENT_PROCESS_OWNER.processStartedAtMs}-${randomUUID3()}`
1767
+ );
1768
+ }
1769
+ async function waitForRuntimeProfileProcessesToDrain(runtimeUserDataDir) {
1770
+ while (await hasLiveProcessUsingUserDataDir(runtimeUserDataDir)) {
1771
+ await sleep4(PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS);
447
1772
  }
448
- rootEntries.sort((left, right) => left.name.localeCompare(right.name));
449
- const collected = /* @__PURE__ */ new Map();
450
- for (const entry of rootEntries) {
451
- if (!shouldIncludePersistentRootEntry(
452
- userDataDir,
453
- profileDirectory,
454
- entry.name
455
- )) {
1773
+ }
1774
+ async function hasLiveProcessUsingUserDataDir(userDataDir) {
1775
+ const liveProcessCommandLines = await listProcessCommandLines();
1776
+ return liveProcessCommandLines.some(
1777
+ (commandLine) => commandLineIncludesUserDataDir(commandLine, userDataDir)
1778
+ );
1779
+ }
1780
+ async function hasActiveRuntimeProfileCreations(persistentUserDataDir) {
1781
+ const registrations = await listRuntimeProfileCreationRegistrations(
1782
+ persistentUserDataDir
1783
+ );
1784
+ let hasLiveCreation = false;
1785
+ for (const registration of registrations) {
1786
+ const marker = registration.marker;
1787
+ if (!marker || marker.persistentUserDataDir !== persistentUserDataDir) {
1788
+ await rm3(registration.filePath, {
1789
+ force: true
1790
+ }).catch(() => void 0);
456
1791
  continue;
457
1792
  }
458
- await collectSnapshotEntry(
459
- join(userDataDir, entry.name),
460
- entry.name,
461
- collected
1793
+ const runtimeMarker = await readRuntimeProfileCreationMarker(
1794
+ marker.runtimeUserDataDir
462
1795
  );
1796
+ if (!runtimeMarker || runtimeMarker.persistentUserDataDir !== persistentUserDataDir || runtimeMarker.runtimeUserDataDir !== marker.runtimeUserDataDir) {
1797
+ await clearRuntimeProfileCreationState(
1798
+ marker.runtimeUserDataDir,
1799
+ persistentUserDataDir
1800
+ );
1801
+ continue;
1802
+ }
1803
+ if (await getProcessLiveness(runtimeMarker.creator) === "dead") {
1804
+ await clearRuntimeProfileCreationState(
1805
+ marker.runtimeUserDataDir,
1806
+ persistentUserDataDir
1807
+ );
1808
+ await rm3(marker.runtimeUserDataDir, {
1809
+ recursive: true,
1810
+ force: true
1811
+ }).catch(() => void 0);
1812
+ continue;
1813
+ }
1814
+ hasLiveCreation = true;
463
1815
  }
464
- return collected;
1816
+ return hasLiveCreation;
465
1817
  }
466
- function shouldIncludePersistentRootEntry(userDataDir, profileDirectory, entry) {
467
- if (entry === profileDirectory) {
468
- return true;
1818
+ async function waitForRuntimeProfileCreationsToDrain(persistentUserDataDir) {
1819
+ while (true) {
1820
+ if (!await hasActiveRuntimeProfileCreations(persistentUserDataDir)) {
1821
+ return;
1822
+ }
1823
+ await sleep4(PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS);
469
1824
  }
470
- if (COPY_SKIP_ENTRIES.has(entry)) {
471
- return false;
1825
+ }
1826
+ function parseRuntimeProfileCreationMarker(value) {
1827
+ if (!value || typeof value !== "object") {
1828
+ return null;
472
1829
  }
473
- if (SKIPPED_ROOT_DIRECTORIES.has(entry)) {
474
- return false;
1830
+ const parsed = value;
1831
+ const creator = parseProcessOwner(parsed.creator);
1832
+ const persistentUserDataDir = typeof parsed.persistentUserDataDir === "string" ? parsed.persistentUserDataDir : void 0;
1833
+ const profileDirectory = parsed.profileDirectory === null ? null : typeof parsed.profileDirectory === "string" ? parsed.profileDirectory : void 0;
1834
+ const runtimeUserDataDir = typeof parsed.runtimeUserDataDir === "string" ? parsed.runtimeUserDataDir : void 0;
1835
+ if (!creator || persistentUserDataDir === void 0 || profileDirectory === void 0 || runtimeUserDataDir === void 0) {
1836
+ return null;
475
1837
  }
476
- return !isProfileDirectory(userDataDir, entry);
1838
+ return {
1839
+ creator,
1840
+ persistentUserDataDir,
1841
+ profileDirectory,
1842
+ runtimeUserDataDir
1843
+ };
477
1844
  }
478
- async function collectSnapshotEntry(sourcePath, relativePath, collected) {
479
- let entryStat;
480
- try {
481
- entryStat = await stat(sourcePath);
482
- } catch {
483
- return;
1845
+ function parseOwnedDirOwner(ownedDirName, ownedDirPrefix) {
1846
+ const remainder = ownedDirName.slice(ownedDirPrefix.length);
1847
+ const firstDashIndex = remainder.indexOf("-");
1848
+ const secondDashIndex = firstDashIndex === -1 ? -1 : remainder.indexOf("-", firstDashIndex + 1);
1849
+ if (firstDashIndex === -1 || secondDashIndex === -1) {
1850
+ return null;
484
1851
  }
485
- if (entryStat.isDirectory()) {
486
- collected.set(relativePath, {
487
- kind: "directory",
488
- hash: null,
489
- sourcePath
490
- });
491
- let children;
492
- try {
493
- children = await readdir(sourcePath, {
494
- encoding: "utf8",
495
- withFileTypes: true
496
- });
497
- } catch {
498
- return;
499
- }
500
- children.sort((left, right) => left.name.localeCompare(right.name));
501
- for (const child of children) {
502
- await collectSnapshotEntry(
503
- join(sourcePath, child.name),
504
- join(relativePath, child.name),
505
- collected
506
- );
507
- }
508
- return;
1852
+ const pid = Number.parseInt(remainder.slice(0, firstDashIndex), 10);
1853
+ const processStartedAtMs = Number.parseInt(
1854
+ remainder.slice(firstDashIndex + 1, secondDashIndex),
1855
+ 10
1856
+ );
1857
+ if (!Number.isInteger(pid) || pid <= 0) {
1858
+ return null;
509
1859
  }
510
- if (entryStat.isFile()) {
511
- collected.set(relativePath, {
512
- kind: "file",
513
- hash: await hashFile(sourcePath),
514
- sourcePath
515
- });
1860
+ if (!Number.isInteger(processStartedAtMs) || processStartedAtMs <= 0) {
1861
+ return null;
516
1862
  }
1863
+ return { pid, processStartedAtMs };
517
1864
  }
518
- function serializeSnapshotManifestEntries(entries) {
519
- return Object.fromEntries(
520
- [...entries.entries()].sort(([leftPath], [rightPath]) => leftPath.localeCompare(rightPath)).map(([relativePath, entry]) => [
521
- relativePath,
1865
+ async function listProcessCommandLines() {
1866
+ if (process.platform === "win32") {
1867
+ return await listWindowsProcessCommandLines();
1868
+ }
1869
+ return await listPsProcessCommandLines();
1870
+ }
1871
+ async function listPsProcessCommandLines() {
1872
+ try {
1873
+ const { stdout } = await execFileAsync2(
1874
+ "ps",
1875
+ ["-axww", "-o", "command="],
522
1876
  {
523
- kind: entry.kind,
524
- hash: entry.hash
1877
+ env: PS_COMMAND_ENV2,
1878
+ maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES2
525
1879
  }
526
- ])
527
- );
528
- }
529
- function deserializeSnapshotManifestEntries(entries) {
530
- const manifestEntries = /* @__PURE__ */ new Map();
531
- for (const [relativePath, entry] of Object.entries(entries)) {
532
- if (!entry || entry.kind !== "directory" && entry.kind !== "file" || !(entry.hash === null || typeof entry.hash === "string")) {
533
- throw new Error(
534
- `Runtime profile metadata for "${relativePath}" is invalid.`
535
- );
536
- }
537
- manifestEntries.set(relativePath, {
538
- kind: entry.kind,
539
- hash: entry.hash
540
- });
1880
+ );
1881
+ return stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
1882
+ } catch {
1883
+ return [];
541
1884
  }
542
- return manifestEntries;
543
1885
  }
544
- function resolveMergedSnapshotEntries(baseEntries, currentEntries, runtimeEntries) {
545
- const mergedEntries = /* @__PURE__ */ new Map();
546
- const relativePaths = /* @__PURE__ */ new Set([
547
- ...baseEntries.keys(),
548
- ...currentEntries.keys(),
549
- ...runtimeEntries.keys()
550
- ]);
551
- for (const relativePath of [...relativePaths].sort(compareSnapshotPaths)) {
552
- mergedEntries.set(
553
- relativePath,
554
- resolveMergedSnapshotEntrySelection(
555
- relativePath,
556
- baseEntries.get(relativePath) ?? null,
557
- currentEntries.get(relativePath) ?? null,
558
- runtimeEntries.get(relativePath) ?? null
559
- )
1886
+ async function listWindowsProcessCommandLines() {
1887
+ const script = [
1888
+ "$processes = Get-CimInstance Win32_Process | Select-Object CommandLine",
1889
+ "$processes | ConvertTo-Json -Compress"
1890
+ ].join("; ");
1891
+ try {
1892
+ const { stdout } = await execFileAsync2(
1893
+ "powershell.exe",
1894
+ ["-NoLogo", "-NoProfile", "-Command", script],
1895
+ {
1896
+ maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES2
1897
+ }
560
1898
  );
1899
+ const parsed = JSON.parse(stdout);
1900
+ const records = Array.isArray(parsed) ? parsed : [parsed];
1901
+ return records.map((record) => record?.CommandLine?.trim() ?? "").filter((commandLine) => commandLine.length > 0);
1902
+ } catch {
1903
+ return [];
561
1904
  }
562
- return mergedEntries;
563
1905
  }
564
- function resolveMergedSnapshotEntrySelection(relativePath, baseEntry, currentEntry, runtimeEntry) {
565
- if (snapshotEntriesEqual(runtimeEntry, baseEntry)) {
566
- return currentEntry ? "current" : null;
567
- }
568
- if (snapshotEntriesEqual(currentEntry, baseEntry)) {
569
- return runtimeEntry ? "runtime" : null;
570
- }
571
- if (!baseEntry) {
572
- if (!currentEntry) {
573
- return runtimeEntry ? "runtime" : null;
574
- }
575
- if (!runtimeEntry) {
576
- return "current";
577
- }
578
- if (snapshotEntriesEqual(currentEntry, runtimeEntry)) {
579
- return "current";
1906
+ function commandLineIncludesUserDataDir(commandLine, userDataDir) {
1907
+ const unquoted = `--user-data-dir=${userDataDir}`;
1908
+ const unquotedIndex = commandLine.indexOf(unquoted);
1909
+ if (unquotedIndex !== -1) {
1910
+ const after = commandLine[unquotedIndex + unquoted.length];
1911
+ if (after === void 0 || after === " " || after === " ") {
1912
+ return true;
580
1913
  }
581
- throw new Error(
582
- `Concurrent runtime updates changed "${relativePath}" differently; refusing to overwrite the persistent profile.`
583
- );
584
- }
585
- if (!currentEntry && !runtimeEntry) {
586
- return null;
587
- }
588
- if (snapshotEntriesEqual(currentEntry, runtimeEntry)) {
589
- return currentEntry ? "current" : null;
590
1914
  }
591
- throw new Error(
592
- `Concurrent runtime updates changed "${relativePath}" differently; refusing to overwrite the persistent profile.`
593
- );
1915
+ return [
1916
+ `--user-data-dir="${userDataDir}"`,
1917
+ `--user-data-dir='${userDataDir}'`
1918
+ ].some((candidate) => commandLine.includes(candidate));
594
1919
  }
595
- function snapshotEntriesEqual(left, right) {
596
- if (!left || !right) {
597
- return left === right;
1920
+ function getErrorCode3(error) {
1921
+ return typeof error === "object" && error !== null && "code" in error && (typeof error.code === "string" || typeof error.code === "number") ? error.code : void 0;
1922
+ }
1923
+ async function sleep4(ms) {
1924
+ await new Promise((resolve) => setTimeout(resolve, ms));
1925
+ }
1926
+
1927
+ // src/browser/shared-real-browser-session.ts
1928
+ import { randomUUID as randomUUID4 } from "crypto";
1929
+ import { spawn } from "child_process";
1930
+ import {
1931
+ mkdir as mkdir4,
1932
+ readFile as readFile5,
1933
+ readdir as readdir3,
1934
+ rm as rm4,
1935
+ writeFile as writeFile4
1936
+ } from "fs/promises";
1937
+ import { createServer } from "net";
1938
+ import { join as join5 } from "path";
1939
+ import {
1940
+ chromium
1941
+ } from "playwright";
1942
+ var SHARED_SESSION_RETRY_DELAY_MS2 = 50;
1943
+ async function acquireSharedRealBrowserSession(options) {
1944
+ const reservation = await reserveSharedSessionClient(options);
1945
+ const sessionContext = await attachToSharedSession(reservation, options);
1946
+ let closed = false;
1947
+ return {
1948
+ browser: sessionContext.browser,
1949
+ context: sessionContext.context,
1950
+ page: sessionContext.page,
1951
+ close: async () => {
1952
+ if (closed) {
1953
+ return;
1954
+ }
1955
+ closed = true;
1956
+ await releaseSharedSessionClient(sessionContext);
1957
+ }
1958
+ };
1959
+ }
1960
+ function getOwnedRealBrowserProcessPolicy(platformName = process.platform) {
1961
+ if (platformName === "win32") {
1962
+ return {
1963
+ detached: false,
1964
+ killStrategy: "taskkill",
1965
+ shouldUnref: true
1966
+ };
1967
+ }
1968
+ if (platformName === "darwin") {
1969
+ return {
1970
+ detached: false,
1971
+ killStrategy: "process",
1972
+ shouldUnref: true
1973
+ };
598
1974
  }
599
- return left.kind === right.kind && left.hash === right.hash;
1975
+ return {
1976
+ detached: true,
1977
+ killStrategy: "process-group",
1978
+ shouldUnref: true
1979
+ };
600
1980
  }
601
- async function materializeMergedPersistentProfileSnapshot(targetUserDataDir, currentEntries, runtimeEntries, mergedEntries) {
602
- const selectedEntries = [...mergedEntries.entries()].filter(([, selection]) => selection !== null).sort(
603
- ([leftPath], [rightPath]) => compareSnapshotPaths(leftPath, rightPath)
604
- );
605
- for (const [relativePath, selection] of selectedEntries) {
606
- const entry = (selection === "current" ? currentEntries.get(relativePath) : runtimeEntries.get(relativePath)) ?? null;
607
- if (!entry) {
1981
+ async function reserveSharedSessionClient(options) {
1982
+ while (true) {
1983
+ const outcome = await withPersistentProfileControlLock(
1984
+ options.persistentProfile.userDataDir,
1985
+ async () => {
1986
+ if (await isPersistentProfileWriteLocked(
1987
+ options.persistentProfile.userDataDir
1988
+ )) {
1989
+ return { kind: "wait" };
1990
+ }
1991
+ if (await hasActiveRuntimeProfileCreations(
1992
+ options.persistentProfile.userDataDir
1993
+ )) {
1994
+ return { kind: "wait" };
1995
+ }
1996
+ return await withSharedSessionLock(
1997
+ options.persistentProfile.userDataDir,
1998
+ async () => {
1999
+ const state = await inspectSharedSessionState(options);
2000
+ if (state.kind === "wait") {
2001
+ return { kind: "wait" };
2002
+ }
2003
+ if (state.kind === "ready") {
2004
+ return {
2005
+ kind: "ready",
2006
+ reservation: await registerSharedSessionClient(
2007
+ options.persistentProfile.userDataDir,
2008
+ state.metadata
2009
+ )
2010
+ };
2011
+ }
2012
+ return {
2013
+ kind: "launch",
2014
+ reservation: await launchSharedSession(options)
2015
+ };
2016
+ }
2017
+ );
2018
+ }
2019
+ );
2020
+ if (outcome.kind === "wait") {
2021
+ await sleep5(SHARED_SESSION_RETRY_DELAY_MS2);
608
2022
  continue;
609
2023
  }
610
- const targetPath = join(targetUserDataDir, relativePath);
611
- if (entry.kind === "directory") {
612
- await mkdir(targetPath, { recursive: true });
613
- continue;
2024
+ if (outcome.kind === "ready") {
2025
+ return outcome.reservation;
2026
+ }
2027
+ try {
2028
+ await waitForSharedSessionReady(
2029
+ outcome.reservation.metadata,
2030
+ options.timeoutMs
2031
+ );
2032
+ } catch (error) {
2033
+ await cleanupFailedSharedSessionLaunch(outcome.reservation);
2034
+ throw error;
2035
+ }
2036
+ try {
2037
+ return await withSharedSessionLock(
2038
+ options.persistentProfile.userDataDir,
2039
+ async () => {
2040
+ const metadata = await readSharedSessionMetadata(
2041
+ options.persistentProfile.userDataDir
2042
+ );
2043
+ if (!metadata || metadata.sessionId !== outcome.reservation.metadata.sessionId || !processOwnersEqual(
2044
+ metadata.browserOwner,
2045
+ outcome.reservation.launchedBrowserOwner
2046
+ )) {
2047
+ throw new Error(
2048
+ "The shared real-browser session changed before launch finalized."
2049
+ );
2050
+ }
2051
+ const readyMetadata = {
2052
+ ...metadata,
2053
+ state: "ready"
2054
+ };
2055
+ await writeSharedSessionMetadata(
2056
+ options.persistentProfile.userDataDir,
2057
+ readyMetadata
2058
+ );
2059
+ return await registerSharedSessionClient(
2060
+ options.persistentProfile.userDataDir,
2061
+ readyMetadata
2062
+ );
2063
+ }
2064
+ );
2065
+ } catch (error) {
2066
+ await cleanupFailedSharedSessionLaunch(outcome.reservation);
2067
+ throw error;
614
2068
  }
615
- await mkdir(dirname(targetPath), { recursive: true });
616
- await copyFile(entry.sourcePath, targetPath);
617
- }
618
- }
619
- function compareSnapshotPaths(left, right) {
620
- const leftDepth = left.split(sep).length;
621
- const rightDepth = right.split(sep).length;
622
- if (leftDepth !== rightDepth) {
623
- return leftDepth - rightDepth;
624
2069
  }
625
- return left.localeCompare(right);
626
2070
  }
627
- async function hashFile(filePath) {
628
- return new Promise((resolve, reject) => {
629
- const hash = createHash("sha256");
630
- const stream = createReadStream(filePath);
631
- stream.on("data", (chunk) => {
632
- hash.update(chunk);
2071
+ async function attachToSharedSession(reservation, options) {
2072
+ let browser = null;
2073
+ let page = null;
2074
+ try {
2075
+ const browserWsUrl = await resolveCdpWebSocketUrl(
2076
+ buildSharedSessionDiscoveryUrl(reservation.metadata.debugPort),
2077
+ options.timeoutMs
2078
+ );
2079
+ browser = await chromium.connectOverCDP(browserWsUrl, {
2080
+ timeout: options.timeoutMs
633
2081
  });
634
- stream.on("error", reject);
635
- stream.on("end", () => {
636
- resolve(hash.digest("hex"));
2082
+ const context = getPrimaryBrowserContext(browser);
2083
+ page = await getSharedSessionPage(context, reservation.reuseExistingPage);
2084
+ if (options.initialUrl) {
2085
+ await page.goto(options.initialUrl, {
2086
+ timeout: options.timeoutMs,
2087
+ waitUntil: "domcontentloaded"
2088
+ });
2089
+ }
2090
+ return {
2091
+ browser,
2092
+ clientId: reservation.client.clientId,
2093
+ context,
2094
+ page,
2095
+ persistentUserDataDir: reservation.metadata.persistentUserDataDir,
2096
+ sessionId: reservation.metadata.sessionId
2097
+ };
2098
+ } catch (error) {
2099
+ if (page) {
2100
+ await page.close().catch(() => void 0);
2101
+ }
2102
+ if (browser) {
2103
+ await browser.close().catch(() => void 0);
2104
+ }
2105
+ await cleanupFailedSharedSessionAttach({
2106
+ clientId: reservation.client.clientId,
2107
+ persistentUserDataDir: reservation.metadata.persistentUserDataDir,
2108
+ sessionId: reservation.metadata.sessionId
637
2109
  });
638
- });
2110
+ throw error;
2111
+ }
639
2112
  }
640
- async function ensurePersistentProfileMetadata(userDataDir, metadata) {
641
- if (existsSync(join(userDataDir, OPENSTEER_META_FILE))) {
2113
+ async function releaseSharedSessionClient(context) {
2114
+ const releasePlan = await prepareSharedSessionCloseIfIdle(
2115
+ context.persistentUserDataDir,
2116
+ context.clientId,
2117
+ context.sessionId
2118
+ );
2119
+ if (releasePlan.closeBrowser) {
2120
+ await closeSharedSessionBrowser(
2121
+ context.persistentUserDataDir,
2122
+ releasePlan,
2123
+ context.browser
2124
+ );
642
2125
  return;
643
2126
  }
644
- await writePersistentProfileMetadata(userDataDir, metadata);
2127
+ await context.page.close().catch(() => void 0);
2128
+ await context.browser.close().catch(() => void 0);
645
2129
  }
646
- async function readPersistentProfileMetadata(userDataDir) {
647
- try {
648
- const raw = await readFile(join(userDataDir, OPENSTEER_META_FILE), "utf8");
649
- const parsed = JSON.parse(raw);
650
- if (typeof parsed.createdAt !== "string" || typeof parsed.profileDirectory !== "string" || typeof parsed.source !== "string") {
651
- return null;
2130
+ async function inspectSharedSessionState(options) {
2131
+ const persistentUserDataDir = options.persistentProfile.userDataDir;
2132
+ const liveClients = await listLiveSharedSessionClients(persistentUserDataDir);
2133
+ const metadata = await readSharedSessionMetadata(persistentUserDataDir);
2134
+ if (!metadata) {
2135
+ if (liveClients.length > 0) {
2136
+ throw new Error(
2137
+ `Shared real-browser session metadata for "${persistentUserDataDir}" is missing while clients are still attached.`
2138
+ );
652
2139
  }
2140
+ await rm4(buildSharedSessionDirPath(persistentUserDataDir), {
2141
+ force: true,
2142
+ recursive: true
2143
+ }).catch(() => void 0);
2144
+ return { kind: "missing" };
2145
+ }
2146
+ assertSharedSessionCompatibility(metadata, options);
2147
+ const browserState = await getProcessLiveness(metadata.browserOwner);
2148
+ if (browserState === "dead") {
2149
+ await rm4(buildSharedSessionDirPath(persistentUserDataDir), {
2150
+ force: true,
2151
+ recursive: true
2152
+ }).catch(() => void 0);
2153
+ return { kind: "missing" };
2154
+ }
2155
+ if (metadata.state === "ready") {
653
2156
  return {
654
- createdAt: parsed.createdAt,
655
- profileDirectory: parsed.profileDirectory,
656
- source: parsed.source
2157
+ kind: "ready",
2158
+ metadata
657
2159
  };
658
- } catch {
659
- return null;
660
2160
  }
661
- }
662
- async function requirePersistentProfileMetadata(userDataDir) {
663
- const metadata = await readPersistentProfileMetadata(userDataDir);
664
- if (!metadata) {
665
- throw new Error(
666
- `Persistent profile metadata was not found for "${userDataDir}".`
667
- );
2161
+ const stateOwnerState = await getProcessLiveness(metadata.stateOwner);
2162
+ if (stateOwnerState === "dead") {
2163
+ const recoveredMetadata = {
2164
+ ...metadata,
2165
+ state: "ready"
2166
+ };
2167
+ await writeSharedSessionMetadata(persistentUserDataDir, recoveredMetadata);
2168
+ return {
2169
+ kind: "ready",
2170
+ metadata: recoveredMetadata
2171
+ };
668
2172
  }
669
- return metadata;
670
- }
671
- function wasDirPublishedByAnotherProcess(error, targetDirPath) {
672
- const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
673
- return existsSync(targetDirPath) && (code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM");
674
- }
675
- async function replaceProfileDirectory(targetUserDataDir, replacementUserDataDir) {
676
- if (!existsSync(targetUserDataDir)) {
677
- await rename(replacementUserDataDir, targetUserDataDir);
678
- return;
2173
+ return { kind: "wait" };
2174
+ }
2175
+ async function launchSharedSession(options) {
2176
+ const persistentUserDataDir = options.persistentProfile.userDataDir;
2177
+ await clearPersistentProfileSingletons(persistentUserDataDir);
2178
+ const debugPort = await reserveDebugPort();
2179
+ const launchArgs = buildRealBrowserLaunchArgs({
2180
+ debugPort,
2181
+ headless: options.headless,
2182
+ profileDirectory: options.profileDirectory,
2183
+ userDataDir: persistentUserDataDir
2184
+ });
2185
+ const processPolicy = getOwnedRealBrowserProcessPolicy();
2186
+ const processHandle = spawn(options.executablePath, launchArgs, {
2187
+ detached: processPolicy.detached,
2188
+ stdio: "ignore"
2189
+ });
2190
+ if (processPolicy.shouldUnref) {
2191
+ processHandle.unref();
679
2192
  }
680
- const backupUserDataDir = buildPersistentProfileBackupDirPath(targetUserDataDir);
681
- let targetMovedToBackup = false;
682
- let replacementPublished = false;
683
2193
  try {
684
- await rename(targetUserDataDir, backupUserDataDir);
685
- targetMovedToBackup = true;
686
- await rename(replacementUserDataDir, targetUserDataDir);
687
- replacementPublished = true;
2194
+ const browserOwner = await waitForSpawnedProcessOwner(
2195
+ processHandle.pid,
2196
+ options.timeoutMs
2197
+ );
2198
+ const metadata = {
2199
+ browserOwner,
2200
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2201
+ debugPort,
2202
+ executablePath: options.executablePath,
2203
+ headless: options.headless,
2204
+ persistentUserDataDir,
2205
+ profileDirectory: options.profileDirectory,
2206
+ sessionId: randomUUID4(),
2207
+ state: "launching",
2208
+ stateOwner: CURRENT_PROCESS_OWNER
2209
+ };
2210
+ await writeSharedSessionMetadata(persistentUserDataDir, metadata);
2211
+ return {
2212
+ launchedBrowserOwner: browserOwner,
2213
+ metadata
2214
+ };
688
2215
  } catch (error) {
689
- if (targetMovedToBackup && !existsSync(targetUserDataDir)) {
690
- await rename(backupUserDataDir, targetUserDataDir).catch(() => void 0);
691
- }
2216
+ await killSpawnedBrowserProcess(processHandle);
2217
+ await rm4(buildSharedSessionDirPath(persistentUserDataDir), {
2218
+ force: true,
2219
+ recursive: true
2220
+ }).catch(() => void 0);
692
2221
  throw error;
693
- } finally {
694
- if (replacementPublished && targetMovedToBackup && existsSync(backupUserDataDir)) {
695
- await rm(backupUserDataDir, {
696
- recursive: true,
697
- force: true
698
- }).catch(() => void 0);
699
- }
700
2222
  }
701
2223
  }
702
- async function withPersistentProfileLock(targetUserDataDir, action) {
703
- const lockDirPath = buildPersistentProfileLockDirPath(targetUserDataDir);
704
- await mkdir(dirname(lockDirPath), { recursive: true });
705
- while (true) {
706
- const tempLockDirPath = `${lockDirPath}-${process.pid}-${PROCESS_STARTED_AT_MS}-${randomUUID()}`;
707
- try {
708
- await mkdir(tempLockDirPath);
709
- await writeLockOwner(tempLockDirPath, CURRENT_PROCESS_LOCK_OWNER);
710
- try {
711
- await rename(tempLockDirPath, lockDirPath);
712
- break;
713
- } catch (error) {
714
- if (!wasDirPublishedByAnotherProcess(error, lockDirPath)) {
715
- throw error;
2224
+ async function cleanupFailedSharedSessionLaunch(reservation) {
2225
+ const shouldPreserveLiveBrowser = await withSharedSessionLock(
2226
+ reservation.metadata.persistentUserDataDir,
2227
+ async () => {
2228
+ const metadata = await readSharedSessionMetadata(
2229
+ reservation.metadata.persistentUserDataDir
2230
+ );
2231
+ if (metadata && metadata.sessionId === reservation.metadata.sessionId && processOwnersEqual(
2232
+ metadata.browserOwner,
2233
+ reservation.launchedBrowserOwner
2234
+ )) {
2235
+ if (await getProcessLiveness(metadata.browserOwner) !== "dead") {
2236
+ const readyMetadata = {
2237
+ ...metadata,
2238
+ state: "ready"
2239
+ };
2240
+ await writeSharedSessionMetadata(
2241
+ reservation.metadata.persistentUserDataDir,
2242
+ readyMetadata
2243
+ );
2244
+ return true;
716
2245
  }
2246
+ await rm4(
2247
+ buildSharedSessionDirPath(
2248
+ reservation.metadata.persistentUserDataDir
2249
+ ),
2250
+ {
2251
+ force: true,
2252
+ recursive: true
2253
+ }
2254
+ ).catch(() => void 0);
717
2255
  }
718
- } finally {
719
- await rm(tempLockDirPath, {
720
- recursive: true,
721
- force: true
722
- }).catch(() => void 0);
723
- }
724
- const owner = await readLockOwner(lockDirPath);
725
- if ((!owner || await getProcessLiveness(owner) === "dead") && await tryReclaimStaleLock(lockDirPath, owner)) {
726
- continue;
2256
+ return false;
727
2257
  }
728
- await sleep(PROFILE_LOCK_RETRY_DELAY_MS);
729
- }
730
- try {
731
- return await action();
732
- } finally {
733
- await rm(lockDirPath, {
734
- recursive: true,
735
- force: true
736
- }).catch(() => void 0);
2258
+ );
2259
+ if (shouldPreserveLiveBrowser) {
2260
+ return;
737
2261
  }
2262
+ await killOwnedBrowserProcess(reservation.launchedBrowserOwner);
2263
+ await waitForProcessToExit(reservation.launchedBrowserOwner, 2e3);
738
2264
  }
739
- async function writeLockOwner(lockDirPath, owner) {
740
- await writeLockParticipant(join(lockDirPath, LOCK_OWNER_FILE), owner);
2265
+ async function cleanupFailedSharedSessionAttach(options) {
2266
+ const closePlan = await prepareSharedSessionCloseIfIdle(
2267
+ options.persistentUserDataDir,
2268
+ options.clientId,
2269
+ options.sessionId
2270
+ );
2271
+ if (!closePlan.closeBrowser) {
2272
+ return;
2273
+ }
2274
+ await closeSharedSessionBrowser(options.persistentUserDataDir, closePlan);
741
2275
  }
742
- async function readLockOwner(lockDirPath) {
743
- return await readLockParticipant(join(lockDirPath, LOCK_OWNER_FILE));
2276
+ async function waitForSharedSessionReady(metadata, timeoutMs) {
2277
+ await resolveCdpWebSocketUrl(
2278
+ buildSharedSessionDiscoveryUrl(metadata.debugPort),
2279
+ timeoutMs
2280
+ );
744
2281
  }
745
- async function writeLockParticipant(filePath, owner, options) {
746
- await writeFile(filePath, JSON.stringify(owner), options);
2282
+ function buildRealBrowserLaunchArgs(options) {
2283
+ const args = [
2284
+ `--user-data-dir=${options.userDataDir}`,
2285
+ `--profile-directory=${options.profileDirectory}`,
2286
+ `--remote-debugging-port=${options.debugPort}`,
2287
+ "--disable-blink-features=AutomationControlled"
2288
+ ];
2289
+ if (options.headless) {
2290
+ args.push("--headless=new");
2291
+ }
2292
+ return args;
747
2293
  }
748
- async function readLockParticipant(filePath) {
749
- return (await readLockParticipantRecord(filePath)).owner;
2294
+ async function requestBrowserShutdown(browser) {
2295
+ let session = null;
2296
+ try {
2297
+ session = await browser.newBrowserCDPSession();
2298
+ await session.send("Browser.close");
2299
+ } catch {
2300
+ } finally {
2301
+ await session?.detach().catch(() => void 0);
2302
+ }
750
2303
  }
751
- async function readLockReclaimerRecord(lockDirPath) {
752
- return await readLockParticipantRecord(
753
- join(buildLockReclaimerDirPath(lockDirPath), LOCK_OWNER_FILE)
754
- );
2304
+ async function killOwnedBrowserProcess(owner) {
2305
+ if (await getProcessLiveness(owner) === "dead") {
2306
+ return;
2307
+ }
2308
+ await killOwnedBrowserProcessByPid(owner.pid);
755
2309
  }
756
- async function readLockParticipantRecord(filePath) {
757
- try {
758
- const raw = await readFile(filePath, "utf8");
759
- const parsed = JSON.parse(raw);
760
- const pid = Number(parsed.pid);
761
- const processStartedAtMs = Number(parsed.processStartedAtMs);
762
- if (!Number.isInteger(pid) || !Number.isInteger(processStartedAtMs)) {
763
- return {
764
- exists: true,
765
- owner: null
766
- };
767
- }
768
- return {
769
- exists: true,
770
- owner: {
771
- pid,
772
- processStartedAtMs
773
- }
774
- };
775
- } catch (error) {
776
- return {
777
- exists: getErrorCode(error) !== "ENOENT",
778
- owner: null
779
- };
2310
+ async function killSpawnedBrowserProcess(processHandle) {
2311
+ const pid = processHandle.pid;
2312
+ if (!pid || processHandle.exitCode !== null) {
2313
+ return;
780
2314
  }
2315
+ await killOwnedBrowserProcessByPid(pid);
2316
+ await waitForPidToExit(pid, 2e3);
781
2317
  }
782
- async function tryReclaimStaleLock(lockDirPath, expectedOwner) {
783
- if (!await tryAcquireLockReclaimer(lockDirPath)) {
784
- return false;
2318
+ async function killOwnedBrowserProcessByPid(pid) {
2319
+ const processPolicy = getOwnedRealBrowserProcessPolicy();
2320
+ if (processPolicy.killStrategy === "taskkill") {
2321
+ await new Promise((resolve) => {
2322
+ const killer = spawn(
2323
+ "taskkill",
2324
+ ["/pid", String(pid), "/t", "/f"],
2325
+ {
2326
+ stdio: "ignore"
2327
+ }
2328
+ );
2329
+ killer.on("error", () => resolve());
2330
+ killer.on("exit", () => resolve());
2331
+ });
2332
+ return;
785
2333
  }
786
- let reclaimed = false;
787
- try {
788
- const owner = await readLockOwner(lockDirPath);
789
- if (!lockOwnersEqual(owner, expectedOwner)) {
790
- return false;
791
- }
792
- if (owner && await getProcessLiveness(owner) !== "dead") {
793
- return false;
794
- }
795
- await rm(lockDirPath, {
796
- recursive: true,
797
- force: true
798
- }).catch(() => void 0);
799
- reclaimed = !existsSync(lockDirPath);
800
- return reclaimed;
801
- } finally {
802
- if (!reclaimed) {
803
- await rm(buildLockReclaimerDirPath(lockDirPath), {
804
- recursive: true,
805
- force: true
806
- }).catch(() => void 0);
2334
+ if (processPolicy.killStrategy === "process-group") {
2335
+ try {
2336
+ process.kill(-pid, "SIGKILL");
2337
+ return;
2338
+ } catch {
807
2339
  }
808
2340
  }
2341
+ try {
2342
+ process.kill(pid, "SIGKILL");
2343
+ } catch {
2344
+ }
809
2345
  }
810
- async function tryAcquireLockReclaimer(lockDirPath) {
811
- const reclaimerDirPath = buildLockReclaimerDirPath(lockDirPath);
812
- while (true) {
813
- const tempReclaimerDirPath = `${reclaimerDirPath}-${process.pid}-${PROCESS_STARTED_AT_MS}-${randomUUID()}`;
814
- try {
815
- await mkdir(tempReclaimerDirPath);
816
- await writeLockOwner(tempReclaimerDirPath, CURRENT_PROCESS_LOCK_OWNER);
817
- try {
818
- await rename(tempReclaimerDirPath, reclaimerDirPath);
819
- return true;
820
- } catch (error) {
821
- if (getErrorCode(error) === "ENOENT") {
822
- return false;
823
- }
824
- if (!wasDirPublishedByAnotherProcess(error, reclaimerDirPath)) {
825
- throw error;
826
- }
827
- }
828
- } catch (error) {
829
- const code = getErrorCode(error);
830
- if (code === "ENOENT") {
831
- return false;
832
- }
833
- throw error;
834
- } finally {
835
- await rm(tempReclaimerDirPath, {
836
- recursive: true,
837
- force: true
838
- }).catch(() => void 0);
839
- }
840
- const reclaimerRecord = await readLockReclaimerRecord(lockDirPath);
841
- if (!reclaimerRecord.exists || !reclaimerRecord.owner) {
842
- return false;
843
- }
844
- if (await getProcessLiveness(reclaimerRecord.owner) !== "dead") {
845
- return false;
2346
+ async function waitForProcessToExit(owner, timeoutMs) {
2347
+ const deadline = Date.now() + timeoutMs;
2348
+ while (Date.now() < deadline) {
2349
+ if (await getProcessLiveness(owner) === "dead") {
2350
+ return;
846
2351
  }
847
- await rm(reclaimerDirPath, {
848
- recursive: true,
849
- force: true
850
- }).catch(() => void 0);
2352
+ await sleep5(50);
851
2353
  }
852
2354
  }
853
- function lockOwnersEqual(left, right) {
854
- if (!left || !right) {
855
- return left === right;
2355
+ async function waitForPidToExit(pid, timeoutMs) {
2356
+ const deadline = Date.now() + timeoutMs;
2357
+ while (Date.now() < deadline) {
2358
+ if (!isProcessRunning(pid)) {
2359
+ return;
2360
+ }
2361
+ await sleep5(50);
856
2362
  }
857
- return left.pid === right.pid && left.processStartedAtMs === right.processStartedAtMs;
858
2363
  }
859
- async function getProcessLiveness(owner) {
860
- if (owner.pid === process.pid && hasMatchingProcessStartTime(
861
- owner.processStartedAtMs,
862
- PROCESS_STARTED_AT_MS
863
- )) {
864
- return "live";
2364
+ async function waitForSpawnedProcessOwner(pid, timeoutMs) {
2365
+ if (!pid || pid <= 0) {
2366
+ throw new Error("Chrome did not expose a child process id.");
865
2367
  }
866
- const startedAtMs = await readProcessStartedAtMs(owner.pid);
867
- if (typeof startedAtMs === "number") {
868
- return hasMatchingProcessStartTime(
869
- owner.processStartedAtMs,
870
- startedAtMs
871
- ) ? "live" : "dead";
2368
+ const deadline = Date.now() + timeoutMs;
2369
+ while (Date.now() < deadline) {
2370
+ const owner = await readProcessOwner(pid);
2371
+ if (owner) {
2372
+ return owner;
2373
+ }
2374
+ await sleep5(50);
872
2375
  }
873
- return isProcessRunning(owner.pid) ? "unknown" : "dead";
874
- }
875
- function hasMatchingProcessStartTime(expectedStartedAtMs, actualStartedAtMs) {
876
- return Math.abs(expectedStartedAtMs - actualStartedAtMs) <= PROCESS_START_TIME_TOLERANCE_MS;
877
- }
878
- function buildPersistentProfileTempDirPrefix(targetUserDataDir) {
879
- return join(
880
- dirname(targetUserDataDir),
881
- `${buildPersistentProfileTempDirNamePrefix(targetUserDataDir)}${process.pid}-${PROCESS_STARTED_AT_MS}-`
2376
+ throw new Error(
2377
+ `Chrome process ${pid} did not report a stable process start time.`
882
2378
  );
883
2379
  }
884
- function buildPersistentProfileTempDirNamePrefix(targetUserDataDir) {
885
- return `${basename(targetUserDataDir)}-tmp-`;
886
- }
887
- function buildPersistentProfileBackupDirPath(targetUserDataDir) {
888
- return join(
889
- dirname(targetUserDataDir),
890
- `${buildPersistentProfileTempDirNamePrefix(targetUserDataDir)}${process.pid}-${PROCESS_STARTED_AT_MS}-backup-${Date.now()}`
2380
+ async function withSharedSessionLock(persistentUserDataDir, action) {
2381
+ return await withDirLock(
2382
+ buildSharedSessionLockPath(persistentUserDataDir),
2383
+ action
891
2384
  );
892
2385
  }
893
- function buildPersistentProfileLockDirPath(targetUserDataDir) {
894
- return join(dirname(targetUserDataDir), `${basename(targetUserDataDir)}.lock`);
895
- }
896
- function buildLockReclaimerDirPath(lockDirPath) {
897
- return join(lockDirPath, LOCK_RECLAIMER_DIR);
898
- }
899
- function buildRuntimeProfileKey(sourceUserDataDir) {
900
- const hash = createHash("sha256").update(sourceUserDataDir).digest("hex").slice(0, 16);
901
- return `${sanitizePathSegment(basename(sourceUserDataDir) || "profile")}-${hash}`;
902
- }
903
- function buildRuntimeProfileDirNamePrefix(sourceUserDataDir) {
904
- return `${buildRuntimeProfileKey(sourceUserDataDir)}-runtime-`;
905
- }
906
- function buildRuntimeProfileDirPrefix(runtimesRootDir, sourceUserDataDir) {
907
- return join(
908
- runtimesRootDir,
909
- `${buildRuntimeProfileDirNamePrefix(sourceUserDataDir)}${process.pid}-${PROCESS_STARTED_AT_MS}-`
2386
+ async function registerSharedSessionClient(persistentUserDataDir, metadata) {
2387
+ const liveClients = await listLiveSharedSessionClients(persistentUserDataDir);
2388
+ const client = buildSharedSessionClientRegistration();
2389
+ await mkdir4(buildSharedSessionClientsDirPath(persistentUserDataDir), {
2390
+ recursive: true
2391
+ });
2392
+ await writeFile4(
2393
+ buildSharedSessionClientPath(persistentUserDataDir, client.clientId),
2394
+ JSON.stringify(client, null, 2),
2395
+ {
2396
+ flag: "wx"
2397
+ }
910
2398
  );
2399
+ return {
2400
+ client,
2401
+ metadata,
2402
+ reuseExistingPage: liveClients.length === 0
2403
+ };
911
2404
  }
912
- async function cleanOrphanedOwnedDirs(rootDir, ownedDirNamePrefix) {
2405
+ async function removeSharedSessionClientRegistration(persistentUserDataDir, clientId) {
2406
+ await rm4(buildSharedSessionClientPath(persistentUserDataDir, clientId), {
2407
+ force: true
2408
+ }).catch(() => void 0);
2409
+ }
2410
+ async function listLiveSharedSessionClients(persistentUserDataDir) {
2411
+ const clientsDirPath = buildSharedSessionClientsDirPath(persistentUserDataDir);
913
2412
  let entries;
914
2413
  try {
915
- entries = await readdir(rootDir, {
2414
+ entries = await readdir3(clientsDirPath, {
916
2415
  encoding: "utf8",
917
2416
  withFileTypes: true
918
2417
  });
919
2418
  } catch {
920
- return;
2419
+ return [];
921
2420
  }
922
- await Promise.all(
923
- entries.map(async (entry) => {
924
- if (!entry.isDirectory() || !entry.name.startsWith(ownedDirNamePrefix)) {
925
- return;
926
- }
927
- if (await isOwnedDirByLiveProcess(entry.name, ownedDirNamePrefix)) {
928
- return;
929
- }
930
- await rm(join(rootDir, entry.name), {
931
- recursive: true,
932
- force: true
933
- }).catch(() => void 0);
934
- })
935
- );
936
- }
937
- async function isOwnedDirByLiveProcess(ownedDirName, ownedDirPrefix) {
938
- const owner = parseOwnedDirOwner(ownedDirName, ownedDirPrefix);
939
- return owner ? await getProcessLiveness(owner) !== "dead" : false;
2421
+ const liveClients = [];
2422
+ for (const entry of entries) {
2423
+ if (!entry.isFile()) {
2424
+ continue;
2425
+ }
2426
+ const filePath = join5(clientsDirPath, entry.name);
2427
+ const registration = await readSharedSessionClientRegistration(filePath);
2428
+ if (!registration) {
2429
+ await rm4(filePath, { force: true }).catch(() => void 0);
2430
+ continue;
2431
+ }
2432
+ if (await getProcessLiveness(registration.owner) === "dead") {
2433
+ await rm4(filePath, { force: true }).catch(() => void 0);
2434
+ continue;
2435
+ }
2436
+ liveClients.push(registration);
2437
+ }
2438
+ return liveClients;
940
2439
  }
941
- function parseOwnedDirOwner(ownedDirName, ownedDirPrefix) {
942
- const remainder = ownedDirName.slice(ownedDirPrefix.length);
943
- const firstDashIndex = remainder.indexOf("-");
944
- const secondDashIndex = firstDashIndex === -1 ? -1 : remainder.indexOf("-", firstDashIndex + 1);
945
- if (firstDashIndex === -1 || secondDashIndex === -1) {
2440
+ async function readSharedSessionClientRegistration(filePath) {
2441
+ try {
2442
+ const raw = await readFile5(filePath, "utf8");
2443
+ return parseSharedSessionClientRegistration(JSON.parse(raw));
2444
+ } catch {
946
2445
  return null;
947
2446
  }
948
- const pid = Number.parseInt(remainder.slice(0, firstDashIndex), 10);
949
- const processStartedAtMs = Number.parseInt(
950
- remainder.slice(firstDashIndex + 1, secondDashIndex),
951
- 10
952
- );
953
- if (!Number.isInteger(pid) || pid <= 0) {
2447
+ }
2448
+ function buildSharedSessionClientRegistration() {
2449
+ return {
2450
+ clientId: randomUUID4(),
2451
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2452
+ owner: CURRENT_PROCESS_OWNER
2453
+ };
2454
+ }
2455
+ function parseSharedSessionClientRegistration(value) {
2456
+ if (!value || typeof value !== "object") {
954
2457
  return null;
955
2458
  }
956
- if (!Number.isInteger(processStartedAtMs) || processStartedAtMs <= 0) {
2459
+ const parsed = value;
2460
+ const owner = parseProcessOwner(parsed.owner);
2461
+ if (!owner || typeof parsed.clientId !== "string" || typeof parsed.createdAt !== "string") {
957
2462
  return null;
958
2463
  }
959
- return { pid, processStartedAtMs };
2464
+ return {
2465
+ clientId: parsed.clientId,
2466
+ createdAt: parsed.createdAt,
2467
+ owner
2468
+ };
960
2469
  }
961
- function isProcessRunning(pid) {
962
- try {
963
- process.kill(pid, 0);
964
- return true;
965
- } catch (error) {
966
- const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
967
- return code !== "ESRCH";
2470
+ function assertSharedSessionCompatibility(metadata, options) {
2471
+ if (metadata.executablePath !== options.executablePath) {
2472
+ throw new Error(
2473
+ `Chrome profile "${options.profileDirectory}" is already running with executable "${metadata.executablePath}", not "${options.executablePath}".`
2474
+ );
968
2475
  }
969
- }
970
- async function readProcessStartedAtMs(pid) {
971
- if (pid <= 0) {
972
- return null;
2476
+ if (metadata.headless !== options.headless) {
2477
+ throw new Error(
2478
+ `Chrome profile "${options.profileDirectory}" is already running with headless=${metadata.headless}, not ${options.headless}.`
2479
+ );
973
2480
  }
974
- if (process.platform === "linux") {
975
- return await readLinuxProcessStartedAtMs(pid);
2481
+ }
2482
+ async function prepareSharedSessionCloseIfIdle(persistentUserDataDir, clientId, sessionId) {
2483
+ return await withSharedSessionLock(persistentUserDataDir, async () => {
2484
+ const metadata = await readSharedSessionMetadata(persistentUserDataDir);
2485
+ await removeSharedSessionClientRegistration(
2486
+ persistentUserDataDir,
2487
+ clientId
2488
+ );
2489
+ if (!metadata || metadata.sessionId !== sessionId) {
2490
+ return {
2491
+ closeBrowser: false,
2492
+ sessionId
2493
+ };
2494
+ }
2495
+ const liveClients = await listLiveSharedSessionClients(
2496
+ persistentUserDataDir
2497
+ );
2498
+ if (liveClients.length > 0) {
2499
+ return {
2500
+ closeBrowser: false,
2501
+ sessionId: metadata.sessionId
2502
+ };
2503
+ }
2504
+ const closingMetadata = {
2505
+ ...metadata,
2506
+ state: "closing",
2507
+ stateOwner: CURRENT_PROCESS_OWNER
2508
+ };
2509
+ await writeSharedSessionMetadata(
2510
+ persistentUserDataDir,
2511
+ closingMetadata
2512
+ );
2513
+ return {
2514
+ browserOwner: closingMetadata.browserOwner,
2515
+ closeBrowser: true,
2516
+ sessionId: closingMetadata.sessionId
2517
+ };
2518
+ });
2519
+ }
2520
+ async function closeSharedSessionBrowser(persistentUserDataDir, closePlan, browser) {
2521
+ if (browser) {
2522
+ await requestBrowserShutdown(browser);
2523
+ await waitForProcessToExit(closePlan.browserOwner, 1e3);
976
2524
  }
977
- if (process.platform === "win32") {
978
- return await readWindowsProcessStartedAtMs(pid);
2525
+ if (await getProcessLiveness(closePlan.browserOwner) !== "dead") {
2526
+ await killOwnedBrowserProcess(closePlan.browserOwner);
2527
+ await waitForProcessToExit(closePlan.browserOwner, 2e3);
979
2528
  }
980
- return await readPsProcessStartedAtMs(pid);
2529
+ await finalizeSharedSessionClose(persistentUserDataDir, closePlan.sessionId);
981
2530
  }
982
- async function readLinuxProcessStartedAtMs(pid) {
983
- let statRaw;
984
- try {
985
- statRaw = await readFile(`/proc/${pid}/stat`, "utf8");
986
- } catch (error) {
987
- return null;
988
- }
989
- const startTicks = parseLinuxProcessStartTicks(statRaw);
990
- if (startTicks === null) {
991
- return null;
2531
+ async function finalizeSharedSessionClose(persistentUserDataDir, sessionId) {
2532
+ await withSharedSessionLock(persistentUserDataDir, async () => {
2533
+ const metadata = await readSharedSessionMetadata(persistentUserDataDir);
2534
+ if (!metadata || metadata.sessionId !== sessionId) {
2535
+ return;
2536
+ }
2537
+ const liveClients = await listLiveSharedSessionClients(
2538
+ persistentUserDataDir
2539
+ );
2540
+ if (liveClients.length > 0) {
2541
+ const readyMetadata = {
2542
+ ...metadata,
2543
+ state: "ready"
2544
+ };
2545
+ await writeSharedSessionMetadata(
2546
+ persistentUserDataDir,
2547
+ readyMetadata
2548
+ );
2549
+ return;
2550
+ }
2551
+ if (await getProcessLiveness(metadata.browserOwner) !== "dead") {
2552
+ const readyMetadata = {
2553
+ ...metadata,
2554
+ state: "ready"
2555
+ };
2556
+ await writeSharedSessionMetadata(
2557
+ persistentUserDataDir,
2558
+ readyMetadata
2559
+ );
2560
+ return;
2561
+ }
2562
+ await rm4(buildSharedSessionDirPath(persistentUserDataDir), {
2563
+ force: true,
2564
+ recursive: true
2565
+ }).catch(() => void 0);
2566
+ });
2567
+ }
2568
+ function getPrimaryBrowserContext(browser) {
2569
+ const contexts = browser.contexts();
2570
+ if (contexts.length === 0) {
2571
+ throw new Error(
2572
+ "Connection succeeded but no browser contexts were exposed."
2573
+ );
992
2574
  }
993
- const [bootTimeMs, clockTicksPerSecond] = await Promise.all([
994
- readLinuxBootTimeMs(),
995
- readLinuxClockTicksPerSecond()
996
- ]);
997
- if (bootTimeMs === null || clockTicksPerSecond === null) {
998
- return null;
2575
+ return contexts[0];
2576
+ }
2577
+ async function getSharedSessionPage(context, reuseExistingPage) {
2578
+ if (reuseExistingPage) {
2579
+ return await getExistingPageOrCreate(context);
999
2580
  }
1000
- return Math.floor(
1001
- bootTimeMs + startTicks * 1e3 / clockTicksPerSecond
1002
- );
2581
+ return await context.newPage();
1003
2582
  }
1004
- function parseLinuxProcessStartTicks(statRaw) {
1005
- const closingParenIndex = statRaw.lastIndexOf(")");
1006
- if (closingParenIndex === -1) {
1007
- return null;
2583
+ async function getExistingPageOrCreate(context) {
2584
+ const existingPage = context.pages()[0];
2585
+ if (existingPage) {
2586
+ return existingPage;
1008
2587
  }
1009
- const fields = statRaw.slice(closingParenIndex + 2).trim().split(/\s+/);
1010
- const startTicks = Number(fields[LINUX_STAT_START_TIME_FIELD_INDEX]);
1011
- return Number.isFinite(startTicks) && startTicks >= 0 ? startTicks : null;
2588
+ return await context.newPage();
1012
2589
  }
1013
- async function readLinuxBootTimeMs() {
1014
- try {
1015
- const statRaw = await readFile("/proc/stat", "utf8");
1016
- const bootTimeLine = statRaw.split("\n").find((line) => line.startsWith("btime "));
1017
- if (!bootTimeLine) {
1018
- return null;
2590
+ function buildSharedSessionDiscoveryUrl(debugPort) {
2591
+ return `http://127.0.0.1:${debugPort}`;
2592
+ }
2593
+ async function resolveCdpWebSocketUrl(cdpUrl, timeoutMs) {
2594
+ if (cdpUrl.startsWith("ws://") || cdpUrl.startsWith("wss://")) {
2595
+ return cdpUrl;
2596
+ }
2597
+ const versionUrl = normalizeDiscoveryUrl(cdpUrl);
2598
+ const deadline = Date.now() + timeoutMs;
2599
+ let lastError = "CDP discovery did not respond.";
2600
+ while (Date.now() < deadline) {
2601
+ const remaining = Math.max(deadline - Date.now(), 1e3);
2602
+ try {
2603
+ const response = await fetch(versionUrl, {
2604
+ signal: AbortSignal.timeout(Math.min(remaining, 5e3))
2605
+ });
2606
+ if (!response.ok) {
2607
+ lastError = `${response.status} ${response.statusText}`;
2608
+ } else {
2609
+ const payload = await response.json();
2610
+ const wsUrl = payload && typeof payload === "object" && !Array.isArray(payload) && typeof payload.webSocketDebuggerUrl === "string" ? payload.webSocketDebuggerUrl : null;
2611
+ if (wsUrl && wsUrl.trim()) {
2612
+ return wsUrl;
2613
+ }
2614
+ lastError = "CDP discovery response did not include webSocketDebuggerUrl.";
2615
+ }
2616
+ } catch (error) {
2617
+ lastError = error instanceof Error ? error.message : "Unknown error";
1019
2618
  }
1020
- const bootTimeSeconds = Number.parseInt(
1021
- bootTimeLine.slice("btime ".length),
1022
- 10
1023
- );
1024
- return Number.isFinite(bootTimeSeconds) ? bootTimeSeconds * 1e3 : null;
1025
- } catch {
1026
- return null;
2619
+ await sleep5(100);
1027
2620
  }
1028
- }
1029
- async function readLinuxClockTicksPerSecond() {
1030
- linuxClockTicksPerSecondPromise ??= execFileAsync(
1031
- "getconf",
1032
- ["CLK_TCK"]
1033
- ).then(
1034
- ({ stdout }) => {
1035
- const value = Number.parseInt(stdout.trim(), 10);
1036
- return Number.isFinite(value) && value > 0 ? value : null;
1037
- },
1038
- () => null
2621
+ throw new Error(
2622
+ `Failed to resolve a CDP websocket URL from ${versionUrl.toString()}: ${lastError}`
1039
2623
  );
1040
- return await linuxClockTicksPerSecondPromise;
1041
2624
  }
1042
- async function readPsProcessStartedAtMs(pid) {
2625
+ function normalizeDiscoveryUrl(cdpUrl) {
2626
+ let parsed;
1043
2627
  try {
1044
- const { stdout } = await execFileAsync(
1045
- "ps",
1046
- ["-p", String(pid), "-o", "lstart="],
1047
- { env: PS_COMMAND_ENV }
2628
+ parsed = new URL(cdpUrl);
2629
+ } catch {
2630
+ throw new Error(
2631
+ `Invalid CDP URL "${cdpUrl}". Use an http(s) or ws(s) endpoint.`
1048
2632
  );
1049
- return parsePsStartedAtMs(stdout);
1050
- } catch (error) {
1051
- return null;
1052
2633
  }
1053
- }
1054
- function parsePsStartedAtMs(stdout) {
1055
- const raw = stdout.trim();
1056
- if (!raw) {
1057
- return null;
2634
+ if (parsed.protocol === "ws:" || parsed.protocol === "wss:") {
2635
+ return parsed;
1058
2636
  }
1059
- const startedAtMs = Date.parse(raw);
1060
- return Number.isNaN(startedAtMs) ? null : startedAtMs;
1061
- }
1062
- async function readWindowsProcessStartedAtMs(pid) {
1063
- const script = [
1064
- "$process = Get-Process -Id " + String(pid) + " -ErrorAction SilentlyContinue",
1065
- "if ($null -eq $process) { exit 3 }",
1066
- '$process.StartTime.ToUniversalTime().ToString("o")'
1067
- ].join("; ");
1068
- try {
1069
- const { stdout } = await execFileAsync(
1070
- "powershell.exe",
1071
- ["-NoLogo", "-NoProfile", "-Command", script]
2637
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2638
+ throw new Error(
2639
+ `Unsupported CDP URL protocol "${parsed.protocol}". Use http(s) or ws(s).`
1072
2640
  );
1073
- return parsePsStartedAtMs(stdout);
1074
- } catch (error) {
1075
- return null;
1076
2641
  }
2642
+ const normalized = new URL(parsed.toString());
2643
+ normalized.pathname = "/json/version";
2644
+ normalized.search = "";
2645
+ normalized.hash = "";
2646
+ return normalized;
1077
2647
  }
1078
- function getErrorCode(error) {
1079
- return typeof error === "object" && error !== null && "code" in error && (typeof error.code === "string" || typeof error.code === "number") ? error.code : void 0;
2648
+ async function reserveDebugPort() {
2649
+ return await new Promise((resolve, reject) => {
2650
+ const server = createServer();
2651
+ server.unref();
2652
+ server.on("error", reject);
2653
+ server.listen(0, "127.0.0.1", () => {
2654
+ const address = server.address();
2655
+ if (!address || typeof address === "string") {
2656
+ server.close();
2657
+ reject(new Error("Failed to reserve a local debug port."));
2658
+ return;
2659
+ }
2660
+ server.close((error) => {
2661
+ if (error) {
2662
+ reject(error);
2663
+ return;
2664
+ }
2665
+ resolve(address.port);
2666
+ });
2667
+ });
2668
+ });
1080
2669
  }
1081
- async function sleep(ms) {
2670
+ async function sleep5(ms) {
1082
2671
  await new Promise((resolve) => setTimeout(resolve, ms));
1083
2672
  }
1084
2673
 
1085
2674
  // src/browser/pool.ts
1086
- import { spawn } from "child_process";
1087
- import { rm as rm2 } from "fs/promises";
1088
- import { createServer } from "net";
1089
2675
  import {
1090
- chromium
2676
+ chromium as chromium2
1091
2677
  } from "playwright";
1092
2678
 
1093
2679
  // src/browser/cdp-proxy.ts
@@ -1507,15 +3093,14 @@ function errorMessage(error) {
1507
3093
  // src/browser/pool.ts
1508
3094
  var BrowserPool = class {
1509
3095
  browser = null;
1510
- cdpProxy = null;
1511
- launchedProcess = null;
1512
- managedRuntimeProfile = null;
3096
+ activeSessionClose = null;
3097
+ closeInFlight = null;
1513
3098
  defaults;
1514
3099
  constructor(defaults = {}) {
1515
3100
  this.defaults = defaults;
1516
3101
  }
1517
3102
  async launch(options = {}) {
1518
- if (this.browser || this.cdpProxy || this.launchedProcess || this.managedRuntimeProfile) {
3103
+ if (this.browser || this.activeSessionClose) {
1519
3104
  await this.close();
1520
3105
  }
1521
3106
  const mode = options.mode ?? this.defaults.mode ?? "chromium";
@@ -1562,30 +3147,26 @@ var BrowserPool = class {
1562
3147
  return this.launchSandbox(options);
1563
3148
  }
1564
3149
  async close() {
1565
- const browser = this.browser;
1566
- const cdpProxy = this.cdpProxy;
1567
- const launchedProcess = this.launchedProcess;
1568
- const managedRuntimeProfile = this.managedRuntimeProfile;
1569
- this.browser = null;
1570
- this.cdpProxy = null;
1571
- this.launchedProcess = null;
3150
+ if (this.closeInFlight) {
3151
+ await this.closeInFlight;
3152
+ return;
3153
+ }
3154
+ const closeOperation = this.closeCurrent();
3155
+ this.closeInFlight = closeOperation;
1572
3156
  try {
1573
- if (browser) {
1574
- await browser.close().catch(() => void 0);
1575
- }
3157
+ await closeOperation;
3158
+ this.browser = null;
3159
+ this.activeSessionClose = null;
1576
3160
  } finally {
1577
- cdpProxy?.close();
1578
- await killProcessTree(launchedProcess);
1579
- if (managedRuntimeProfile) {
1580
- await persistIsolatedRuntimeProfile(
1581
- managedRuntimeProfile.userDataDir,
1582
- managedRuntimeProfile.persistentUserDataDir
1583
- );
1584
- if (this.managedRuntimeProfile === managedRuntimeProfile) {
1585
- this.managedRuntimeProfile = null;
1586
- }
1587
- }
3161
+ this.closeInFlight = null;
3162
+ }
3163
+ }
3164
+ async closeCurrent() {
3165
+ if (this.activeSessionClose) {
3166
+ await this.activeSessionClose();
3167
+ return;
1588
3168
  }
3169
+ await this.browser?.close().catch(() => void 0);
1589
3170
  }
1590
3171
  async connectToRunning(cdpUrl, timeout) {
1591
3172
  let browser = null;
@@ -1600,11 +3181,14 @@ var BrowserPool = class {
1600
3181
  }
1601
3182
  cdpProxy = new CDPProxy(browserWsUrl, targetId);
1602
3183
  const proxyWsUrl = await cdpProxy.start();
1603
- browser = await chromium.connectOverCDP(proxyWsUrl, {
3184
+ browser = await chromium2.connectOverCDP(proxyWsUrl, {
1604
3185
  timeout: timeout ?? 3e4
1605
3186
  });
1606
3187
  this.browser = browser;
1607
- this.cdpProxy = cdpProxy;
3188
+ this.activeSessionClose = async () => {
3189
+ await browser?.close().catch(() => void 0);
3190
+ cdpProxy?.close();
3191
+ };
1608
3192
  const { context, page } = await pickBrowserContextAndPage(browser);
1609
3193
  return { browser, context, page, isExternal: true };
1610
3194
  } catch (error) {
@@ -1613,7 +3197,7 @@ var BrowserPool = class {
1613
3197
  }
1614
3198
  cdpProxy?.close();
1615
3199
  this.browser = null;
1616
- this.cdpProxy = null;
3200
+ this.activeSessionClose = null;
1617
3201
  throw error;
1618
3202
  }
1619
3203
  }
@@ -1633,60 +3217,29 @@ var BrowserPool = class {
1633
3217
  sourceUserDataDir,
1634
3218
  profileDirectory
1635
3219
  );
1636
- const runtimeProfile = await createIsolatedRuntimeProfile(
1637
- persistentProfile.userDataDir
1638
- );
1639
- const debugPort = await reserveDebugPort();
1640
- const headless = resolveLaunchHeadless(
1641
- "real",
1642
- options.headless,
1643
- this.defaults.headless
1644
- );
1645
- const launchArgs = buildRealBrowserLaunchArgs({
1646
- userDataDir: runtimeProfile.userDataDir,
3220
+ const sharedSession = await acquireSharedRealBrowserSession({
3221
+ executablePath,
3222
+ headless: resolveLaunchHeadless(
3223
+ "real",
3224
+ options.headless,
3225
+ this.defaults.headless
3226
+ ),
3227
+ initialUrl: options.initialUrl,
3228
+ persistentProfile,
1647
3229
  profileDirectory,
1648
- debugPort,
1649
- headless
3230
+ timeoutMs: options.timeout ?? 3e4
1650
3231
  });
1651
- const processHandle = spawn(executablePath, launchArgs, {
1652
- detached: process.platform !== "win32",
1653
- stdio: "ignore"
1654
- });
1655
- processHandle.unref();
1656
- let browser = null;
1657
- try {
1658
- const wsUrl = await resolveCdpWebSocketUrl(
1659
- `http://127.0.0.1:${debugPort}`,
1660
- options.timeout ?? 3e4
1661
- );
1662
- browser = await chromium.connectOverCDP(wsUrl, {
1663
- timeout: options.timeout ?? 3e4
1664
- });
1665
- const { context, page } = await createOwnedBrowserContextAndPage(
1666
- browser
1667
- );
1668
- if (options.initialUrl) {
1669
- await page.goto(options.initialUrl, {
1670
- waitUntil: "domcontentloaded",
1671
- timeout: options.timeout ?? 3e4
1672
- });
1673
- }
1674
- this.browser = browser;
1675
- this.launchedProcess = processHandle;
1676
- this.managedRuntimeProfile = runtimeProfile;
1677
- return { browser, context, page, isExternal: false };
1678
- } catch (error) {
1679
- await browser?.close().catch(() => void 0);
1680
- await killProcessTree(processHandle);
1681
- await rm2(runtimeProfile.userDataDir, {
1682
- recursive: true,
1683
- force: true
1684
- }).catch(() => void 0);
1685
- throw error;
1686
- }
3232
+ this.browser = sharedSession.browser;
3233
+ this.activeSessionClose = sharedSession.close;
3234
+ return {
3235
+ browser: sharedSession.browser,
3236
+ context: sharedSession.context,
3237
+ page: sharedSession.page,
3238
+ isExternal: false
3239
+ };
1687
3240
  }
1688
3241
  async launchSandbox(options) {
1689
- const browser = await chromium.launch({
3242
+ const browser = await chromium2.launch({
1690
3243
  headless: resolveLaunchHeadless(
1691
3244
  "chromium",
1692
3245
  options.headless,
@@ -1699,11 +3252,14 @@ var BrowserPool = class {
1699
3252
  const context = await browser.newContext(options.context || {});
1700
3253
  const page = await context.newPage();
1701
3254
  this.browser = browser;
3255
+ this.activeSessionClose = async () => {
3256
+ await browser.close().catch(() => void 0);
3257
+ };
1702
3258
  return { browser, context, page, isExternal: false };
1703
3259
  }
1704
3260
  };
1705
3261
  async function pickBrowserContextAndPage(browser) {
1706
- const context = getPrimaryBrowserContext(browser);
3262
+ const context = getPrimaryBrowserContext2(browser);
1707
3263
  const page = await getAttachedPageOrCreate(context);
1708
3264
  return { context, page };
1709
3265
  }
@@ -1716,11 +3272,6 @@ function resolveLaunchHeadless(mode, requestedHeadless, defaultHeadless) {
1716
3272
  }
1717
3273
  return mode === "real";
1718
3274
  }
1719
- async function createOwnedBrowserContextAndPage(browser) {
1720
- const context = getPrimaryBrowserContext(browser);
1721
- const page = await getExistingPageOrCreate(context);
1722
- return { context, page };
1723
- }
1724
3275
  async function getAttachedPageOrCreate(context) {
1725
3276
  const pages = context.pages();
1726
3277
  const inspectablePage = pages.find(
@@ -1735,14 +3286,7 @@ async function getAttachedPageOrCreate(context) {
1735
3286
  }
1736
3287
  return await context.newPage();
1737
3288
  }
1738
- async function getExistingPageOrCreate(context) {
1739
- const existingPage = context.pages()[0];
1740
- if (existingPage) {
1741
- return existingPage;
1742
- }
1743
- return await context.newPage();
1744
- }
1745
- function getPrimaryBrowserContext(browser) {
3289
+ function getPrimaryBrowserContext2(browser) {
1746
3290
  const contexts = browser.contexts();
1747
3291
  if (contexts.length === 0) {
1748
3292
  throw new Error(
@@ -1761,125 +3305,6 @@ function safePageUrl(page) {
1761
3305
  return "";
1762
3306
  }
1763
3307
  }
1764
- function normalizeDiscoveryUrl(cdpUrl) {
1765
- let parsed;
1766
- try {
1767
- parsed = new URL(cdpUrl);
1768
- } catch {
1769
- throw new Error(
1770
- `Invalid CDP URL "${cdpUrl}". Use an http(s) or ws(s) endpoint.`
1771
- );
1772
- }
1773
- if (parsed.protocol === "ws:" || parsed.protocol === "wss:") {
1774
- return parsed;
1775
- }
1776
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1777
- throw new Error(
1778
- `Unsupported CDP URL protocol "${parsed.protocol}". Use http(s) or ws(s).`
1779
- );
1780
- }
1781
- const normalized = new URL(parsed.toString());
1782
- normalized.pathname = "/json/version";
1783
- normalized.search = "";
1784
- normalized.hash = "";
1785
- return normalized;
1786
- }
1787
- async function resolveCdpWebSocketUrl(cdpUrl, timeoutMs) {
1788
- if (cdpUrl.startsWith("ws://") || cdpUrl.startsWith("wss://")) {
1789
- return cdpUrl;
1790
- }
1791
- const versionUrl = normalizeDiscoveryUrl(cdpUrl);
1792
- const deadline = Date.now() + timeoutMs;
1793
- let lastError = "CDP discovery did not respond.";
1794
- while (Date.now() < deadline) {
1795
- const remaining = Math.max(deadline - Date.now(), 1e3);
1796
- try {
1797
- const response = await fetch(versionUrl, {
1798
- signal: AbortSignal.timeout(Math.min(remaining, 5e3))
1799
- });
1800
- if (!response.ok) {
1801
- lastError = `${response.status} ${response.statusText}`;
1802
- } else {
1803
- const payload = await response.json();
1804
- const wsUrl = payload && typeof payload === "object" && !Array.isArray(payload) && typeof payload.webSocketDebuggerUrl === "string" ? payload.webSocketDebuggerUrl : null;
1805
- if (wsUrl && wsUrl.trim()) {
1806
- return wsUrl;
1807
- }
1808
- lastError = "CDP discovery response did not include webSocketDebuggerUrl.";
1809
- }
1810
- } catch (error) {
1811
- lastError = error instanceof Error ? error.message : "Unknown error";
1812
- }
1813
- await sleep2(100);
1814
- }
1815
- throw new Error(
1816
- `Failed to resolve a CDP websocket URL from ${versionUrl.toString()}: ${lastError}`
1817
- );
1818
- }
1819
- async function reserveDebugPort() {
1820
- return await new Promise((resolve, reject) => {
1821
- const server = createServer();
1822
- server.unref();
1823
- server.on("error", reject);
1824
- server.listen(0, "127.0.0.1", () => {
1825
- const address = server.address();
1826
- if (!address || typeof address === "string") {
1827
- server.close();
1828
- reject(new Error("Failed to reserve a local debug port."));
1829
- return;
1830
- }
1831
- server.close((error) => {
1832
- if (error) {
1833
- reject(error);
1834
- return;
1835
- }
1836
- resolve(address.port);
1837
- });
1838
- });
1839
- });
1840
- }
1841
- function buildRealBrowserLaunchArgs(options) {
1842
- const args = [
1843
- `--user-data-dir=${options.userDataDir}`,
1844
- `--profile-directory=${options.profileDirectory}`,
1845
- `--remote-debugging-port=${options.debugPort}`,
1846
- "--disable-blink-features=AutomationControlled"
1847
- ];
1848
- if (options.headless) {
1849
- args.push("--headless=new");
1850
- }
1851
- return args;
1852
- }
1853
- async function killProcessTree(processHandle) {
1854
- if (!processHandle || processHandle.pid == null || processHandle.exitCode !== null) {
1855
- return;
1856
- }
1857
- if (process.platform === "win32") {
1858
- await new Promise((resolve) => {
1859
- const killer = spawn(
1860
- "taskkill",
1861
- ["/pid", String(processHandle.pid), "/t", "/f"],
1862
- {
1863
- stdio: "ignore"
1864
- }
1865
- );
1866
- killer.on("error", () => resolve());
1867
- killer.on("exit", () => resolve());
1868
- });
1869
- return;
1870
- }
1871
- try {
1872
- process.kill(-processHandle.pid, "SIGKILL");
1873
- } catch {
1874
- try {
1875
- processHandle.kill("SIGKILL");
1876
- } catch {
1877
- }
1878
- }
1879
- }
1880
- async function sleep2(ms) {
1881
- await new Promise((resolve) => setTimeout(resolve, ms));
1882
- }
1883
3308
 
1884
3309
  // src/navigation.ts
1885
3310
  var DEFAULT_TIMEOUT = 3e4;
@@ -2259,7 +3684,7 @@ var StealthCdpRuntime = class _StealthCdpRuntime {
2259
3684
  TRANSIENT_CONTEXT_RETRY_DELAY_MS,
2260
3685
  Math.max(0, deadline - Date.now())
2261
3686
  );
2262
- await sleep3(retryDelay);
3687
+ await sleep6(retryDelay);
2263
3688
  }
2264
3689
  }
2265
3690
  }
@@ -2292,7 +3717,7 @@ var StealthCdpRuntime = class _StealthCdpRuntime {
2292
3717
  () => ({ kind: "resolved" }),
2293
3718
  (error) => ({ kind: "rejected", error })
2294
3719
  );
2295
- const timeoutPromise = sleep3(
3720
+ const timeoutPromise = sleep6(
2296
3721
  timeout + FRAME_EVALUATE_GRACE_MS
2297
3722
  ).then(() => ({ kind: "timeout" }));
2298
3723
  const result = await Promise.race([
@@ -2434,7 +3859,7 @@ function isIgnorableFrameError(error) {
2434
3859
  const message = error.message;
2435
3860
  return message.includes("Frame was detached") || message.includes("Target page, context or browser has been closed") || isTransientExecutionContextError(error) || message.includes("No frame for given id found");
2436
3861
  }
2437
- function sleep3(ms) {
3862
+ function sleep6(ms) {
2438
3863
  return new Promise((resolve) => {
2439
3864
  setTimeout(resolve, ms);
2440
3865
  });
@@ -6686,7 +8111,7 @@ async function closeTab(context, activePage, index) {
6686
8111
  }
6687
8112
 
6688
8113
  // src/actions/cookies.ts
6689
- import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
8114
+ import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
6690
8115
  async function getCookies(context, url) {
6691
8116
  return context.cookies(url ? [url] : void 0);
6692
8117
  }
@@ -6698,10 +8123,10 @@ async function clearCookies(context) {
6698
8123
  }
6699
8124
  async function exportCookies(context, filePath, url) {
6700
8125
  const cookies = await context.cookies(url ? [url] : void 0);
6701
- await writeFile2(filePath, JSON.stringify(cookies, null, 2), "utf-8");
8126
+ await writeFile5(filePath, JSON.stringify(cookies, null, 2), "utf-8");
6702
8127
  }
6703
8128
  async function importCookies(context, filePath) {
6704
- const raw = await readFile2(filePath, "utf-8");
8129
+ const raw = await readFile6(filePath, "utf-8");
6705
8130
  const cookies = JSON.parse(raw);
6706
8131
  await context.addCookies(cookies);
6707
8132
  }
@@ -8529,7 +9954,7 @@ async function executeAgentAction(page, action) {
8529
9954
  }
8530
9955
  case "wait": {
8531
9956
  const ms = numberOr(action.timeMs, action.time_ms, 1e3);
8532
- await sleep4(ms);
9957
+ await sleep7(ms);
8533
9958
  return;
8534
9959
  }
8535
9960
  case "goto": {
@@ -8694,7 +10119,7 @@ async function pressKeyCombo(page, combo) {
8694
10119
  }
8695
10120
  }
8696
10121
  }
8697
- function sleep4(ms) {
10122
+ function sleep7(ms) {
8698
10123
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
8699
10124
  }
8700
10125
 
@@ -8725,7 +10150,7 @@ var OpensteerCuaAgentHandler = class {
8725
10150
  if (isMutatingAgentAction(action)) {
8726
10151
  this.onMutatingAction?.(action);
8727
10152
  }
8728
- await sleep5(this.config.waitBetweenActionsMs);
10153
+ await sleep8(this.config.waitBetweenActionsMs);
8729
10154
  });
8730
10155
  try {
8731
10156
  const result = await this.client.execute({
@@ -8787,7 +10212,7 @@ var OpensteerCuaAgentHandler = class {
8787
10212
  await this.cursorController.preview({ x, y }, "agent");
8788
10213
  }
8789
10214
  };
8790
- function sleep5(ms) {
10215
+ function sleep8(ms) {
8791
10216
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
8792
10217
  }
8793
10218
 
@@ -9223,7 +10648,7 @@ var CursorController = class {
9223
10648
  for (const step of motion.points) {
9224
10649
  await this.renderer.move(step, this.style);
9225
10650
  if (motion.stepDelayMs > 0) {
9226
- await sleep6(motion.stepDelayMs);
10651
+ await sleep9(motion.stepDelayMs);
9227
10652
  }
9228
10653
  }
9229
10654
  if (shouldPulse(intent)) {
@@ -9381,12 +10806,12 @@ function clamp2(value, min, max) {
9381
10806
  function shouldPulse(intent) {
9382
10807
  return intent === "click" || intent === "dblclick" || intent === "rightclick" || intent === "agent";
9383
10808
  }
9384
- function sleep6(ms) {
10809
+ function sleep9(ms) {
9385
10810
  return new Promise((resolve) => setTimeout(resolve, ms));
9386
10811
  }
9387
10812
 
9388
10813
  // src/opensteer.ts
9389
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
10814
+ import { createHash as createHash2, randomUUID as randomUUID5 } from "crypto";
9390
10815
 
9391
10816
  // src/action-wait.ts
9392
10817
  var ROBUST_PROFILE = {
@@ -9581,7 +11006,7 @@ var AdaptiveNetworkTracker = class {
9581
11006
  this.idleSince = 0;
9582
11007
  }
9583
11008
  const remaining = Math.max(1, options.deadline - now);
9584
- await sleep7(Math.min(NETWORK_POLL_MS, remaining));
11009
+ await sleep10(Math.min(NETWORK_POLL_MS, remaining));
9585
11010
  }
9586
11011
  }
9587
11012
  handleRequestStarted = (request) => {
@@ -9626,7 +11051,7 @@ var AdaptiveNetworkTracker = class {
9626
11051
  return false;
9627
11052
  }
9628
11053
  };
9629
- async function sleep7(ms) {
11054
+ async function sleep10(ms) {
9630
11055
  await new Promise((resolve) => {
9631
11056
  setTimeout(resolve, ms);
9632
11057
  });
@@ -11406,15 +12831,19 @@ var Opensteer = class _Opensteer {
11406
12831
  }
11407
12832
  return;
11408
12833
  }
12834
+ let closedOwnedBrowser = false;
11409
12835
  try {
11410
12836
  if (this.ownsBrowser) {
11411
12837
  await this.pool.close();
12838
+ closedOwnedBrowser = true;
11412
12839
  }
11413
12840
  } finally {
11414
12841
  this.browser = null;
11415
12842
  this.pageRef = null;
11416
12843
  this.contextRef = null;
11417
- this.ownsBrowser = false;
12844
+ if (!this.ownsBrowser || closedOwnedBrowser) {
12845
+ this.ownsBrowser = false;
12846
+ }
11418
12847
  if (this.cursorController) {
11419
12848
  await this.cursorController.dispose().catch(() => void 0);
11420
12849
  }
@@ -13741,7 +15170,7 @@ function normalizeCloudBrowserProfilePreference(value, source) {
13741
15170
  }
13742
15171
  function buildLocalRunId(namespace) {
13743
15172
  const normalized = namespace.trim() || "default";
13744
- return `${normalized}-${Date.now().toString(36)}-${randomUUID2().slice(0, 8)}`;
15173
+ return `${normalized}-${Date.now().toString(36)}-${randomUUID5().slice(0, 8)}`;
13745
15174
  }
13746
15175
 
13747
15176
  export {
@@ -13749,6 +15178,8 @@ export {
13749
15178
  clearPersistentProfileSingletons,
13750
15179
  createIsolatedRuntimeProfile,
13751
15180
  persistIsolatedRuntimeProfile,
15181
+ hasActiveRuntimeProfileCreations,
15182
+ getOwnedRealBrowserProcessPolicy,
13752
15183
  BrowserPool,
13753
15184
  waitForVisualStability,
13754
15185
  createEmptyRegistry,