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.
package/dist/index.cjs CHANGED
@@ -491,8 +491,10 @@ __export(index_exports, {
491
491
  getElementValue: () => getElementValue,
492
492
  getModelProvider: () => getModelProvider,
493
493
  getOrCreatePersistentProfile: () => getOrCreatePersistentProfile,
494
+ getOwnedRealBrowserProcessPolicy: () => getOwnedRealBrowserProcessPolicy,
494
495
  getPageHtml: () => getPageHtml,
495
496
  getPageTitle: () => getPageTitle,
497
+ hasActiveRuntimeProfileCreations: () => hasActiveRuntimeProfileCreations,
496
498
  importCookies: () => importCookies,
497
499
  isCloudActionMethod: () => isCloudActionMethod,
498
500
  isCloudErrorCode: () => isCloudErrorCode,
@@ -531,10 +533,7 @@ module.exports = __toCommonJS(index_exports);
531
533
  var import_crypto = require("crypto");
532
534
 
533
535
  // src/browser/pool.ts
534
- var import_node_child_process2 = require("child_process");
535
- var import_promises2 = require("fs/promises");
536
- var import_node_net = require("net");
537
- var import_playwright = require("playwright");
536
+ var import_playwright2 = require("playwright");
538
537
 
539
538
  // src/browser/cdp-proxy.ts
540
539
  var import_ws = __toESM(require("ws"), 1);
@@ -1027,1064 +1026,2644 @@ function listLocalChromeProfiles(userDataDir = detectChromePaths().defaultUserDa
1027
1026
  }
1028
1027
 
1029
1028
  // src/browser/persistent-profile.ts
1029
+ var import_node_crypto3 = require("crypto");
1030
+ var import_node_child_process2 = require("child_process");
1031
+ var import_node_fs3 = require("fs");
1032
+ var import_promises4 = require("fs/promises");
1033
+ var import_node_os = require("os");
1034
+ var import_node_path4 = require("path");
1035
+ var import_node_util2 = require("util");
1036
+
1037
+ // src/browser/persistent-profile-coordination.ts
1038
+ var import_node_path2 = require("path");
1039
+
1040
+ // src/browser/dir-lock.ts
1030
1041
  var import_node_crypto = require("crypto");
1031
- var import_node_child_process = require("child_process");
1032
1042
  var import_node_fs = require("fs");
1033
- var import_promises = require("fs/promises");
1034
- var import_node_os = require("os");
1043
+ var import_promises2 = require("fs/promises");
1035
1044
  var import_node_path = require("path");
1045
+
1046
+ // src/browser/process-owner.ts
1047
+ var import_node_child_process = require("child_process");
1048
+ var import_promises = require("fs/promises");
1036
1049
  var import_node_util = require("util");
1037
1050
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
1038
- var OPENSTEER_META_FILE = ".opensteer-meta.json";
1039
- var OPENSTEER_RUNTIME_META_FILE = ".opensteer-runtime.json";
1040
- var LOCK_OWNER_FILE = "owner.json";
1041
- var LOCK_RECLAIMER_DIR = "reclaimer";
1042
1051
  var PROCESS_STARTED_AT_MS = Math.floor(Date.now() - process.uptime() * 1e3);
1043
1052
  var PROCESS_START_TIME_TOLERANCE_MS = 1e3;
1044
- var PROFILE_LOCK_RETRY_DELAY_MS = 50;
1053
+ var PROCESS_LIST_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
1045
1054
  var PS_COMMAND_ENV = { ...process.env, LC_ALL: "C" };
1046
1055
  var LINUX_STAT_START_TIME_FIELD_INDEX = 19;
1047
- var CHROME_SINGLETON_ENTRIES = /* @__PURE__ */ new Set([
1048
- "SingletonCookie",
1049
- "SingletonLock",
1050
- "SingletonSocket",
1051
- "DevToolsActivePort",
1052
- "lockfile"
1053
- ]);
1054
- var COPY_SKIP_ENTRIES = /* @__PURE__ */ new Set([
1055
- ...CHROME_SINGLETON_ENTRIES,
1056
- OPENSTEER_META_FILE,
1057
- OPENSTEER_RUNTIME_META_FILE
1058
- ]);
1059
- var SKIPPED_ROOT_DIRECTORIES = /* @__PURE__ */ new Set([
1060
- "Crash Reports",
1061
- "Crashpad",
1062
- "BrowserMetrics",
1063
- "GrShaderCache",
1064
- "ShaderCache",
1065
- "GraphiteDawnCache",
1066
- "component_crx_cache",
1067
- "Crowd Deny",
1068
- "hyphen-data",
1069
- "OnDeviceHeadSuggestModel",
1070
- "OptimizationGuidePredictionModels",
1071
- "Segmentation Platform",
1072
- "SmartCardDeviceNames",
1073
- "WidevineCdm",
1074
- "pnacl"
1075
- ]);
1076
- var CURRENT_PROCESS_LOCK_OWNER = {
1056
+ var CURRENT_PROCESS_OWNER = {
1077
1057
  pid: process.pid,
1078
1058
  processStartedAtMs: PROCESS_STARTED_AT_MS
1079
1059
  };
1080
1060
  var linuxClockTicksPerSecondPromise = null;
1081
- async function getOrCreatePersistentProfile(sourceUserDataDir, profileDirectory, profilesRootDir = defaultPersistentProfilesRootDir()) {
1082
- const resolvedSourceUserDataDir = expandHome(sourceUserDataDir);
1083
- const targetUserDataDir = (0, import_node_path.join)(
1084
- expandHome(profilesRootDir),
1085
- buildPersistentProfileKey(resolvedSourceUserDataDir, profileDirectory)
1086
- );
1087
- const sourceProfileDir = (0, import_node_path.join)(resolvedSourceUserDataDir, profileDirectory);
1088
- const metadata = buildPersistentProfileMetadata(
1089
- resolvedSourceUserDataDir,
1090
- profileDirectory
1091
- );
1092
- await (0, import_promises.mkdir)((0, import_node_path.dirname)(targetUserDataDir), { recursive: true });
1093
- return await withPersistentProfileLock(targetUserDataDir, async () => {
1094
- await cleanOrphanedOwnedDirs(
1095
- (0, import_node_path.dirname)(targetUserDataDir),
1096
- buildPersistentProfileTempDirNamePrefix(targetUserDataDir)
1097
- );
1098
- if (!(0, import_node_fs.existsSync)(sourceProfileDir)) {
1099
- throw new Error(
1100
- `Chrome profile "${profileDirectory}" was not found in "${resolvedSourceUserDataDir}".`
1101
- );
1102
- }
1103
- const created = await createPersistentProfileClone(
1104
- resolvedSourceUserDataDir,
1105
- sourceProfileDir,
1106
- targetUserDataDir,
1107
- profileDirectory,
1108
- metadata
1109
- );
1110
- await ensurePersistentProfileMetadata(targetUserDataDir, metadata);
1111
- return {
1112
- created,
1113
- userDataDir: targetUserDataDir
1114
- };
1115
- });
1116
- }
1117
- async function clearPersistentProfileSingletons(userDataDir) {
1118
- await Promise.all(
1119
- [...CHROME_SINGLETON_ENTRIES].map(
1120
- (entry) => (0, import_promises.rm)((0, import_node_path.join)(userDataDir, entry), {
1121
- force: true,
1122
- recursive: true
1123
- }).catch(() => void 0)
1124
- )
1125
- );
1126
- }
1127
- async function createIsolatedRuntimeProfile(sourceUserDataDir, runtimesRootDir = defaultRuntimeProfilesRootDir()) {
1128
- const resolvedSourceUserDataDir = expandHome(sourceUserDataDir);
1129
- const runtimeRootDir = expandHome(runtimesRootDir);
1130
- await (0, import_promises.mkdir)(runtimeRootDir, { recursive: true });
1131
- return await withPersistentProfileLock(
1132
- resolvedSourceUserDataDir,
1133
- async () => {
1134
- const sourceMetadata = await readPersistentProfileMetadata(
1135
- resolvedSourceUserDataDir
1136
- );
1137
- await cleanOrphanedOwnedDirs(
1138
- runtimeRootDir,
1139
- buildRuntimeProfileDirNamePrefix(resolvedSourceUserDataDir)
1140
- );
1141
- const runtimeUserDataDir = await (0, import_promises.mkdtemp)(
1142
- buildRuntimeProfileDirPrefix(
1143
- runtimeRootDir,
1144
- resolvedSourceUserDataDir
1145
- )
1146
- );
1147
- try {
1148
- await copyUserDataDirSnapshot(
1149
- resolvedSourceUserDataDir,
1150
- runtimeUserDataDir
1151
- );
1152
- await writeRuntimeProfileMetadata(
1153
- runtimeUserDataDir,
1154
- await buildRuntimeProfileMetadata(
1155
- runtimeUserDataDir,
1156
- sourceMetadata?.profileDirectory ?? null
1157
- )
1158
- );
1159
- return {
1160
- persistentUserDataDir: resolvedSourceUserDataDir,
1161
- userDataDir: runtimeUserDataDir
1162
- };
1163
- } catch (error) {
1164
- await (0, import_promises.rm)(runtimeUserDataDir, {
1165
- recursive: true,
1166
- force: true
1167
- }).catch(() => void 0);
1168
- throw error;
1169
- }
1170
- }
1171
- );
1172
- }
1173
- async function persistIsolatedRuntimeProfile(runtimeUserDataDir, persistentUserDataDir) {
1174
- const resolvedRuntimeUserDataDir = expandHome(runtimeUserDataDir);
1175
- const resolvedPersistentUserDataDir = expandHome(persistentUserDataDir);
1176
- await withPersistentProfileLock(resolvedPersistentUserDataDir, async () => {
1177
- await (0, import_promises.mkdir)((0, import_node_path.dirname)(resolvedPersistentUserDataDir), { recursive: true });
1178
- await cleanOrphanedOwnedDirs(
1179
- (0, import_node_path.dirname)(resolvedPersistentUserDataDir),
1180
- buildPersistentProfileTempDirNamePrefix(resolvedPersistentUserDataDir)
1181
- );
1182
- const metadata = await requirePersistentProfileMetadata(
1183
- resolvedPersistentUserDataDir
1184
- );
1185
- const runtimeMetadata = await requireRuntimeProfileMetadata(
1186
- resolvedRuntimeUserDataDir,
1187
- metadata.profileDirectory
1188
- );
1189
- await mergePersistentProfileSnapshot(
1190
- resolvedRuntimeUserDataDir,
1191
- resolvedPersistentUserDataDir,
1192
- metadata,
1193
- runtimeMetadata
1194
- );
1195
- });
1196
- await (0, import_promises.rm)(resolvedRuntimeUserDataDir, {
1197
- recursive: true,
1198
- force: true
1199
- });
1200
- }
1201
- function buildPersistentProfileKey(sourceUserDataDir, profileDirectory) {
1202
- const hash = (0, import_node_crypto.createHash)("sha256").update(`${sourceUserDataDir}\0${profileDirectory}`).digest("hex").slice(0, 16);
1203
- const sourceLabel = sanitizePathSegment((0, import_node_path.basename)(sourceUserDataDir) || "user-data");
1204
- const profileLabel = sanitizePathSegment(profileDirectory || "Default");
1205
- return `${sourceLabel}-${profileLabel}-${hash}`;
1061
+ function parseProcessOwner(value) {
1062
+ if (!value || typeof value !== "object") {
1063
+ return null;
1064
+ }
1065
+ const parsed = value;
1066
+ const pid = Number(parsed.pid);
1067
+ const processStartedAtMs = Number(parsed.processStartedAtMs);
1068
+ if (!Number.isInteger(pid) || pid <= 0) {
1069
+ return null;
1070
+ }
1071
+ if (!Number.isInteger(processStartedAtMs) || processStartedAtMs <= 0) {
1072
+ return null;
1073
+ }
1074
+ return {
1075
+ pid,
1076
+ processStartedAtMs
1077
+ };
1206
1078
  }
1207
- function defaultPersistentProfilesRootDir() {
1208
- return (0, import_node_path.join)((0, import_node_os.homedir)(), ".opensteer", "real-browser-profiles");
1079
+ function processOwnersEqual(left, right) {
1080
+ if (!left || !right) {
1081
+ return left === right;
1082
+ }
1083
+ return left.pid === right.pid && left.processStartedAtMs === right.processStartedAtMs;
1209
1084
  }
1210
- function defaultRuntimeProfilesRootDir() {
1211
- return (0, import_node_path.join)((0, import_node_os.tmpdir)(), "opensteer-real-browser-runtimes");
1085
+ async function getProcessLiveness(owner) {
1086
+ if (owner.pid === process.pid && hasMatchingProcessStartTime(
1087
+ owner.processStartedAtMs,
1088
+ PROCESS_STARTED_AT_MS
1089
+ )) {
1090
+ return "live";
1091
+ }
1092
+ const startedAtMs = await readProcessStartedAtMs(owner.pid);
1093
+ if (typeof startedAtMs === "number") {
1094
+ return hasMatchingProcessStartTime(
1095
+ owner.processStartedAtMs,
1096
+ startedAtMs
1097
+ ) ? "live" : "dead";
1098
+ }
1099
+ return isProcessRunning(owner.pid) ? "unknown" : "dead";
1212
1100
  }
1213
- function sanitizePathSegment(value) {
1214
- const sanitized = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/-+/g, "-");
1215
- return sanitized.replace(/^-|-$/g, "") || "profile";
1101
+ function isProcessRunning(pid) {
1102
+ try {
1103
+ process.kill(pid, 0);
1104
+ return true;
1105
+ } catch (error) {
1106
+ const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
1107
+ return code !== "ESRCH";
1108
+ }
1216
1109
  }
1217
- function isProfileDirectory(userDataDir, entry) {
1218
- return (0, import_node_fs.existsSync)((0, import_node_path.join)(userDataDir, entry, "Preferences"));
1110
+ async function readProcessOwner(pid) {
1111
+ const processStartedAtMs = await readProcessStartedAtMs(pid);
1112
+ if (processStartedAtMs === null) {
1113
+ return null;
1114
+ }
1115
+ return {
1116
+ pid,
1117
+ processStartedAtMs
1118
+ };
1219
1119
  }
1220
- async function copyUserDataDirSnapshot(sourceUserDataDir, targetUserDataDir) {
1221
- await (0, import_promises.cp)(sourceUserDataDir, targetUserDataDir, {
1222
- recursive: true,
1223
- filter: (candidatePath) => shouldCopyRuntimeSnapshotEntry(sourceUserDataDir, candidatePath)
1224
- });
1120
+ function hasMatchingProcessStartTime(expectedStartedAtMs, actualStartedAtMs) {
1121
+ return Math.abs(expectedStartedAtMs - actualStartedAtMs) <= PROCESS_START_TIME_TOLERANCE_MS;
1225
1122
  }
1226
- function shouldCopyRuntimeSnapshotEntry(userDataDir, candidatePath) {
1227
- const candidateRelativePath = (0, import_node_path.relative)(userDataDir, candidatePath);
1228
- if (!candidateRelativePath) {
1229
- return true;
1123
+ async function readProcessStartedAtMs(pid) {
1124
+ if (pid <= 0) {
1125
+ return null;
1230
1126
  }
1231
- const segments = candidateRelativePath.split(import_node_path.sep).filter(Boolean);
1232
- if (segments.length !== 1) {
1233
- return true;
1127
+ if (process.platform === "linux") {
1128
+ return await readLinuxProcessStartedAtMs(pid);
1234
1129
  }
1235
- return !COPY_SKIP_ENTRIES.has(segments[0]);
1130
+ if (process.platform === "win32") {
1131
+ return await readWindowsProcessStartedAtMs(pid);
1132
+ }
1133
+ return await readPsProcessStartedAtMs(pid);
1236
1134
  }
1237
- async function copyRootLevelEntries(sourceUserDataDir, targetUserDataDir, targetProfileDirectory) {
1238
- let entries;
1135
+ async function readLinuxProcessStartedAtMs(pid) {
1136
+ let statRaw;
1239
1137
  try {
1240
- entries = await (0, import_promises.readdir)(sourceUserDataDir);
1138
+ statRaw = await (0, import_promises.readFile)(`/proc/${pid}/stat`, "utf8");
1241
1139
  } catch {
1242
- return;
1140
+ return null;
1243
1141
  }
1244
- const copyTasks = [];
1245
- for (const entry of entries) {
1246
- if (COPY_SKIP_ENTRIES.has(entry)) continue;
1247
- if (entry === targetProfileDirectory) continue;
1248
- const sourcePath = (0, import_node_path.join)(sourceUserDataDir, entry);
1249
- const targetPath = (0, import_node_path.join)(targetUserDataDir, entry);
1250
- if ((0, import_node_fs.existsSync)(targetPath)) continue;
1251
- let entryStat;
1252
- try {
1253
- entryStat = await (0, import_promises.stat)(sourcePath);
1254
- } catch {
1255
- continue;
1256
- }
1257
- if (entryStat.isFile()) {
1258
- copyTasks.push((0, import_promises.copyFile)(sourcePath, targetPath).catch(() => void 0));
1259
- } else if (entryStat.isDirectory()) {
1260
- if (isProfileDirectory(sourceUserDataDir, entry)) continue;
1261
- if (SKIPPED_ROOT_DIRECTORIES.has(entry)) continue;
1262
- copyTasks.push(
1263
- (0, import_promises.cp)(sourcePath, targetPath, { recursive: true }).catch(
1264
- () => void 0
1265
- )
1266
- );
1267
- }
1142
+ const startTicks = parseLinuxProcessStartTicks(statRaw);
1143
+ if (startTicks === null) {
1144
+ return null;
1268
1145
  }
1269
- await Promise.all(copyTasks);
1270
- }
1271
- async function writePersistentProfileMetadata(userDataDir, metadata) {
1272
- await (0, import_promises.writeFile)(
1273
- (0, import_node_path.join)(userDataDir, OPENSTEER_META_FILE),
1274
- JSON.stringify(metadata, null, 2)
1146
+ const [bootTimeMs, clockTicksPerSecond] = await Promise.all([
1147
+ readLinuxBootTimeMs(),
1148
+ readLinuxClockTicksPerSecond()
1149
+ ]);
1150
+ if (bootTimeMs === null || clockTicksPerSecond === null) {
1151
+ return null;
1152
+ }
1153
+ return Math.floor(
1154
+ bootTimeMs + startTicks * 1e3 / clockTicksPerSecond
1275
1155
  );
1276
1156
  }
1277
- function buildPersistentProfileMetadata(sourceUserDataDir, profileDirectory) {
1278
- return {
1279
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1280
- profileDirectory,
1281
- source: sourceUserDataDir
1282
- };
1283
- }
1284
- async function createPersistentProfileClone(sourceUserDataDir, sourceProfileDir, targetUserDataDir, profileDirectory, metadata) {
1285
- if ((0, import_node_fs.existsSync)(targetUserDataDir)) {
1286
- return false;
1157
+ function parseLinuxProcessStartTicks(statRaw) {
1158
+ const closingParenIndex = statRaw.lastIndexOf(")");
1159
+ if (closingParenIndex === -1) {
1160
+ return null;
1287
1161
  }
1288
- const tempUserDataDir = await (0, import_promises.mkdtemp)(
1289
- buildPersistentProfileTempDirPrefix(targetUserDataDir)
1290
- );
1291
- let published = false;
1292
- try {
1293
- await materializePersistentProfileSnapshot(
1294
- sourceUserDataDir,
1295
- sourceProfileDir,
1296
- tempUserDataDir,
1297
- profileDirectory,
1298
- metadata
1162
+ const fields = statRaw.slice(closingParenIndex + 2).trim().split(/\s+/);
1163
+ const startTicks = Number(fields[LINUX_STAT_START_TIME_FIELD_INDEX]);
1164
+ return Number.isFinite(startTicks) && startTicks >= 0 ? startTicks : null;
1165
+ }
1166
+ async function readLinuxBootTimeMs() {
1167
+ try {
1168
+ const statRaw = await (0, import_promises.readFile)("/proc/stat", "utf8");
1169
+ const bootTimeLine = statRaw.split("\n").find((line) => line.startsWith("btime "));
1170
+ if (!bootTimeLine) {
1171
+ return null;
1172
+ }
1173
+ const bootTimeSeconds = Number.parseInt(
1174
+ bootTimeLine.slice("btime ".length),
1175
+ 10
1176
+ );
1177
+ return Number.isFinite(bootTimeSeconds) ? bootTimeSeconds * 1e3 : null;
1178
+ } catch {
1179
+ return null;
1180
+ }
1181
+ }
1182
+ async function readLinuxClockTicksPerSecond() {
1183
+ if (!linuxClockTicksPerSecondPromise) {
1184
+ linuxClockTicksPerSecondPromise = execFileAsync("getconf", ["CLK_TCK"], {
1185
+ encoding: "utf8",
1186
+ maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES
1187
+ }).then(({ stdout }) => {
1188
+ const value = Number.parseInt(stdout.trim(), 10);
1189
+ return Number.isFinite(value) && value > 0 ? value : null;
1190
+ }).catch(() => null);
1191
+ }
1192
+ return await linuxClockTicksPerSecondPromise;
1193
+ }
1194
+ async function readWindowsProcessStartedAtMs(pid) {
1195
+ try {
1196
+ const { stdout } = await execFileAsync(
1197
+ "powershell.exe",
1198
+ [
1199
+ "-NoProfile",
1200
+ "-Command",
1201
+ `(Get-Process -Id ${pid}).StartTime.ToUniversalTime().ToString("o")`
1202
+ ],
1203
+ {
1204
+ encoding: "utf8",
1205
+ maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES
1206
+ }
1207
+ );
1208
+ const isoTimestamp = stdout.trim();
1209
+ if (!isoTimestamp) {
1210
+ return null;
1211
+ }
1212
+ const startedAtMs = Date.parse(isoTimestamp);
1213
+ return Number.isFinite(startedAtMs) ? startedAtMs : null;
1214
+ } catch {
1215
+ return null;
1216
+ }
1217
+ }
1218
+ async function readPsProcessStartedAtMs(pid) {
1219
+ try {
1220
+ const { stdout } = await execFileAsync(
1221
+ "ps",
1222
+ ["-o", "lstart=", "-p", String(pid)],
1223
+ {
1224
+ encoding: "utf8",
1225
+ env: PS_COMMAND_ENV,
1226
+ maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES
1227
+ }
1299
1228
  );
1229
+ const startedAt = stdout.trim();
1230
+ if (!startedAt) {
1231
+ return null;
1232
+ }
1233
+ const startedAtMs = Date.parse(startedAt.replace(/\s+/g, " "));
1234
+ return Number.isFinite(startedAtMs) ? startedAtMs : null;
1235
+ } catch {
1236
+ return null;
1237
+ }
1238
+ }
1239
+
1240
+ // src/browser/dir-lock.ts
1241
+ var LOCK_OWNER_FILE = "owner.json";
1242
+ var LOCK_RECLAIMER_DIR = "reclaimer";
1243
+ var LOCK_RETRY_DELAY_MS = 50;
1244
+ async function withDirLock(lockDirPath, action) {
1245
+ const releaseLock = await acquireDirLock(lockDirPath);
1246
+ try {
1247
+ return await action();
1248
+ } finally {
1249
+ await releaseLock();
1250
+ }
1251
+ }
1252
+ async function acquireDirLock(lockDirPath) {
1253
+ while (true) {
1254
+ const releaseLock = await tryAcquireDirLock(lockDirPath);
1255
+ if (releaseLock) {
1256
+ return releaseLock;
1257
+ }
1258
+ await sleep(LOCK_RETRY_DELAY_MS);
1259
+ }
1260
+ }
1261
+ async function tryAcquireDirLock(lockDirPath) {
1262
+ await (0, import_promises2.mkdir)((0, import_node_path.dirname)(lockDirPath), { recursive: true });
1263
+ while (true) {
1264
+ const tempLockDirPath = `${lockDirPath}-${process.pid}-${CURRENT_PROCESS_OWNER.processStartedAtMs}-${(0, import_node_crypto.randomUUID)()}`;
1265
+ try {
1266
+ await (0, import_promises2.mkdir)(tempLockDirPath);
1267
+ await writeLockOwner(tempLockDirPath, CURRENT_PROCESS_OWNER);
1268
+ try {
1269
+ await (0, import_promises2.rename)(tempLockDirPath, lockDirPath);
1270
+ break;
1271
+ } catch (error) {
1272
+ if (!wasDirPublishedByAnotherProcess(error, lockDirPath)) {
1273
+ throw error;
1274
+ }
1275
+ }
1276
+ } finally {
1277
+ await (0, import_promises2.rm)(tempLockDirPath, {
1278
+ recursive: true,
1279
+ force: true
1280
+ }).catch(() => void 0);
1281
+ }
1282
+ const owner = await readLockOwner(lockDirPath);
1283
+ if ((!owner || await getProcessLiveness(owner) === "dead") && await tryReclaimStaleLock(lockDirPath, owner)) {
1284
+ continue;
1285
+ }
1286
+ return null;
1287
+ }
1288
+ return async () => {
1289
+ await (0, import_promises2.rm)(lockDirPath, {
1290
+ recursive: true,
1291
+ force: true
1292
+ }).catch(() => void 0);
1293
+ };
1294
+ }
1295
+ async function isDirLockHeld(lockDirPath) {
1296
+ if (!(0, import_node_fs.existsSync)(lockDirPath)) {
1297
+ return false;
1298
+ }
1299
+ const owner = await readLockOwner(lockDirPath);
1300
+ if ((!owner || await getProcessLiveness(owner) === "dead") && await tryReclaimStaleLock(lockDirPath, owner)) {
1301
+ return false;
1302
+ }
1303
+ return (0, import_node_fs.existsSync)(lockDirPath);
1304
+ }
1305
+ function getErrorCode(error) {
1306
+ return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
1307
+ }
1308
+ function wasDirPublishedByAnotherProcess(error, targetDirPath) {
1309
+ const code = getErrorCode(error);
1310
+ return (0, import_node_fs.existsSync)(targetDirPath) && (code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM");
1311
+ }
1312
+ async function writeLockOwner(lockDirPath, owner) {
1313
+ await (0, import_promises2.writeFile)((0, import_node_path.join)(lockDirPath, LOCK_OWNER_FILE), JSON.stringify(owner));
1314
+ }
1315
+ async function readLockOwner(lockDirPath) {
1316
+ return await readLockParticipant((0, import_node_path.join)(lockDirPath, LOCK_OWNER_FILE));
1317
+ }
1318
+ async function readLockParticipant(filePath) {
1319
+ return (await readLockParticipantRecord(filePath)).owner;
1320
+ }
1321
+ async function readLockParticipantRecord(filePath) {
1322
+ try {
1323
+ const raw = await (0, import_promises2.readFile)(filePath, "utf8");
1324
+ const owner = parseProcessOwner(JSON.parse(raw));
1325
+ return {
1326
+ exists: true,
1327
+ owner
1328
+ };
1329
+ } catch (error) {
1330
+ return {
1331
+ exists: getErrorCode(error) !== "ENOENT",
1332
+ owner: null
1333
+ };
1334
+ }
1335
+ }
1336
+ async function readLockReclaimerRecord(lockDirPath) {
1337
+ return await readLockParticipantRecord(
1338
+ (0, import_node_path.join)(buildLockReclaimerDirPath(lockDirPath), LOCK_OWNER_FILE)
1339
+ );
1340
+ }
1341
+ async function tryReclaimStaleLock(lockDirPath, expectedOwner) {
1342
+ if (!await tryAcquireLockReclaimer(lockDirPath)) {
1343
+ return false;
1344
+ }
1345
+ let reclaimed = false;
1346
+ try {
1347
+ const owner = await readLockOwner(lockDirPath);
1348
+ if (!processOwnersEqual(owner, expectedOwner)) {
1349
+ return false;
1350
+ }
1351
+ if (owner && await getProcessLiveness(owner) !== "dead") {
1352
+ return false;
1353
+ }
1354
+ await (0, import_promises2.rm)(lockDirPath, {
1355
+ recursive: true,
1356
+ force: true
1357
+ }).catch(() => void 0);
1358
+ reclaimed = !(0, import_node_fs.existsSync)(lockDirPath);
1359
+ return reclaimed;
1360
+ } finally {
1361
+ if (!reclaimed) {
1362
+ await (0, import_promises2.rm)(buildLockReclaimerDirPath(lockDirPath), {
1363
+ recursive: true,
1364
+ force: true
1365
+ }).catch(() => void 0);
1366
+ }
1367
+ }
1368
+ }
1369
+ async function tryAcquireLockReclaimer(lockDirPath) {
1370
+ const reclaimerDirPath = buildLockReclaimerDirPath(lockDirPath);
1371
+ while (true) {
1372
+ const tempReclaimerDirPath = `${reclaimerDirPath}-${process.pid}-${CURRENT_PROCESS_OWNER.processStartedAtMs}-${(0, import_node_crypto.randomUUID)()}`;
1300
1373
  try {
1301
- await (0, import_promises.rename)(tempUserDataDir, targetUserDataDir);
1374
+ await (0, import_promises2.mkdir)(tempReclaimerDirPath);
1375
+ await writeLockOwner(tempReclaimerDirPath, CURRENT_PROCESS_OWNER);
1376
+ try {
1377
+ await (0, import_promises2.rename)(tempReclaimerDirPath, reclaimerDirPath);
1378
+ return true;
1379
+ } catch (error) {
1380
+ if (getErrorCode(error) === "ENOENT") {
1381
+ return false;
1382
+ }
1383
+ if (!wasDirPublishedByAnotherProcess(error, reclaimerDirPath)) {
1384
+ throw error;
1385
+ }
1386
+ }
1302
1387
  } catch (error) {
1303
- if (wasDirPublishedByAnotherProcess(error, targetUserDataDir)) {
1388
+ const code = getErrorCode(error);
1389
+ if (code === "ENOENT") {
1304
1390
  return false;
1305
1391
  }
1306
1392
  throw error;
1307
- }
1308
- published = true;
1309
- return true;
1310
- } finally {
1311
- if (!published) {
1312
- await (0, import_promises.rm)(tempUserDataDir, {
1393
+ } finally {
1394
+ await (0, import_promises2.rm)(tempReclaimerDirPath, {
1313
1395
  recursive: true,
1314
1396
  force: true
1315
1397
  }).catch(() => void 0);
1316
1398
  }
1399
+ const reclaimerRecord = await readLockReclaimerRecord(lockDirPath);
1400
+ if (!reclaimerRecord.exists || !reclaimerRecord.owner) {
1401
+ return false;
1402
+ }
1403
+ if (await getProcessLiveness(reclaimerRecord.owner) !== "dead") {
1404
+ return false;
1405
+ }
1406
+ await (0, import_promises2.rm)(reclaimerDirPath, {
1407
+ recursive: true,
1408
+ force: true
1409
+ }).catch(() => void 0);
1317
1410
  }
1318
1411
  }
1319
- async function materializePersistentProfileSnapshot(sourceUserDataDir, sourceProfileDir, targetUserDataDir, profileDirectory, metadata) {
1320
- if (!(0, import_node_fs.existsSync)(sourceProfileDir)) {
1321
- throw new Error(
1322
- `Chrome profile "${profileDirectory}" was not found in "${sourceUserDataDir}".`
1323
- );
1412
+ function buildLockReclaimerDirPath(lockDirPath) {
1413
+ return (0, import_node_path.join)(lockDirPath, LOCK_RECLAIMER_DIR);
1414
+ }
1415
+ async function sleep(ms) {
1416
+ await new Promise((resolve) => setTimeout(resolve, ms));
1417
+ }
1418
+
1419
+ // src/browser/persistent-profile-coordination.ts
1420
+ var PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS = 50;
1421
+ async function withPersistentProfileControlLock(targetUserDataDir, action) {
1422
+ return await withDirLock(
1423
+ buildPersistentProfileControlLockDirPath(targetUserDataDir),
1424
+ action
1425
+ );
1426
+ }
1427
+ async function acquirePersistentProfileWriteLock(targetUserDataDir) {
1428
+ const controlLockDirPath = buildPersistentProfileControlLockDirPath(targetUserDataDir);
1429
+ const writeLockDirPath = buildPersistentProfileWriteLockDirPath(
1430
+ targetUserDataDir
1431
+ );
1432
+ while (true) {
1433
+ let releaseWriteLock = null;
1434
+ const releaseControlLock = await acquireDirLock(controlLockDirPath);
1435
+ try {
1436
+ releaseWriteLock = await tryAcquireDirLock(writeLockDirPath);
1437
+ } finally {
1438
+ await releaseControlLock();
1439
+ }
1440
+ if (releaseWriteLock) {
1441
+ return releaseWriteLock;
1442
+ }
1443
+ await sleep2(PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS);
1324
1444
  }
1325
- await (0, import_promises.cp)(sourceProfileDir, (0, import_node_path.join)(targetUserDataDir, profileDirectory), {
1326
- recursive: true
1327
- });
1328
- await copyRootLevelEntries(sourceUserDataDir, targetUserDataDir, profileDirectory);
1329
- await writePersistentProfileMetadata(targetUserDataDir, metadata);
1330
1445
  }
1331
- async function mergePersistentProfileSnapshot(runtimeUserDataDir, persistentUserDataDir, metadata, runtimeMetadata) {
1332
- const tempUserDataDir = await (0, import_promises.mkdtemp)(
1333
- buildPersistentProfileTempDirPrefix(persistentUserDataDir)
1446
+ async function isPersistentProfileWriteLocked(targetUserDataDir) {
1447
+ return await isDirLockHeld(
1448
+ buildPersistentProfileWriteLockDirPath(targetUserDataDir)
1334
1449
  );
1335
- let published = false;
1450
+ }
1451
+ function buildPersistentProfileWriteLockDirPath(targetUserDataDir) {
1452
+ return (0, import_node_path2.join)((0, import_node_path2.dirname)(targetUserDataDir), `${(0, import_node_path2.basename)(targetUserDataDir)}.lock`);
1453
+ }
1454
+ function buildPersistentProfileControlLockDirPath(targetUserDataDir) {
1455
+ return (0, import_node_path2.join)(
1456
+ (0, import_node_path2.dirname)(targetUserDataDir),
1457
+ `${(0, import_node_path2.basename)(targetUserDataDir)}.control.lock`
1458
+ );
1459
+ }
1460
+ async function sleep2(ms) {
1461
+ await new Promise((resolve) => setTimeout(resolve, ms));
1462
+ }
1463
+
1464
+ // src/browser/shared-real-browser-session-state.ts
1465
+ var import_node_crypto2 = require("crypto");
1466
+ var import_node_fs2 = require("fs");
1467
+ var import_promises3 = require("fs/promises");
1468
+ var import_node_path3 = require("path");
1469
+ var SHARED_SESSION_METADATA_FILE = "session.json";
1470
+ var SHARED_SESSION_CLIENTS_DIR = "clients";
1471
+ var SHARED_SESSION_RETRY_DELAY_MS = 50;
1472
+ var SHARED_SESSION_METADATA_TEMP_FILE_PREFIX = `${SHARED_SESSION_METADATA_FILE}.`;
1473
+ var SHARED_SESSION_METADATA_TEMP_FILE_SUFFIX = ".tmp";
1474
+ function buildSharedSessionDirPath(persistentUserDataDir) {
1475
+ return (0, import_node_path3.join)(
1476
+ (0, import_node_path3.dirname)(persistentUserDataDir),
1477
+ `${(0, import_node_path3.basename)(persistentUserDataDir)}.session`
1478
+ );
1479
+ }
1480
+ function buildSharedSessionLockPath(persistentUserDataDir) {
1481
+ return `${buildSharedSessionDirPath(persistentUserDataDir)}.lock`;
1482
+ }
1483
+ function buildSharedSessionClientsDirPath(persistentUserDataDir) {
1484
+ return (0, import_node_path3.join)(
1485
+ buildSharedSessionDirPath(persistentUserDataDir),
1486
+ SHARED_SESSION_CLIENTS_DIR
1487
+ );
1488
+ }
1489
+ function buildSharedSessionClientPath(persistentUserDataDir, clientId) {
1490
+ return (0, import_node_path3.join)(
1491
+ buildSharedSessionClientsDirPath(persistentUserDataDir),
1492
+ `${clientId}.json`
1493
+ );
1494
+ }
1495
+ async function readSharedSessionMetadata(persistentUserDataDir) {
1496
+ return (await readSharedSessionMetadataRecord(persistentUserDataDir)).metadata;
1497
+ }
1498
+ async function writeSharedSessionMetadata(persistentUserDataDir, metadata) {
1499
+ const sessionDirPath = buildSharedSessionDirPath(persistentUserDataDir);
1500
+ const metadataPath = buildSharedSessionMetadataPath(persistentUserDataDir);
1501
+ const tempPath = buildSharedSessionMetadataTempPath(sessionDirPath);
1502
+ await (0, import_promises3.mkdir)(sessionDirPath, { recursive: true });
1336
1503
  try {
1337
- const baseEntries = deserializeSnapshotManifestEntries(
1338
- runtimeMetadata.baseEntries
1339
- );
1340
- const currentEntries = await collectPersistentSnapshotEntries(
1341
- persistentUserDataDir,
1342
- metadata.profileDirectory
1343
- );
1344
- const runtimeEntries = await collectPersistentSnapshotEntries(
1345
- runtimeUserDataDir,
1346
- metadata.profileDirectory
1347
- );
1348
- const mergedEntries = resolveMergedSnapshotEntries(
1349
- baseEntries,
1350
- currentEntries,
1351
- runtimeEntries
1352
- );
1353
- await materializeMergedPersistentProfileSnapshot(
1354
- tempUserDataDir,
1355
- currentEntries,
1356
- runtimeEntries,
1357
- mergedEntries
1358
- );
1359
- await writePersistentProfileMetadata(tempUserDataDir, metadata);
1360
- await replaceProfileDirectory(persistentUserDataDir, tempUserDataDir);
1361
- published = true;
1504
+ await (0, import_promises3.writeFile)(tempPath, JSON.stringify(metadata, null, 2));
1505
+ await (0, import_promises3.rename)(tempPath, metadataPath);
1506
+ } finally {
1507
+ await (0, import_promises3.rm)(tempPath, { force: true }).catch(() => void 0);
1508
+ }
1509
+ }
1510
+ async function hasLiveSharedRealBrowserSession(persistentUserDataDir) {
1511
+ const sessionDirPath = buildSharedSessionDirPath(persistentUserDataDir);
1512
+ const metadataRecord = await readSharedSessionMetadataRecord(
1513
+ persistentUserDataDir
1514
+ );
1515
+ if (!metadataRecord.exists) {
1516
+ return await hasLiveSharedSessionPublisherOrClients(sessionDirPath);
1517
+ }
1518
+ if (!metadataRecord.metadata) {
1519
+ return true;
1520
+ }
1521
+ if (await getProcessLiveness(metadataRecord.metadata.browserOwner) === "dead") {
1522
+ await (0, import_promises3.rm)(sessionDirPath, {
1523
+ force: true,
1524
+ recursive: true
1525
+ }).catch(() => void 0);
1526
+ return false;
1527
+ }
1528
+ return true;
1529
+ }
1530
+ async function waitForSharedRealBrowserSessionToDrain(persistentUserDataDir) {
1531
+ while (true) {
1532
+ if (!await hasLiveSharedRealBrowserSession(persistentUserDataDir)) {
1533
+ return;
1534
+ }
1535
+ await sleep3(SHARED_SESSION_RETRY_DELAY_MS);
1536
+ }
1537
+ }
1538
+ async function readSharedSessionMetadataRecord(persistentUserDataDir) {
1539
+ try {
1540
+ const raw = await (0, import_promises3.readFile)(
1541
+ buildSharedSessionMetadataPath(persistentUserDataDir),
1542
+ "utf8"
1543
+ );
1544
+ return {
1545
+ exists: true,
1546
+ metadata: parseSharedSessionMetadata(JSON.parse(raw))
1547
+ };
1548
+ } catch (error) {
1549
+ return {
1550
+ exists: getErrorCode2(error) !== "ENOENT",
1551
+ metadata: null
1552
+ };
1553
+ }
1554
+ }
1555
+ async function hasLiveSharedSessionPublisherOrClients(sessionDirPath) {
1556
+ if (!(0, import_node_fs2.existsSync)(sessionDirPath)) {
1557
+ return false;
1558
+ }
1559
+ let entries;
1560
+ try {
1561
+ entries = await readDirNames(sessionDirPath);
1562
+ } catch (error) {
1563
+ return getErrorCode2(error) !== "ENOENT";
1564
+ }
1565
+ let hasUnknownEntries = false;
1566
+ for (const entry of entries) {
1567
+ if (entry === SHARED_SESSION_METADATA_FILE) {
1568
+ return true;
1569
+ }
1570
+ if (entry === SHARED_SESSION_CLIENTS_DIR) {
1571
+ if (await hasDirectoryEntries((0, import_node_path3.join)(sessionDirPath, entry))) {
1572
+ return true;
1573
+ }
1574
+ continue;
1575
+ }
1576
+ const owner = parseSharedSessionMetadataTempOwner(entry);
1577
+ if (!owner) {
1578
+ if (isSharedSessionMetadataTempFile(entry)) {
1579
+ continue;
1580
+ }
1581
+ hasUnknownEntries = true;
1582
+ continue;
1583
+ }
1584
+ if (await getProcessLiveness(owner) !== "dead") {
1585
+ return true;
1586
+ }
1587
+ }
1588
+ if (hasUnknownEntries) {
1589
+ return true;
1590
+ }
1591
+ await (0, import_promises3.rm)(sessionDirPath, {
1592
+ force: true,
1593
+ recursive: true
1594
+ }).catch(() => void 0);
1595
+ return false;
1596
+ }
1597
+ function buildSharedSessionMetadataPath(persistentUserDataDir) {
1598
+ return (0, import_node_path3.join)(
1599
+ buildSharedSessionDirPath(persistentUserDataDir),
1600
+ SHARED_SESSION_METADATA_FILE
1601
+ );
1602
+ }
1603
+ function buildSharedSessionMetadataTempPath(sessionDirPath) {
1604
+ return (0, import_node_path3.join)(
1605
+ sessionDirPath,
1606
+ [
1607
+ SHARED_SESSION_METADATA_FILE,
1608
+ CURRENT_PROCESS_OWNER.pid,
1609
+ CURRENT_PROCESS_OWNER.processStartedAtMs,
1610
+ (0, import_node_crypto2.randomUUID)(),
1611
+ "tmp"
1612
+ ].join(".")
1613
+ );
1614
+ }
1615
+ function parseSharedSessionMetadata(value) {
1616
+ if (!value || typeof value !== "object") {
1617
+ return null;
1618
+ }
1619
+ const parsed = value;
1620
+ const browserOwner = parseProcessOwner(parsed.browserOwner);
1621
+ const stateOwner = parseProcessOwner(parsed.stateOwner);
1622
+ const state = parsed.state === "launching" || parsed.state === "ready" || parsed.state === "closing" ? parsed.state : null;
1623
+ 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) {
1624
+ return null;
1625
+ }
1626
+ return {
1627
+ browserOwner,
1628
+ createdAt: parsed.createdAt,
1629
+ debugPort: parsed.debugPort,
1630
+ executablePath: parsed.executablePath,
1631
+ headless: parsed.headless,
1632
+ persistentUserDataDir: parsed.persistentUserDataDir,
1633
+ profileDirectory: parsed.profileDirectory,
1634
+ sessionId: parsed.sessionId,
1635
+ state,
1636
+ stateOwner
1637
+ };
1638
+ }
1639
+ function parseSharedSessionMetadataTempOwner(entryName) {
1640
+ if (!isSharedSessionMetadataTempFile(entryName)) {
1641
+ return null;
1642
+ }
1643
+ const segments = entryName.split(".");
1644
+ if (segments.length < 5) {
1645
+ return null;
1646
+ }
1647
+ return parseProcessOwner({
1648
+ pid: Number.parseInt(segments[2] ?? "", 10),
1649
+ processStartedAtMs: Number.parseInt(segments[3] ?? "", 10)
1650
+ });
1651
+ }
1652
+ function isSharedSessionMetadataTempFile(entryName) {
1653
+ return entryName.startsWith(SHARED_SESSION_METADATA_TEMP_FILE_PREFIX) && entryName.endsWith(SHARED_SESSION_METADATA_TEMP_FILE_SUFFIX);
1654
+ }
1655
+ function getErrorCode2(error) {
1656
+ return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
1657
+ }
1658
+ async function hasDirectoryEntries(dirPath) {
1659
+ try {
1660
+ return (await readDirNames(dirPath)).length > 0;
1661
+ } catch (error) {
1662
+ return getErrorCode2(error) !== "ENOENT";
1663
+ }
1664
+ }
1665
+ async function readDirNames(dirPath) {
1666
+ return await (0, import_promises3.readdir)(dirPath, { encoding: "utf8" });
1667
+ }
1668
+ async function sleep3(ms) {
1669
+ await new Promise((resolve) => setTimeout(resolve, ms));
1670
+ }
1671
+
1672
+ // src/browser/persistent-profile.ts
1673
+ var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
1674
+ var OPENSTEER_META_FILE = ".opensteer-meta.json";
1675
+ var OPENSTEER_RUNTIME_META_FILE = ".opensteer-runtime.json";
1676
+ var OPENSTEER_RUNTIME_CREATING_FILE = ".opensteer-runtime-creating.json";
1677
+ var PROCESS_LIST_MAX_BUFFER_BYTES2 = 16 * 1024 * 1024;
1678
+ var PS_COMMAND_ENV2 = { ...process.env, LC_ALL: "C" };
1679
+ var CHROME_SINGLETON_ENTRIES = /* @__PURE__ */ new Set([
1680
+ "SingletonCookie",
1681
+ "SingletonLock",
1682
+ "SingletonSocket",
1683
+ "DevToolsActivePort",
1684
+ "lockfile"
1685
+ ]);
1686
+ var COPY_SKIP_ENTRIES = /* @__PURE__ */ new Set([
1687
+ ...CHROME_SINGLETON_ENTRIES,
1688
+ OPENSTEER_META_FILE,
1689
+ OPENSTEER_RUNTIME_META_FILE,
1690
+ OPENSTEER_RUNTIME_CREATING_FILE
1691
+ ]);
1692
+ var SKIPPED_ROOT_DIRECTORIES = /* @__PURE__ */ new Set([
1693
+ "Crash Reports",
1694
+ "Crashpad",
1695
+ "BrowserMetrics",
1696
+ "GrShaderCache",
1697
+ "ShaderCache",
1698
+ "GraphiteDawnCache",
1699
+ "component_crx_cache",
1700
+ "Crowd Deny",
1701
+ "hyphen-data",
1702
+ "OnDeviceHeadSuggestModel",
1703
+ "OptimizationGuidePredictionModels",
1704
+ "Segmentation Platform",
1705
+ "SmartCardDeviceNames",
1706
+ "WidevineCdm",
1707
+ "pnacl"
1708
+ ]);
1709
+ async function getOrCreatePersistentProfile(sourceUserDataDir, profileDirectory, profilesRootDir = defaultPersistentProfilesRootDir()) {
1710
+ const resolvedSourceUserDataDir = expandHome(sourceUserDataDir);
1711
+ const targetUserDataDir = (0, import_node_path4.join)(
1712
+ expandHome(profilesRootDir),
1713
+ buildPersistentProfileKey(resolvedSourceUserDataDir, profileDirectory)
1714
+ );
1715
+ const sourceProfileDir = (0, import_node_path4.join)(resolvedSourceUserDataDir, profileDirectory);
1716
+ const metadata = buildPersistentProfileMetadata(
1717
+ resolvedSourceUserDataDir,
1718
+ profileDirectory
1719
+ );
1720
+ await (0, import_promises4.mkdir)((0, import_node_path4.dirname)(targetUserDataDir), { recursive: true });
1721
+ if (await isHealthyPersistentProfile(
1722
+ targetUserDataDir,
1723
+ resolvedSourceUserDataDir,
1724
+ profileDirectory
1725
+ ) && !await isPersistentProfileWriteLocked(targetUserDataDir)) {
1726
+ return {
1727
+ created: false,
1728
+ userDataDir: targetUserDataDir
1729
+ };
1730
+ }
1731
+ return await withPersistentProfileWriteAccess(targetUserDataDir, async () => {
1732
+ await recoverPersistentProfileBackup(targetUserDataDir);
1733
+ await cleanOrphanedOwnedDirs(
1734
+ (0, import_node_path4.dirname)(targetUserDataDir),
1735
+ buildPersistentProfileTempDirNamePrefix(targetUserDataDir)
1736
+ );
1737
+ if (!(0, import_node_fs3.existsSync)(sourceProfileDir)) {
1738
+ throw new Error(
1739
+ `Chrome profile "${profileDirectory}" was not found in "${resolvedSourceUserDataDir}".`
1740
+ );
1741
+ }
1742
+ const created = await createPersistentProfileClone(
1743
+ resolvedSourceUserDataDir,
1744
+ sourceProfileDir,
1745
+ targetUserDataDir,
1746
+ profileDirectory,
1747
+ metadata
1748
+ );
1749
+ await ensurePersistentProfileMetadata(targetUserDataDir, metadata);
1750
+ return {
1751
+ created,
1752
+ userDataDir: targetUserDataDir
1753
+ };
1754
+ });
1755
+ }
1756
+ async function clearPersistentProfileSingletons(userDataDir) {
1757
+ await Promise.all(
1758
+ [...CHROME_SINGLETON_ENTRIES].map(
1759
+ (entry) => (0, import_promises4.rm)((0, import_node_path4.join)(userDataDir, entry), {
1760
+ force: true,
1761
+ recursive: true
1762
+ }).catch(() => void 0)
1763
+ )
1764
+ );
1765
+ }
1766
+ async function createIsolatedRuntimeProfile(sourceUserDataDir, runtimesRootDir = defaultRuntimeProfilesRootDir()) {
1767
+ const resolvedSourceUserDataDir = expandHome(sourceUserDataDir);
1768
+ const runtimeRootDir = expandHome(runtimesRootDir);
1769
+ await (0, import_promises4.mkdir)(runtimeRootDir, { recursive: true });
1770
+ const sourceMetadata = await requirePersistentProfileMetadata(
1771
+ resolvedSourceUserDataDir
1772
+ );
1773
+ const runtimeProfile = await reserveRuntimeProfileCreation(
1774
+ resolvedSourceUserDataDir,
1775
+ runtimeRootDir,
1776
+ sourceMetadata.profileDirectory
1777
+ );
1778
+ try {
1779
+ await cleanOrphanedRuntimeProfileDirs(
1780
+ runtimeRootDir,
1781
+ buildRuntimeProfileDirNamePrefix(resolvedSourceUserDataDir)
1782
+ );
1783
+ await copyUserDataDirSnapshot(
1784
+ resolvedSourceUserDataDir,
1785
+ runtimeProfile.userDataDir
1786
+ );
1787
+ const currentSourceMetadata = await readPersistentProfileMetadata(
1788
+ resolvedSourceUserDataDir
1789
+ );
1790
+ await writeRuntimeProfileMetadata(
1791
+ runtimeProfile.userDataDir,
1792
+ await buildRuntimeProfileMetadata(
1793
+ runtimeProfile.userDataDir,
1794
+ resolvedSourceUserDataDir,
1795
+ currentSourceMetadata?.profileDirectory ?? sourceMetadata.profileDirectory
1796
+ )
1797
+ );
1798
+ await clearRuntimeProfileCreationState(
1799
+ runtimeProfile.userDataDir,
1800
+ resolvedSourceUserDataDir
1801
+ );
1802
+ return {
1803
+ persistentUserDataDir: resolvedSourceUserDataDir,
1804
+ userDataDir: runtimeProfile.userDataDir
1805
+ };
1806
+ } catch (error) {
1807
+ await clearRuntimeProfileCreationState(
1808
+ runtimeProfile.userDataDir,
1809
+ resolvedSourceUserDataDir
1810
+ );
1811
+ await (0, import_promises4.rm)(runtimeProfile.userDataDir, {
1812
+ recursive: true,
1813
+ force: true
1814
+ }).catch(() => void 0);
1815
+ throw error;
1816
+ }
1817
+ }
1818
+ async function persistIsolatedRuntimeProfile(runtimeUserDataDir, persistentUserDataDir) {
1819
+ const resolvedRuntimeUserDataDir = expandHome(runtimeUserDataDir);
1820
+ const resolvedPersistentUserDataDir = expandHome(persistentUserDataDir);
1821
+ let claimedRuntimeUserDataDir = null;
1822
+ try {
1823
+ await withPersistentProfileWriteAccess(
1824
+ resolvedPersistentUserDataDir,
1825
+ async () => {
1826
+ await (0, import_promises4.mkdir)((0, import_node_path4.dirname)(resolvedPersistentUserDataDir), {
1827
+ recursive: true
1828
+ });
1829
+ await recoverPersistentProfileBackup(resolvedPersistentUserDataDir);
1830
+ await cleanOrphanedOwnedDirs(
1831
+ (0, import_node_path4.dirname)(resolvedPersistentUserDataDir),
1832
+ buildPersistentProfileTempDirNamePrefix(
1833
+ resolvedPersistentUserDataDir
1834
+ )
1835
+ );
1836
+ const metadata = await requirePersistentProfileMetadata(
1837
+ resolvedPersistentUserDataDir
1838
+ );
1839
+ claimedRuntimeUserDataDir = await claimRuntimeProfileForPersist(
1840
+ resolvedRuntimeUserDataDir
1841
+ );
1842
+ const runtimeMetadata = await requireRuntimeProfileMetadata(
1843
+ claimedRuntimeUserDataDir,
1844
+ resolvedPersistentUserDataDir,
1845
+ metadata.profileDirectory,
1846
+ resolvedRuntimeUserDataDir
1847
+ );
1848
+ await mergePersistentProfileSnapshot(
1849
+ claimedRuntimeUserDataDir,
1850
+ resolvedPersistentUserDataDir,
1851
+ metadata,
1852
+ runtimeMetadata
1853
+ );
1854
+ }
1855
+ );
1856
+ } catch (error) {
1857
+ if (claimedRuntimeUserDataDir) {
1858
+ try {
1859
+ await restoreClaimedRuntimeProfile(
1860
+ claimedRuntimeUserDataDir,
1861
+ resolvedRuntimeUserDataDir
1862
+ );
1863
+ } catch (restoreError) {
1864
+ throw new AggregateError(
1865
+ [error, restoreError],
1866
+ `Failed to restore runtime profile "${resolvedRuntimeUserDataDir}" after persistence failed.`
1867
+ );
1868
+ }
1869
+ }
1870
+ throw error;
1871
+ }
1872
+ if (claimedRuntimeUserDataDir) {
1873
+ await (0, import_promises4.rm)(claimedRuntimeUserDataDir, {
1874
+ recursive: true,
1875
+ force: true
1876
+ }).catch(() => void 0);
1877
+ }
1878
+ }
1879
+ function buildPersistentProfileKey(sourceUserDataDir, profileDirectory) {
1880
+ const hash = (0, import_node_crypto3.createHash)("sha256").update(`${sourceUserDataDir}\0${profileDirectory}`).digest("hex").slice(0, 16);
1881
+ const sourceLabel = sanitizePathSegment((0, import_node_path4.basename)(sourceUserDataDir) || "user-data");
1882
+ const profileLabel = sanitizePathSegment(profileDirectory || "Default");
1883
+ return `${sourceLabel}-${profileLabel}-${hash}`;
1884
+ }
1885
+ function defaultPersistentProfilesRootDir() {
1886
+ return (0, import_node_path4.join)((0, import_node_os.homedir)(), ".opensteer", "real-browser-profiles");
1887
+ }
1888
+ function defaultRuntimeProfilesRootDir() {
1889
+ return (0, import_node_path4.join)((0, import_node_os.tmpdir)(), "opensteer-real-browser-runtimes");
1890
+ }
1891
+ function sanitizePathSegment(value) {
1892
+ const sanitized = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/-+/g, "-");
1893
+ return sanitized.replace(/^-|-$/g, "") || "profile";
1894
+ }
1895
+ function isProfileDirectory(userDataDir, entry) {
1896
+ return (0, import_node_fs3.existsSync)((0, import_node_path4.join)(userDataDir, entry, "Preferences"));
1897
+ }
1898
+ async function copyUserDataDirSnapshot(sourceUserDataDir, targetUserDataDir) {
1899
+ await (0, import_promises4.cp)(sourceUserDataDir, targetUserDataDir, {
1900
+ recursive: true,
1901
+ filter: (candidatePath) => shouldCopyRuntimeSnapshotEntry(sourceUserDataDir, candidatePath)
1902
+ });
1903
+ }
1904
+ function shouldCopyRuntimeSnapshotEntry(userDataDir, candidatePath) {
1905
+ const candidateRelativePath = (0, import_node_path4.relative)(userDataDir, candidatePath);
1906
+ if (!candidateRelativePath) {
1907
+ return true;
1908
+ }
1909
+ const segments = candidateRelativePath.split(import_node_path4.sep).filter(Boolean);
1910
+ if (segments.length !== 1) {
1911
+ return true;
1912
+ }
1913
+ return !COPY_SKIP_ENTRIES.has(segments[0]);
1914
+ }
1915
+ async function copyRootLevelEntries(sourceUserDataDir, targetUserDataDir, targetProfileDirectory) {
1916
+ let entries;
1917
+ try {
1918
+ entries = await (0, import_promises4.readdir)(sourceUserDataDir);
1919
+ } catch {
1920
+ return;
1921
+ }
1922
+ const copyTasks = [];
1923
+ for (const entry of entries) {
1924
+ if (COPY_SKIP_ENTRIES.has(entry)) continue;
1925
+ if (entry === targetProfileDirectory) continue;
1926
+ const sourcePath = (0, import_node_path4.join)(sourceUserDataDir, entry);
1927
+ const targetPath = (0, import_node_path4.join)(targetUserDataDir, entry);
1928
+ if ((0, import_node_fs3.existsSync)(targetPath)) continue;
1929
+ let entryStat;
1930
+ try {
1931
+ entryStat = await (0, import_promises4.stat)(sourcePath);
1932
+ } catch {
1933
+ continue;
1934
+ }
1935
+ if (entryStat.isFile()) {
1936
+ copyTasks.push((0, import_promises4.copyFile)(sourcePath, targetPath).catch(() => void 0));
1937
+ } else if (entryStat.isDirectory()) {
1938
+ if (isProfileDirectory(sourceUserDataDir, entry)) continue;
1939
+ if (SKIPPED_ROOT_DIRECTORIES.has(entry)) continue;
1940
+ copyTasks.push(
1941
+ (0, import_promises4.cp)(sourcePath, targetPath, { recursive: true }).catch(
1942
+ () => void 0
1943
+ )
1944
+ );
1945
+ }
1946
+ }
1947
+ await Promise.all(copyTasks);
1948
+ }
1949
+ async function writePersistentProfileMetadata(userDataDir, metadata) {
1950
+ await (0, import_promises4.writeFile)(
1951
+ (0, import_node_path4.join)(userDataDir, OPENSTEER_META_FILE),
1952
+ JSON.stringify(metadata, null, 2)
1953
+ );
1954
+ }
1955
+ function buildPersistentProfileMetadata(sourceUserDataDir, profileDirectory) {
1956
+ return {
1957
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1958
+ profileDirectory,
1959
+ source: sourceUserDataDir
1960
+ };
1961
+ }
1962
+ async function createPersistentProfileClone(sourceUserDataDir, sourceProfileDir, targetUserDataDir, profileDirectory, metadata) {
1963
+ if ((0, import_node_fs3.existsSync)(targetUserDataDir)) {
1964
+ return false;
1965
+ }
1966
+ const tempUserDataDir = await (0, import_promises4.mkdtemp)(
1967
+ buildPersistentProfileTempDirPrefix(targetUserDataDir)
1968
+ );
1969
+ let published = false;
1970
+ try {
1971
+ await materializePersistentProfileSnapshot(
1972
+ sourceUserDataDir,
1973
+ sourceProfileDir,
1974
+ tempUserDataDir,
1975
+ profileDirectory,
1976
+ metadata
1977
+ );
1978
+ try {
1979
+ await (0, import_promises4.rename)(tempUserDataDir, targetUserDataDir);
1980
+ } catch (error) {
1981
+ if (wasDirPublishedByAnotherProcess2(error, targetUserDataDir)) {
1982
+ return false;
1983
+ }
1984
+ throw error;
1985
+ }
1986
+ published = true;
1987
+ return true;
1988
+ } finally {
1989
+ if (!published) {
1990
+ await (0, import_promises4.rm)(tempUserDataDir, {
1991
+ recursive: true,
1992
+ force: true
1993
+ }).catch(() => void 0);
1994
+ }
1995
+ }
1996
+ }
1997
+ async function materializePersistentProfileSnapshot(sourceUserDataDir, sourceProfileDir, targetUserDataDir, profileDirectory, metadata) {
1998
+ if (!(0, import_node_fs3.existsSync)(sourceProfileDir)) {
1999
+ throw new Error(
2000
+ `Chrome profile "${profileDirectory}" was not found in "${sourceUserDataDir}".`
2001
+ );
2002
+ }
2003
+ await (0, import_promises4.cp)(sourceProfileDir, (0, import_node_path4.join)(targetUserDataDir, profileDirectory), {
2004
+ recursive: true
2005
+ });
2006
+ await copyRootLevelEntries(sourceUserDataDir, targetUserDataDir, profileDirectory);
2007
+ await writePersistentProfileMetadata(targetUserDataDir, metadata);
2008
+ }
2009
+ async function mergePersistentProfileSnapshot(runtimeUserDataDir, persistentUserDataDir, metadata, runtimeMetadata) {
2010
+ const tempUserDataDir = await (0, import_promises4.mkdtemp)(
2011
+ buildPersistentProfileTempDirPrefix(persistentUserDataDir)
2012
+ );
2013
+ let published = false;
2014
+ try {
2015
+ const baseEntries = deserializeSnapshotManifestEntries(
2016
+ runtimeMetadata.baseEntries
2017
+ );
2018
+ const currentEntries = await collectPersistentSnapshotEntries(
2019
+ persistentUserDataDir,
2020
+ metadata.profileDirectory
2021
+ );
2022
+ const runtimeEntries = await collectPersistentSnapshotEntries(
2023
+ runtimeUserDataDir,
2024
+ metadata.profileDirectory
2025
+ );
2026
+ const mergedEntries = resolveMergedSnapshotEntries(
2027
+ baseEntries,
2028
+ currentEntries,
2029
+ runtimeEntries
2030
+ );
2031
+ await materializeMergedPersistentProfileSnapshot(
2032
+ tempUserDataDir,
2033
+ currentEntries,
2034
+ runtimeEntries,
2035
+ mergedEntries
2036
+ );
2037
+ await writePersistentProfileMetadata(tempUserDataDir, metadata);
2038
+ await replaceProfileDirectory(persistentUserDataDir, tempUserDataDir);
2039
+ published = true;
1362
2040
  } finally {
1363
2041
  if (!published) {
1364
- await (0, import_promises.rm)(tempUserDataDir, {
2042
+ await (0, import_promises4.rm)(tempUserDataDir, {
2043
+ recursive: true,
2044
+ force: true
2045
+ }).catch(() => void 0);
2046
+ }
2047
+ }
2048
+ }
2049
+ async function buildRuntimeProfileMetadata(runtimeUserDataDir, persistentUserDataDir, profileDirectory) {
2050
+ const baseEntries = profileDirectory ? serializeSnapshotManifestEntries(
2051
+ await collectPersistentSnapshotEntries(
2052
+ runtimeUserDataDir,
2053
+ profileDirectory
2054
+ )
2055
+ ) : {};
2056
+ return {
2057
+ baseEntries,
2058
+ creator: CURRENT_PROCESS_OWNER,
2059
+ persistentUserDataDir,
2060
+ profileDirectory
2061
+ };
2062
+ }
2063
+ async function writeRuntimeProfileMetadata(userDataDir, metadata) {
2064
+ await (0, import_promises4.writeFile)(
2065
+ (0, import_node_path4.join)(userDataDir, OPENSTEER_RUNTIME_META_FILE),
2066
+ JSON.stringify(metadata, null, 2)
2067
+ );
2068
+ }
2069
+ async function writeRuntimeProfileCreationMarker(userDataDir, marker) {
2070
+ await (0, import_promises4.writeFile)(
2071
+ (0, import_node_path4.join)(userDataDir, OPENSTEER_RUNTIME_CREATING_FILE),
2072
+ JSON.stringify(marker, null, 2)
2073
+ );
2074
+ }
2075
+ async function readRuntimeProfileMetadata(userDataDir) {
2076
+ try {
2077
+ const raw = await (0, import_promises4.readFile)(
2078
+ (0, import_node_path4.join)(userDataDir, OPENSTEER_RUNTIME_META_FILE),
2079
+ "utf8"
2080
+ );
2081
+ const parsed = JSON.parse(raw);
2082
+ const creator = parseProcessOwner(parsed.creator);
2083
+ const persistentUserDataDir = typeof parsed.persistentUserDataDir === "string" ? parsed.persistentUserDataDir : void 0;
2084
+ const profileDirectory = parsed.profileDirectory === null ? null : typeof parsed.profileDirectory === "string" ? parsed.profileDirectory : void 0;
2085
+ if (!creator || persistentUserDataDir === void 0 || profileDirectory === void 0 || typeof parsed.baseEntries !== "object" || parsed.baseEntries === null || Array.isArray(parsed.baseEntries)) {
2086
+ return null;
2087
+ }
2088
+ const baseEntries = deserializeSnapshotManifestEntries(
2089
+ parsed.baseEntries
2090
+ );
2091
+ return {
2092
+ baseEntries: Object.fromEntries(baseEntries),
2093
+ creator,
2094
+ persistentUserDataDir,
2095
+ profileDirectory
2096
+ };
2097
+ } catch {
2098
+ return null;
2099
+ }
2100
+ }
2101
+ async function readRuntimeProfileCreationMarker(userDataDir) {
2102
+ try {
2103
+ const raw = await (0, import_promises4.readFile)(
2104
+ (0, import_node_path4.join)(userDataDir, OPENSTEER_RUNTIME_CREATING_FILE),
2105
+ "utf8"
2106
+ );
2107
+ return parseRuntimeProfileCreationMarker(JSON.parse(raw));
2108
+ } catch {
2109
+ return null;
2110
+ }
2111
+ }
2112
+ async function requireRuntimeProfileMetadata(userDataDir, expectedPersistentUserDataDir, expectedProfileDirectory, displayUserDataDir = userDataDir) {
2113
+ const metadata = await readRuntimeProfileMetadata(userDataDir);
2114
+ if (!metadata) {
2115
+ throw new Error(
2116
+ `Runtime profile metadata was not found for "${displayUserDataDir}".`
2117
+ );
2118
+ }
2119
+ if (metadata.profileDirectory !== expectedProfileDirectory) {
2120
+ throw new Error(
2121
+ `Runtime profile "${displayUserDataDir}" was created for profile "${metadata.profileDirectory ?? "unknown"}", expected "${expectedProfileDirectory}".`
2122
+ );
2123
+ }
2124
+ if (metadata.persistentUserDataDir !== expectedPersistentUserDataDir) {
2125
+ throw new Error(
2126
+ `Runtime profile "${displayUserDataDir}" does not belong to persistent profile "${expectedPersistentUserDataDir}".`
2127
+ );
2128
+ }
2129
+ return metadata;
2130
+ }
2131
+ async function collectPersistentSnapshotEntries(userDataDir, profileDirectory) {
2132
+ let rootEntries;
2133
+ try {
2134
+ rootEntries = await (0, import_promises4.readdir)(userDataDir, {
2135
+ encoding: "utf8",
2136
+ withFileTypes: true
2137
+ });
2138
+ } catch {
2139
+ return /* @__PURE__ */ new Map();
2140
+ }
2141
+ rootEntries.sort((left, right) => left.name.localeCompare(right.name));
2142
+ const collected = /* @__PURE__ */ new Map();
2143
+ for (const entry of rootEntries) {
2144
+ if (!shouldIncludePersistentRootEntry(
2145
+ userDataDir,
2146
+ profileDirectory,
2147
+ entry.name
2148
+ )) {
2149
+ continue;
2150
+ }
2151
+ await collectSnapshotEntry(
2152
+ (0, import_node_path4.join)(userDataDir, entry.name),
2153
+ entry.name,
2154
+ collected
2155
+ );
2156
+ }
2157
+ return collected;
2158
+ }
2159
+ function shouldIncludePersistentRootEntry(userDataDir, profileDirectory, entry) {
2160
+ if (entry === profileDirectory) {
2161
+ return true;
2162
+ }
2163
+ if (COPY_SKIP_ENTRIES.has(entry)) {
2164
+ return false;
2165
+ }
2166
+ if (SKIPPED_ROOT_DIRECTORIES.has(entry)) {
2167
+ return false;
2168
+ }
2169
+ return !isProfileDirectory(userDataDir, entry);
2170
+ }
2171
+ async function collectSnapshotEntry(sourcePath, relativePath, collected) {
2172
+ let entryStat;
2173
+ try {
2174
+ entryStat = await (0, import_promises4.stat)(sourcePath);
2175
+ } catch {
2176
+ return;
2177
+ }
2178
+ if (entryStat.isDirectory()) {
2179
+ collected.set(relativePath, {
2180
+ kind: "directory",
2181
+ hash: null,
2182
+ sourcePath
2183
+ });
2184
+ let children;
2185
+ try {
2186
+ children = await (0, import_promises4.readdir)(sourcePath, {
2187
+ encoding: "utf8",
2188
+ withFileTypes: true
2189
+ });
2190
+ } catch {
2191
+ return;
2192
+ }
2193
+ children.sort((left, right) => left.name.localeCompare(right.name));
2194
+ for (const child of children) {
2195
+ await collectSnapshotEntry(
2196
+ (0, import_node_path4.join)(sourcePath, child.name),
2197
+ (0, import_node_path4.join)(relativePath, child.name),
2198
+ collected
2199
+ );
2200
+ }
2201
+ return;
2202
+ }
2203
+ if (entryStat.isFile()) {
2204
+ collected.set(relativePath, {
2205
+ kind: "file",
2206
+ hash: await hashSnapshotFile(sourcePath, relativePath),
2207
+ sourcePath
2208
+ });
2209
+ }
2210
+ }
2211
+ function serializeSnapshotManifestEntries(entries) {
2212
+ return Object.fromEntries(
2213
+ [...entries.entries()].sort(([leftPath], [rightPath]) => leftPath.localeCompare(rightPath)).map(([relativePath, entry]) => [
2214
+ relativePath,
2215
+ {
2216
+ kind: entry.kind,
2217
+ hash: entry.hash
2218
+ }
2219
+ ])
2220
+ );
2221
+ }
2222
+ function deserializeSnapshotManifestEntries(entries) {
2223
+ const manifestEntries = /* @__PURE__ */ new Map();
2224
+ for (const [relativePath, entry] of Object.entries(entries)) {
2225
+ if (!entry || entry.kind !== "directory" && entry.kind !== "file" || !(entry.hash === null || typeof entry.hash === "string")) {
2226
+ throw new Error(
2227
+ `Runtime profile metadata for "${relativePath}" is invalid.`
2228
+ );
2229
+ }
2230
+ manifestEntries.set(relativePath, {
2231
+ kind: entry.kind,
2232
+ hash: entry.hash
2233
+ });
2234
+ }
2235
+ return manifestEntries;
2236
+ }
2237
+ function resolveMergedSnapshotEntries(baseEntries, currentEntries, runtimeEntries) {
2238
+ const mergedEntries = /* @__PURE__ */ new Map();
2239
+ const relativePaths = /* @__PURE__ */ new Set([
2240
+ ...baseEntries.keys(),
2241
+ ...currentEntries.keys(),
2242
+ ...runtimeEntries.keys()
2243
+ ]);
2244
+ for (const relativePath of [...relativePaths].sort(compareSnapshotPaths)) {
2245
+ mergedEntries.set(
2246
+ relativePath,
2247
+ resolveMergedSnapshotEntrySelection(
2248
+ relativePath,
2249
+ baseEntries.get(relativePath) ?? null,
2250
+ currentEntries.get(relativePath) ?? null,
2251
+ runtimeEntries.get(relativePath) ?? null
2252
+ )
2253
+ );
2254
+ }
2255
+ return mergedEntries;
2256
+ }
2257
+ function resolveMergedSnapshotEntrySelection(relativePath, baseEntry, currentEntry, runtimeEntry) {
2258
+ if (snapshotEntriesEqual(runtimeEntry, baseEntry)) {
2259
+ return currentEntry ? "current" : null;
2260
+ }
2261
+ if (snapshotEntriesEqual(currentEntry, baseEntry)) {
2262
+ return runtimeEntry ? "runtime" : null;
2263
+ }
2264
+ if (!baseEntry) {
2265
+ if (!currentEntry) {
2266
+ return runtimeEntry ? "runtime" : null;
2267
+ }
2268
+ if (!runtimeEntry) {
2269
+ return "current";
2270
+ }
2271
+ if (snapshotEntriesEqual(currentEntry, runtimeEntry)) {
2272
+ return "current";
2273
+ }
2274
+ throw new Error(
2275
+ `Concurrent runtime updates changed "${relativePath}" differently; refusing to overwrite the persistent profile.`
2276
+ );
2277
+ }
2278
+ if (!currentEntry && !runtimeEntry) {
2279
+ return null;
2280
+ }
2281
+ if (snapshotEntriesEqual(currentEntry, runtimeEntry)) {
2282
+ return currentEntry ? "current" : null;
2283
+ }
2284
+ throw new Error(
2285
+ `Concurrent runtime updates changed "${relativePath}" differently; refusing to overwrite the persistent profile.`
2286
+ );
2287
+ }
2288
+ function snapshotEntriesEqual(left, right) {
2289
+ if (!left || !right) {
2290
+ return left === right;
2291
+ }
2292
+ return left.kind === right.kind && left.hash === right.hash;
2293
+ }
2294
+ async function materializeMergedPersistentProfileSnapshot(targetUserDataDir, currentEntries, runtimeEntries, mergedEntries) {
2295
+ const selectedEntries = [...mergedEntries.entries()].filter(([, selection]) => selection !== null).sort(
2296
+ ([leftPath], [rightPath]) => compareSnapshotPaths(leftPath, rightPath)
2297
+ );
2298
+ for (const [relativePath, selection] of selectedEntries) {
2299
+ const entry = (selection === "current" ? currentEntries.get(relativePath) : runtimeEntries.get(relativePath)) ?? null;
2300
+ if (!entry) {
2301
+ continue;
2302
+ }
2303
+ const targetPath = (0, import_node_path4.join)(targetUserDataDir, relativePath);
2304
+ if (entry.kind === "directory") {
2305
+ await (0, import_promises4.mkdir)(targetPath, { recursive: true });
2306
+ continue;
2307
+ }
2308
+ await (0, import_promises4.mkdir)((0, import_node_path4.dirname)(targetPath), { recursive: true });
2309
+ await (0, import_promises4.copyFile)(entry.sourcePath, targetPath);
2310
+ }
2311
+ }
2312
+ function compareSnapshotPaths(left, right) {
2313
+ const leftDepth = left.split(import_node_path4.sep).length;
2314
+ const rightDepth = right.split(import_node_path4.sep).length;
2315
+ if (leftDepth !== rightDepth) {
2316
+ return leftDepth - rightDepth;
2317
+ }
2318
+ return left.localeCompare(right);
2319
+ }
2320
+ async function hashSnapshotFile(filePath, relativePath) {
2321
+ const normalizedJson = await readNormalizedSnapshotJson(filePath, relativePath);
2322
+ if (normalizedJson !== null) {
2323
+ return (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(normalizedJson)).digest("hex");
2324
+ }
2325
+ return await hashFile(filePath);
2326
+ }
2327
+ async function readNormalizedSnapshotJson(filePath, relativePath) {
2328
+ const normalizer = SNAPSHOT_JSON_NORMALIZERS.get(relativePath);
2329
+ if (!normalizer) {
2330
+ return null;
2331
+ }
2332
+ try {
2333
+ const parsed = JSON.parse(await (0, import_promises4.readFile)(filePath, "utf8"));
2334
+ return normalizer(parsed);
2335
+ } catch {
2336
+ return null;
2337
+ }
2338
+ }
2339
+ var SNAPSHOT_JSON_NORMALIZERS = /* @__PURE__ */ new Map([["Local State", normalizeLocalStateSnapshotJson]]);
2340
+ function normalizeLocalStateSnapshotJson(value) {
2341
+ if (!isJsonRecord(value)) {
2342
+ return value;
2343
+ }
2344
+ const { user_experience_metrics: _ignored, ...rest } = value;
2345
+ return rest;
2346
+ }
2347
+ function isJsonRecord(value) {
2348
+ return !!value && typeof value === "object" && !Array.isArray(value);
2349
+ }
2350
+ async function hashFile(filePath) {
2351
+ return new Promise((resolve, reject) => {
2352
+ const hash = (0, import_node_crypto3.createHash)("sha256");
2353
+ const stream = (0, import_node_fs3.createReadStream)(filePath);
2354
+ stream.on("data", (chunk) => {
2355
+ hash.update(chunk);
2356
+ });
2357
+ stream.on("error", reject);
2358
+ stream.on("end", () => {
2359
+ resolve(hash.digest("hex"));
2360
+ });
2361
+ });
2362
+ }
2363
+ async function ensurePersistentProfileMetadata(userDataDir, metadata) {
2364
+ if ((0, import_node_fs3.existsSync)((0, import_node_path4.join)(userDataDir, OPENSTEER_META_FILE))) {
2365
+ return;
2366
+ }
2367
+ await writePersistentProfileMetadata(userDataDir, metadata);
2368
+ }
2369
+ async function recoverPersistentProfileBackup(targetUserDataDir) {
2370
+ const backupDirPaths = await listPersistentProfileBackupDirs(targetUserDataDir);
2371
+ if (backupDirPaths.length === 0) {
2372
+ return;
2373
+ }
2374
+ if (!(0, import_node_fs3.existsSync)(targetUserDataDir)) {
2375
+ const [latestBackupDirPath, ...staleBackupDirPaths] = backupDirPaths;
2376
+ await (0, import_promises4.rename)(latestBackupDirPath, targetUserDataDir);
2377
+ await Promise.all(
2378
+ staleBackupDirPaths.map(
2379
+ (backupDirPath) => (0, import_promises4.rm)(backupDirPath, {
2380
+ recursive: true,
2381
+ force: true
2382
+ }).catch(() => void 0)
2383
+ )
2384
+ );
2385
+ return;
2386
+ }
2387
+ await Promise.all(
2388
+ backupDirPaths.map(
2389
+ (backupDirPath) => (0, import_promises4.rm)(backupDirPath, {
2390
+ recursive: true,
2391
+ force: true
2392
+ }).catch(() => void 0)
2393
+ )
2394
+ );
2395
+ }
2396
+ async function listPersistentProfileBackupDirs(targetUserDataDir) {
2397
+ const profilesDir = (0, import_node_path4.dirname)(targetUserDataDir);
2398
+ let entries;
2399
+ try {
2400
+ entries = await (0, import_promises4.readdir)(profilesDir, {
2401
+ encoding: "utf8",
2402
+ withFileTypes: true
2403
+ });
2404
+ } catch {
2405
+ return [];
2406
+ }
2407
+ const backupDirNamePrefix = buildPersistentProfileBackupDirNamePrefix(targetUserDataDir);
2408
+ return entries.filter(
2409
+ (entry) => entry.isDirectory() && entry.name.startsWith(backupDirNamePrefix)
2410
+ ).map((entry) => (0, import_node_path4.join)(profilesDir, entry.name)).sort((leftPath, rightPath) => rightPath.localeCompare(leftPath));
2411
+ }
2412
+ async function readPersistentProfileMetadata(userDataDir) {
2413
+ try {
2414
+ const raw = await (0, import_promises4.readFile)((0, import_node_path4.join)(userDataDir, OPENSTEER_META_FILE), "utf8");
2415
+ const parsed = JSON.parse(raw);
2416
+ if (typeof parsed.createdAt !== "string" || typeof parsed.profileDirectory !== "string" || typeof parsed.source !== "string") {
2417
+ return null;
2418
+ }
2419
+ return {
2420
+ createdAt: parsed.createdAt,
2421
+ profileDirectory: parsed.profileDirectory,
2422
+ source: parsed.source
2423
+ };
2424
+ } catch {
2425
+ return null;
2426
+ }
2427
+ }
2428
+ async function requirePersistentProfileMetadata(userDataDir) {
2429
+ const metadata = await readPersistentProfileMetadata(userDataDir);
2430
+ if (!metadata) {
2431
+ throw new Error(
2432
+ `Persistent profile metadata was not found for "${userDataDir}".`
2433
+ );
2434
+ }
2435
+ return metadata;
2436
+ }
2437
+ async function isHealthyPersistentProfile(userDataDir, expectedSourceUserDataDir, expectedProfileDirectory) {
2438
+ if (!(0, import_node_fs3.existsSync)(userDataDir) || !(0, import_node_fs3.existsSync)((0, import_node_path4.join)(userDataDir, expectedProfileDirectory))) {
2439
+ return false;
2440
+ }
2441
+ const metadata = await readPersistentProfileMetadata(userDataDir);
2442
+ return metadata?.source === expectedSourceUserDataDir && metadata.profileDirectory === expectedProfileDirectory;
2443
+ }
2444
+ function wasDirPublishedByAnotherProcess2(error, targetDirPath) {
2445
+ const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
2446
+ return (0, import_node_fs3.existsSync)(targetDirPath) && (code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM");
2447
+ }
2448
+ async function replaceProfileDirectory(targetUserDataDir, replacementUserDataDir) {
2449
+ if (!(0, import_node_fs3.existsSync)(targetUserDataDir)) {
2450
+ await (0, import_promises4.rename)(replacementUserDataDir, targetUserDataDir);
2451
+ return;
2452
+ }
2453
+ const backupUserDataDir = buildPersistentProfileBackupDirPath(targetUserDataDir);
2454
+ let targetMovedToBackup = false;
2455
+ let replacementPublished = false;
2456
+ try {
2457
+ await (0, import_promises4.rename)(targetUserDataDir, backupUserDataDir);
2458
+ targetMovedToBackup = true;
2459
+ await (0, import_promises4.rename)(replacementUserDataDir, targetUserDataDir);
2460
+ replacementPublished = true;
2461
+ } catch (error) {
2462
+ if (targetMovedToBackup && !(0, import_node_fs3.existsSync)(targetUserDataDir)) {
2463
+ await (0, import_promises4.rename)(backupUserDataDir, targetUserDataDir).catch(() => void 0);
2464
+ }
2465
+ throw error;
2466
+ } finally {
2467
+ if (replacementPublished && targetMovedToBackup && (0, import_node_fs3.existsSync)(backupUserDataDir)) {
2468
+ await (0, import_promises4.rm)(backupUserDataDir, {
2469
+ recursive: true,
2470
+ force: true
2471
+ }).catch(() => void 0);
2472
+ }
2473
+ }
2474
+ }
2475
+ async function withPersistentProfileWriteAccess(targetUserDataDir, action) {
2476
+ const releaseWriteLock = await acquirePersistentProfileWriteLock(
2477
+ targetUserDataDir
2478
+ );
2479
+ try {
2480
+ await waitForRuntimeProfileCreationsToDrain(targetUserDataDir);
2481
+ await waitForSharedRealBrowserSessionToDrain(targetUserDataDir);
2482
+ return await action();
2483
+ } finally {
2484
+ await releaseWriteLock();
2485
+ }
2486
+ }
2487
+ function buildPersistentProfileTempDirPrefix(targetUserDataDir) {
2488
+ return (0, import_node_path4.join)(
2489
+ (0, import_node_path4.dirname)(targetUserDataDir),
2490
+ `${buildPersistentProfileTempDirNamePrefix(targetUserDataDir)}${process.pid}-${CURRENT_PROCESS_OWNER.processStartedAtMs}-`
2491
+ );
2492
+ }
2493
+ function buildPersistentProfileTempDirNamePrefix(targetUserDataDir) {
2494
+ return `${(0, import_node_path4.basename)(targetUserDataDir)}-tmp-`;
2495
+ }
2496
+ function buildPersistentProfileBackupDirPath(targetUserDataDir) {
2497
+ return (0, import_node_path4.join)(
2498
+ (0, import_node_path4.dirname)(targetUserDataDir),
2499
+ `${buildPersistentProfileBackupDirNamePrefix(targetUserDataDir)}${Date.now()}-${process.pid}-${CURRENT_PROCESS_OWNER.processStartedAtMs}-${(0, import_node_crypto3.randomUUID)()}`
2500
+ );
2501
+ }
2502
+ function buildPersistentProfileBackupDirNamePrefix(targetUserDataDir) {
2503
+ return `${(0, import_node_path4.basename)(targetUserDataDir)}-backup-`;
2504
+ }
2505
+ function buildRuntimeProfileCreationRegistryDirPath(persistentUserDataDir) {
2506
+ return (0, import_node_path4.join)(
2507
+ (0, import_node_path4.dirname)(persistentUserDataDir),
2508
+ `${(0, import_node_path4.basename)(persistentUserDataDir)}.creating`
2509
+ );
2510
+ }
2511
+ function buildRuntimeProfileCreationRegistrationPath(persistentUserDataDir, runtimeUserDataDir) {
2512
+ const key = (0, import_node_crypto3.createHash)("sha256").update(runtimeUserDataDir).digest("hex").slice(0, 16);
2513
+ return (0, import_node_path4.join)(
2514
+ buildRuntimeProfileCreationRegistryDirPath(persistentUserDataDir),
2515
+ `${key}.json`
2516
+ );
2517
+ }
2518
+ function buildRuntimeProfileKey(sourceUserDataDir) {
2519
+ const hash = (0, import_node_crypto3.createHash)("sha256").update(sourceUserDataDir).digest("hex").slice(0, 16);
2520
+ return `${sanitizePathSegment((0, import_node_path4.basename)(sourceUserDataDir) || "profile")}-${hash}`;
2521
+ }
2522
+ function buildRuntimeProfileDirNamePrefix(sourceUserDataDir) {
2523
+ return `${buildRuntimeProfileKey(sourceUserDataDir)}-runtime-`;
2524
+ }
2525
+ function buildRuntimeProfileDirPrefix(runtimesRootDir, sourceUserDataDir) {
2526
+ return (0, import_node_path4.join)(
2527
+ runtimesRootDir,
2528
+ buildRuntimeProfileDirNamePrefix(sourceUserDataDir)
2529
+ );
2530
+ }
2531
+ async function reserveRuntimeProfileCreation(persistentUserDataDir, runtimeRootDir, profileDirectory) {
2532
+ while (true) {
2533
+ let runtimeUserDataDir = null;
2534
+ await withPersistentProfileControlLock(
2535
+ persistentUserDataDir,
2536
+ async () => {
2537
+ if (await isPersistentProfileWriteLocked(persistentUserDataDir)) {
2538
+ return;
2539
+ }
2540
+ if (await hasLiveSharedRealBrowserSession(persistentUserDataDir)) {
2541
+ return;
2542
+ }
2543
+ const createdRuntimeUserDataDir = await (0, import_promises4.mkdtemp)(
2544
+ buildRuntimeProfileDirPrefix(
2545
+ runtimeRootDir,
2546
+ persistentUserDataDir
2547
+ )
2548
+ );
2549
+ runtimeUserDataDir = createdRuntimeUserDataDir;
2550
+ const marker = {
2551
+ creator: CURRENT_PROCESS_OWNER,
2552
+ persistentUserDataDir,
2553
+ profileDirectory,
2554
+ runtimeUserDataDir: createdRuntimeUserDataDir
2555
+ };
2556
+ await writeRuntimeProfileCreationMarker(
2557
+ createdRuntimeUserDataDir,
2558
+ marker
2559
+ );
2560
+ await writeRuntimeProfileCreationRegistration(marker);
2561
+ }
2562
+ );
2563
+ if (runtimeUserDataDir) {
2564
+ return {
2565
+ persistentUserDataDir,
2566
+ userDataDir: runtimeUserDataDir
2567
+ };
2568
+ }
2569
+ await sleep4(PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS);
2570
+ }
2571
+ }
2572
+ async function clearRuntimeProfileCreationState(runtimeUserDataDir, persistentUserDataDir) {
2573
+ await Promise.all([
2574
+ (0, import_promises4.rm)((0, import_node_path4.join)(runtimeUserDataDir, OPENSTEER_RUNTIME_CREATING_FILE), {
2575
+ force: true
2576
+ }).catch(() => void 0),
2577
+ (0, import_promises4.rm)(
2578
+ buildRuntimeProfileCreationRegistrationPath(
2579
+ persistentUserDataDir,
2580
+ runtimeUserDataDir
2581
+ ),
2582
+ {
2583
+ force: true
2584
+ }
2585
+ ).catch(() => void 0)
2586
+ ]);
2587
+ }
2588
+ async function writeRuntimeProfileCreationRegistration(marker) {
2589
+ const registryDirPath = buildRuntimeProfileCreationRegistryDirPath(
2590
+ marker.persistentUserDataDir
2591
+ );
2592
+ await (0, import_promises4.mkdir)(registryDirPath, { recursive: true });
2593
+ await (0, import_promises4.writeFile)(
2594
+ buildRuntimeProfileCreationRegistrationPath(
2595
+ marker.persistentUserDataDir,
2596
+ marker.runtimeUserDataDir
2597
+ ),
2598
+ JSON.stringify(marker, null, 2)
2599
+ );
2600
+ }
2601
+ async function listRuntimeProfileCreationRegistrations(persistentUserDataDir) {
2602
+ const registryDirPath = buildRuntimeProfileCreationRegistryDirPath(
2603
+ persistentUserDataDir
2604
+ );
2605
+ let entries;
2606
+ try {
2607
+ entries = await (0, import_promises4.readdir)(registryDirPath, {
2608
+ encoding: "utf8",
2609
+ withFileTypes: true
2610
+ });
2611
+ } catch {
2612
+ return [];
2613
+ }
2614
+ return await Promise.all(
2615
+ entries.filter((entry) => entry.isFile()).map(async (entry) => {
2616
+ const filePath = (0, import_node_path4.join)(registryDirPath, entry.name);
2617
+ return {
2618
+ filePath,
2619
+ marker: await readRuntimeProfileCreationRegistration(filePath)
2620
+ };
2621
+ })
2622
+ );
2623
+ }
2624
+ async function readRuntimeProfileCreationRegistration(filePath) {
2625
+ try {
2626
+ const raw = await (0, import_promises4.readFile)(filePath, "utf8");
2627
+ return parseRuntimeProfileCreationMarker(JSON.parse(raw));
2628
+ } catch {
2629
+ return null;
2630
+ }
2631
+ }
2632
+ async function cleanOrphanedRuntimeProfileDirs(rootDir, runtimeDirNamePrefix) {
2633
+ let entries;
2634
+ try {
2635
+ entries = await (0, import_promises4.readdir)(rootDir, {
2636
+ encoding: "utf8",
2637
+ withFileTypes: true
2638
+ });
2639
+ } catch {
2640
+ return;
2641
+ }
2642
+ const liveProcessCommandLines = await listProcessCommandLines();
2643
+ await Promise.all(
2644
+ entries.map(async (entry) => {
2645
+ if (!entry.isDirectory() || !entry.name.startsWith(runtimeDirNamePrefix)) {
2646
+ return;
2647
+ }
2648
+ const runtimeDirPath = (0, import_node_path4.join)(rootDir, entry.name);
2649
+ const creationMarker = await readRuntimeProfileCreationMarker(
2650
+ runtimeDirPath
2651
+ );
2652
+ if (await isRuntimeProfileDirInUse(
2653
+ runtimeDirPath,
2654
+ liveProcessCommandLines
2655
+ )) {
2656
+ return;
2657
+ }
2658
+ await (0, import_promises4.rm)(runtimeDirPath, {
2659
+ recursive: true,
2660
+ force: true
2661
+ }).catch(() => void 0);
2662
+ if (creationMarker) {
2663
+ await clearRuntimeProfileCreationState(
2664
+ runtimeDirPath,
2665
+ creationMarker.persistentUserDataDir
2666
+ );
2667
+ }
2668
+ })
2669
+ );
2670
+ }
2671
+ async function cleanOrphanedOwnedDirs(rootDir, ownedDirNamePrefix) {
2672
+ let entries;
2673
+ try {
2674
+ entries = await (0, import_promises4.readdir)(rootDir, {
2675
+ encoding: "utf8",
2676
+ withFileTypes: true
2677
+ });
2678
+ } catch {
2679
+ return;
2680
+ }
2681
+ await Promise.all(
2682
+ entries.map(async (entry) => {
2683
+ if (!entry.isDirectory() || !entry.name.startsWith(ownedDirNamePrefix)) {
2684
+ return;
2685
+ }
2686
+ if (await isOwnedDirByLiveProcess(entry.name, ownedDirNamePrefix)) {
2687
+ return;
2688
+ }
2689
+ await (0, import_promises4.rm)((0, import_node_path4.join)(rootDir, entry.name), {
1365
2690
  recursive: true,
1366
2691
  force: true
1367
2692
  }).catch(() => void 0);
1368
- }
1369
- }
1370
- }
1371
- async function buildRuntimeProfileMetadata(runtimeUserDataDir, profileDirectory) {
1372
- if (!profileDirectory) {
1373
- return {
1374
- baseEntries: {},
1375
- profileDirectory: null
1376
- };
1377
- }
1378
- const baseEntries = await collectPersistentSnapshotEntries(
1379
- runtimeUserDataDir,
1380
- profileDirectory
2693
+ })
1381
2694
  );
1382
- return {
1383
- baseEntries: serializeSnapshotManifestEntries(baseEntries),
1384
- profileDirectory
1385
- };
1386
2695
  }
1387
- async function writeRuntimeProfileMetadata(userDataDir, metadata) {
1388
- await (0, import_promises.writeFile)(
1389
- (0, import_node_path.join)(userDataDir, OPENSTEER_RUNTIME_META_FILE),
1390
- JSON.stringify(metadata, null, 2)
2696
+ async function isOwnedDirByLiveProcess(ownedDirName, ownedDirPrefix) {
2697
+ const owner = parseOwnedDirOwner(ownedDirName, ownedDirPrefix);
2698
+ return owner ? await getProcessLiveness(owner) !== "dead" : false;
2699
+ }
2700
+ async function isRuntimeProfileDirInUse(runtimeDirPath, liveProcessCommandLines) {
2701
+ const creationMarker = await readRuntimeProfileCreationMarker(runtimeDirPath);
2702
+ if (creationMarker && await getProcessLiveness(creationMarker.creator) !== "dead") {
2703
+ return true;
2704
+ }
2705
+ const metadata = await readRuntimeProfileMetadata(runtimeDirPath);
2706
+ if (metadata && await getProcessLiveness(metadata.creator) !== "dead") {
2707
+ return true;
2708
+ }
2709
+ return liveProcessCommandLines.some(
2710
+ (commandLine) => commandLineIncludesUserDataDir(commandLine, runtimeDirPath)
1391
2711
  );
1392
2712
  }
1393
- async function readRuntimeProfileMetadata(userDataDir) {
1394
- try {
1395
- const raw = await (0, import_promises.readFile)(
1396
- (0, import_node_path.join)(userDataDir, OPENSTEER_RUNTIME_META_FILE),
1397
- "utf8"
1398
- );
1399
- const parsed = JSON.parse(raw);
1400
- const profileDirectory = parsed.profileDirectory === null ? null : typeof parsed.profileDirectory === "string" ? parsed.profileDirectory : void 0;
1401
- if (profileDirectory === void 0 || typeof parsed.baseEntries !== "object" || parsed.baseEntries === null || Array.isArray(parsed.baseEntries)) {
1402
- return null;
2713
+ async function claimRuntimeProfileForPersist(runtimeUserDataDir) {
2714
+ while (true) {
2715
+ await waitForRuntimeProfileProcessesToDrain(runtimeUserDataDir);
2716
+ const claimedRuntimeUserDataDir = buildClaimedRuntimeProfileDirPath(runtimeUserDataDir);
2717
+ try {
2718
+ await (0, import_promises4.rename)(runtimeUserDataDir, claimedRuntimeUserDataDir);
2719
+ } catch (error) {
2720
+ const code = getErrorCode3(error);
2721
+ if (code === "ENOENT") {
2722
+ throw new Error(
2723
+ `Runtime profile "${runtimeUserDataDir}" was not found.`
2724
+ );
2725
+ }
2726
+ if (code === "EACCES" || code === "EBUSY" || code === "EPERM") {
2727
+ await sleep4(PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS);
2728
+ continue;
2729
+ }
2730
+ throw error;
1403
2731
  }
1404
- const baseEntries = deserializeSnapshotManifestEntries(
1405
- parsed.baseEntries
1406
- );
1407
- return {
1408
- baseEntries: Object.fromEntries(baseEntries),
1409
- profileDirectory
1410
- };
1411
- } catch {
1412
- return null;
2732
+ if (!await hasLiveProcessUsingUserDataDir(runtimeUserDataDir)) {
2733
+ return claimedRuntimeUserDataDir;
2734
+ }
2735
+ await (0, import_promises4.rename)(
2736
+ claimedRuntimeUserDataDir,
2737
+ runtimeUserDataDir
2738
+ ).catch(() => void 0);
2739
+ await sleep4(PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS);
1413
2740
  }
1414
2741
  }
1415
- async function requireRuntimeProfileMetadata(userDataDir, expectedProfileDirectory) {
1416
- const metadata = await readRuntimeProfileMetadata(userDataDir);
1417
- if (!metadata) {
1418
- throw new Error(
1419
- `Runtime profile metadata was not found for "${userDataDir}".`
1420
- );
2742
+ async function restoreClaimedRuntimeProfile(claimedRuntimeUserDataDir, runtimeUserDataDir) {
2743
+ if (!(0, import_node_fs3.existsSync)(claimedRuntimeUserDataDir)) {
2744
+ return;
1421
2745
  }
1422
- if (metadata.profileDirectory !== expectedProfileDirectory) {
2746
+ if ((0, import_node_fs3.existsSync)(runtimeUserDataDir)) {
1423
2747
  throw new Error(
1424
- `Runtime profile "${userDataDir}" was created for profile "${metadata.profileDirectory ?? "unknown"}", expected "${expectedProfileDirectory}".`
2748
+ `Runtime profile "${runtimeUserDataDir}" was recreated before the failed persist could restore it from "${claimedRuntimeUserDataDir}".`
1425
2749
  );
1426
2750
  }
1427
- return metadata;
2751
+ await (0, import_promises4.rename)(claimedRuntimeUserDataDir, runtimeUserDataDir);
1428
2752
  }
1429
- async function collectPersistentSnapshotEntries(userDataDir, profileDirectory) {
1430
- let rootEntries;
1431
- try {
1432
- rootEntries = await (0, import_promises.readdir)(userDataDir, {
1433
- encoding: "utf8",
1434
- withFileTypes: true
1435
- });
1436
- } catch {
1437
- return /* @__PURE__ */ new Map();
2753
+ function buildClaimedRuntimeProfileDirPath(runtimeUserDataDir) {
2754
+ return (0, import_node_path4.join)(
2755
+ (0, import_node_path4.dirname)(runtimeUserDataDir),
2756
+ `${(0, import_node_path4.basename)(runtimeUserDataDir)}-persisting-${process.pid}-${CURRENT_PROCESS_OWNER.processStartedAtMs}-${(0, import_node_crypto3.randomUUID)()}`
2757
+ );
2758
+ }
2759
+ async function waitForRuntimeProfileProcessesToDrain(runtimeUserDataDir) {
2760
+ while (await hasLiveProcessUsingUserDataDir(runtimeUserDataDir)) {
2761
+ await sleep4(PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS);
1438
2762
  }
1439
- rootEntries.sort((left, right) => left.name.localeCompare(right.name));
1440
- const collected = /* @__PURE__ */ new Map();
1441
- for (const entry of rootEntries) {
1442
- if (!shouldIncludePersistentRootEntry(
1443
- userDataDir,
1444
- profileDirectory,
1445
- entry.name
1446
- )) {
2763
+ }
2764
+ async function hasLiveProcessUsingUserDataDir(userDataDir) {
2765
+ const liveProcessCommandLines = await listProcessCommandLines();
2766
+ return liveProcessCommandLines.some(
2767
+ (commandLine) => commandLineIncludesUserDataDir(commandLine, userDataDir)
2768
+ );
2769
+ }
2770
+ async function hasActiveRuntimeProfileCreations(persistentUserDataDir) {
2771
+ const registrations = await listRuntimeProfileCreationRegistrations(
2772
+ persistentUserDataDir
2773
+ );
2774
+ let hasLiveCreation = false;
2775
+ for (const registration of registrations) {
2776
+ const marker = registration.marker;
2777
+ if (!marker || marker.persistentUserDataDir !== persistentUserDataDir) {
2778
+ await (0, import_promises4.rm)(registration.filePath, {
2779
+ force: true
2780
+ }).catch(() => void 0);
1447
2781
  continue;
1448
2782
  }
1449
- await collectSnapshotEntry(
1450
- (0, import_node_path.join)(userDataDir, entry.name),
1451
- entry.name,
1452
- collected
2783
+ const runtimeMarker = await readRuntimeProfileCreationMarker(
2784
+ marker.runtimeUserDataDir
1453
2785
  );
2786
+ if (!runtimeMarker || runtimeMarker.persistentUserDataDir !== persistentUserDataDir || runtimeMarker.runtimeUserDataDir !== marker.runtimeUserDataDir) {
2787
+ await clearRuntimeProfileCreationState(
2788
+ marker.runtimeUserDataDir,
2789
+ persistentUserDataDir
2790
+ );
2791
+ continue;
2792
+ }
2793
+ if (await getProcessLiveness(runtimeMarker.creator) === "dead") {
2794
+ await clearRuntimeProfileCreationState(
2795
+ marker.runtimeUserDataDir,
2796
+ persistentUserDataDir
2797
+ );
2798
+ await (0, import_promises4.rm)(marker.runtimeUserDataDir, {
2799
+ recursive: true,
2800
+ force: true
2801
+ }).catch(() => void 0);
2802
+ continue;
2803
+ }
2804
+ hasLiveCreation = true;
1454
2805
  }
1455
- return collected;
2806
+ return hasLiveCreation;
1456
2807
  }
1457
- function shouldIncludePersistentRootEntry(userDataDir, profileDirectory, entry) {
1458
- if (entry === profileDirectory) {
1459
- return true;
2808
+ async function waitForRuntimeProfileCreationsToDrain(persistentUserDataDir) {
2809
+ while (true) {
2810
+ if (!await hasActiveRuntimeProfileCreations(persistentUserDataDir)) {
2811
+ return;
2812
+ }
2813
+ await sleep4(PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS);
1460
2814
  }
1461
- if (COPY_SKIP_ENTRIES.has(entry)) {
1462
- return false;
2815
+ }
2816
+ function parseRuntimeProfileCreationMarker(value) {
2817
+ if (!value || typeof value !== "object") {
2818
+ return null;
1463
2819
  }
1464
- if (SKIPPED_ROOT_DIRECTORIES.has(entry)) {
1465
- return false;
2820
+ const parsed = value;
2821
+ const creator = parseProcessOwner(parsed.creator);
2822
+ const persistentUserDataDir = typeof parsed.persistentUserDataDir === "string" ? parsed.persistentUserDataDir : void 0;
2823
+ const profileDirectory = parsed.profileDirectory === null ? null : typeof parsed.profileDirectory === "string" ? parsed.profileDirectory : void 0;
2824
+ const runtimeUserDataDir = typeof parsed.runtimeUserDataDir === "string" ? parsed.runtimeUserDataDir : void 0;
2825
+ if (!creator || persistentUserDataDir === void 0 || profileDirectory === void 0 || runtimeUserDataDir === void 0) {
2826
+ return null;
1466
2827
  }
1467
- return !isProfileDirectory(userDataDir, entry);
2828
+ return {
2829
+ creator,
2830
+ persistentUserDataDir,
2831
+ profileDirectory,
2832
+ runtimeUserDataDir
2833
+ };
1468
2834
  }
1469
- async function collectSnapshotEntry(sourcePath, relativePath, collected) {
1470
- let entryStat;
1471
- try {
1472
- entryStat = await (0, import_promises.stat)(sourcePath);
1473
- } catch {
1474
- return;
2835
+ function parseOwnedDirOwner(ownedDirName, ownedDirPrefix) {
2836
+ const remainder = ownedDirName.slice(ownedDirPrefix.length);
2837
+ const firstDashIndex = remainder.indexOf("-");
2838
+ const secondDashIndex = firstDashIndex === -1 ? -1 : remainder.indexOf("-", firstDashIndex + 1);
2839
+ if (firstDashIndex === -1 || secondDashIndex === -1) {
2840
+ return null;
1475
2841
  }
1476
- if (entryStat.isDirectory()) {
1477
- collected.set(relativePath, {
1478
- kind: "directory",
1479
- hash: null,
1480
- sourcePath
1481
- });
1482
- let children;
1483
- try {
1484
- children = await (0, import_promises.readdir)(sourcePath, {
1485
- encoding: "utf8",
1486
- withFileTypes: true
1487
- });
1488
- } catch {
1489
- return;
1490
- }
1491
- children.sort((left, right) => left.name.localeCompare(right.name));
1492
- for (const child of children) {
1493
- await collectSnapshotEntry(
1494
- (0, import_node_path.join)(sourcePath, child.name),
1495
- (0, import_node_path.join)(relativePath, child.name),
1496
- collected
1497
- );
1498
- }
1499
- return;
2842
+ const pid = Number.parseInt(remainder.slice(0, firstDashIndex), 10);
2843
+ const processStartedAtMs = Number.parseInt(
2844
+ remainder.slice(firstDashIndex + 1, secondDashIndex),
2845
+ 10
2846
+ );
2847
+ if (!Number.isInteger(pid) || pid <= 0) {
2848
+ return null;
1500
2849
  }
1501
- if (entryStat.isFile()) {
1502
- collected.set(relativePath, {
1503
- kind: "file",
1504
- hash: await hashFile(sourcePath),
1505
- sourcePath
1506
- });
2850
+ if (!Number.isInteger(processStartedAtMs) || processStartedAtMs <= 0) {
2851
+ return null;
1507
2852
  }
2853
+ return { pid, processStartedAtMs };
1508
2854
  }
1509
- function serializeSnapshotManifestEntries(entries) {
1510
- return Object.fromEntries(
1511
- [...entries.entries()].sort(([leftPath], [rightPath]) => leftPath.localeCompare(rightPath)).map(([relativePath, entry]) => [
1512
- relativePath,
2855
+ async function listProcessCommandLines() {
2856
+ if (process.platform === "win32") {
2857
+ return await listWindowsProcessCommandLines();
2858
+ }
2859
+ return await listPsProcessCommandLines();
2860
+ }
2861
+ async function listPsProcessCommandLines() {
2862
+ try {
2863
+ const { stdout } = await execFileAsync2(
2864
+ "ps",
2865
+ ["-axww", "-o", "command="],
1513
2866
  {
1514
- kind: entry.kind,
1515
- hash: entry.hash
2867
+ env: PS_COMMAND_ENV2,
2868
+ maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES2
1516
2869
  }
1517
- ])
1518
- );
1519
- }
1520
- function deserializeSnapshotManifestEntries(entries) {
1521
- const manifestEntries = /* @__PURE__ */ new Map();
1522
- for (const [relativePath, entry] of Object.entries(entries)) {
1523
- if (!entry || entry.kind !== "directory" && entry.kind !== "file" || !(entry.hash === null || typeof entry.hash === "string")) {
1524
- throw new Error(
1525
- `Runtime profile metadata for "${relativePath}" is invalid.`
1526
- );
1527
- }
1528
- manifestEntries.set(relativePath, {
1529
- kind: entry.kind,
1530
- hash: entry.hash
1531
- });
2870
+ );
2871
+ return stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
2872
+ } catch {
2873
+ return [];
1532
2874
  }
1533
- return manifestEntries;
1534
2875
  }
1535
- function resolveMergedSnapshotEntries(baseEntries, currentEntries, runtimeEntries) {
1536
- const mergedEntries = /* @__PURE__ */ new Map();
1537
- const relativePaths = /* @__PURE__ */ new Set([
1538
- ...baseEntries.keys(),
1539
- ...currentEntries.keys(),
1540
- ...runtimeEntries.keys()
1541
- ]);
1542
- for (const relativePath of [...relativePaths].sort(compareSnapshotPaths)) {
1543
- mergedEntries.set(
1544
- relativePath,
1545
- resolveMergedSnapshotEntrySelection(
1546
- relativePath,
1547
- baseEntries.get(relativePath) ?? null,
1548
- currentEntries.get(relativePath) ?? null,
1549
- runtimeEntries.get(relativePath) ?? null
1550
- )
2876
+ async function listWindowsProcessCommandLines() {
2877
+ const script = [
2878
+ "$processes = Get-CimInstance Win32_Process | Select-Object CommandLine",
2879
+ "$processes | ConvertTo-Json -Compress"
2880
+ ].join("; ");
2881
+ try {
2882
+ const { stdout } = await execFileAsync2(
2883
+ "powershell.exe",
2884
+ ["-NoLogo", "-NoProfile", "-Command", script],
2885
+ {
2886
+ maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES2
2887
+ }
1551
2888
  );
2889
+ const parsed = JSON.parse(stdout);
2890
+ const records = Array.isArray(parsed) ? parsed : [parsed];
2891
+ return records.map((record) => record?.CommandLine?.trim() ?? "").filter((commandLine) => commandLine.length > 0);
2892
+ } catch {
2893
+ return [];
1552
2894
  }
1553
- return mergedEntries;
1554
2895
  }
1555
- function resolveMergedSnapshotEntrySelection(relativePath, baseEntry, currentEntry, runtimeEntry) {
1556
- if (snapshotEntriesEqual(runtimeEntry, baseEntry)) {
1557
- return currentEntry ? "current" : null;
1558
- }
1559
- if (snapshotEntriesEqual(currentEntry, baseEntry)) {
1560
- return runtimeEntry ? "runtime" : null;
1561
- }
1562
- if (!baseEntry) {
1563
- if (!currentEntry) {
1564
- return runtimeEntry ? "runtime" : null;
1565
- }
1566
- if (!runtimeEntry) {
1567
- return "current";
1568
- }
1569
- if (snapshotEntriesEqual(currentEntry, runtimeEntry)) {
1570
- return "current";
2896
+ function commandLineIncludesUserDataDir(commandLine, userDataDir) {
2897
+ const unquoted = `--user-data-dir=${userDataDir}`;
2898
+ const unquotedIndex = commandLine.indexOf(unquoted);
2899
+ if (unquotedIndex !== -1) {
2900
+ const after = commandLine[unquotedIndex + unquoted.length];
2901
+ if (after === void 0 || after === " " || after === " ") {
2902
+ return true;
1571
2903
  }
1572
- throw new Error(
1573
- `Concurrent runtime updates changed "${relativePath}" differently; refusing to overwrite the persistent profile.`
1574
- );
1575
- }
1576
- if (!currentEntry && !runtimeEntry) {
1577
- return null;
1578
- }
1579
- if (snapshotEntriesEqual(currentEntry, runtimeEntry)) {
1580
- return currentEntry ? "current" : null;
1581
2904
  }
1582
- throw new Error(
1583
- `Concurrent runtime updates changed "${relativePath}" differently; refusing to overwrite the persistent profile.`
1584
- );
2905
+ return [
2906
+ `--user-data-dir="${userDataDir}"`,
2907
+ `--user-data-dir='${userDataDir}'`
2908
+ ].some((candidate) => commandLine.includes(candidate));
2909
+ }
2910
+ function getErrorCode3(error) {
2911
+ return typeof error === "object" && error !== null && "code" in error && (typeof error.code === "string" || typeof error.code === "number") ? error.code : void 0;
2912
+ }
2913
+ async function sleep4(ms) {
2914
+ await new Promise((resolve) => setTimeout(resolve, ms));
2915
+ }
2916
+
2917
+ // src/browser/shared-real-browser-session.ts
2918
+ var import_node_crypto4 = require("crypto");
2919
+ var import_node_child_process3 = require("child_process");
2920
+ var import_promises5 = require("fs/promises");
2921
+ var import_node_net = require("net");
2922
+ var import_node_path5 = require("path");
2923
+ var import_playwright = require("playwright");
2924
+ var SHARED_SESSION_RETRY_DELAY_MS2 = 50;
2925
+ async function acquireSharedRealBrowserSession(options) {
2926
+ const reservation = await reserveSharedSessionClient(options);
2927
+ const sessionContext = await attachToSharedSession(reservation, options);
2928
+ let closed = false;
2929
+ return {
2930
+ browser: sessionContext.browser,
2931
+ context: sessionContext.context,
2932
+ page: sessionContext.page,
2933
+ close: async () => {
2934
+ if (closed) {
2935
+ return;
2936
+ }
2937
+ closed = true;
2938
+ await releaseSharedSessionClient(sessionContext);
2939
+ }
2940
+ };
1585
2941
  }
1586
- function snapshotEntriesEqual(left, right) {
1587
- if (!left || !right) {
1588
- return left === right;
2942
+ function getOwnedRealBrowserProcessPolicy(platformName = process.platform) {
2943
+ if (platformName === "win32") {
2944
+ return {
2945
+ detached: false,
2946
+ killStrategy: "taskkill",
2947
+ shouldUnref: true
2948
+ };
1589
2949
  }
1590
- return left.kind === right.kind && left.hash === right.hash;
2950
+ if (platformName === "darwin") {
2951
+ return {
2952
+ detached: false,
2953
+ killStrategy: "process",
2954
+ shouldUnref: true
2955
+ };
2956
+ }
2957
+ return {
2958
+ detached: true,
2959
+ killStrategy: "process-group",
2960
+ shouldUnref: true
2961
+ };
1591
2962
  }
1592
- async function materializeMergedPersistentProfileSnapshot(targetUserDataDir, currentEntries, runtimeEntries, mergedEntries) {
1593
- const selectedEntries = [...mergedEntries.entries()].filter(([, selection]) => selection !== null).sort(
1594
- ([leftPath], [rightPath]) => compareSnapshotPaths(leftPath, rightPath)
1595
- );
1596
- for (const [relativePath, selection] of selectedEntries) {
1597
- const entry = (selection === "current" ? currentEntries.get(relativePath) : runtimeEntries.get(relativePath)) ?? null;
1598
- if (!entry) {
2963
+ async function reserveSharedSessionClient(options) {
2964
+ while (true) {
2965
+ const outcome = await withPersistentProfileControlLock(
2966
+ options.persistentProfile.userDataDir,
2967
+ async () => {
2968
+ if (await isPersistentProfileWriteLocked(
2969
+ options.persistentProfile.userDataDir
2970
+ )) {
2971
+ return { kind: "wait" };
2972
+ }
2973
+ if (await hasActiveRuntimeProfileCreations(
2974
+ options.persistentProfile.userDataDir
2975
+ )) {
2976
+ return { kind: "wait" };
2977
+ }
2978
+ return await withSharedSessionLock(
2979
+ options.persistentProfile.userDataDir,
2980
+ async () => {
2981
+ const state = await inspectSharedSessionState(options);
2982
+ if (state.kind === "wait") {
2983
+ return { kind: "wait" };
2984
+ }
2985
+ if (state.kind === "ready") {
2986
+ return {
2987
+ kind: "ready",
2988
+ reservation: await registerSharedSessionClient(
2989
+ options.persistentProfile.userDataDir,
2990
+ state.metadata
2991
+ )
2992
+ };
2993
+ }
2994
+ return {
2995
+ kind: "launch",
2996
+ reservation: await launchSharedSession(options)
2997
+ };
2998
+ }
2999
+ );
3000
+ }
3001
+ );
3002
+ if (outcome.kind === "wait") {
3003
+ await sleep5(SHARED_SESSION_RETRY_DELAY_MS2);
1599
3004
  continue;
1600
3005
  }
1601
- const targetPath = (0, import_node_path.join)(targetUserDataDir, relativePath);
1602
- if (entry.kind === "directory") {
1603
- await (0, import_promises.mkdir)(targetPath, { recursive: true });
1604
- continue;
3006
+ if (outcome.kind === "ready") {
3007
+ return outcome.reservation;
3008
+ }
3009
+ try {
3010
+ await waitForSharedSessionReady(
3011
+ outcome.reservation.metadata,
3012
+ options.timeoutMs
3013
+ );
3014
+ } catch (error) {
3015
+ await cleanupFailedSharedSessionLaunch(outcome.reservation);
3016
+ throw error;
3017
+ }
3018
+ try {
3019
+ return await withSharedSessionLock(
3020
+ options.persistentProfile.userDataDir,
3021
+ async () => {
3022
+ const metadata = await readSharedSessionMetadata(
3023
+ options.persistentProfile.userDataDir
3024
+ );
3025
+ if (!metadata || metadata.sessionId !== outcome.reservation.metadata.sessionId || !processOwnersEqual(
3026
+ metadata.browserOwner,
3027
+ outcome.reservation.launchedBrowserOwner
3028
+ )) {
3029
+ throw new Error(
3030
+ "The shared real-browser session changed before launch finalized."
3031
+ );
3032
+ }
3033
+ const readyMetadata = {
3034
+ ...metadata,
3035
+ state: "ready"
3036
+ };
3037
+ await writeSharedSessionMetadata(
3038
+ options.persistentProfile.userDataDir,
3039
+ readyMetadata
3040
+ );
3041
+ return await registerSharedSessionClient(
3042
+ options.persistentProfile.userDataDir,
3043
+ readyMetadata
3044
+ );
3045
+ }
3046
+ );
3047
+ } catch (error) {
3048
+ await cleanupFailedSharedSessionLaunch(outcome.reservation);
3049
+ throw error;
1605
3050
  }
1606
- await (0, import_promises.mkdir)((0, import_node_path.dirname)(targetPath), { recursive: true });
1607
- await (0, import_promises.copyFile)(entry.sourcePath, targetPath);
1608
- }
1609
- }
1610
- function compareSnapshotPaths(left, right) {
1611
- const leftDepth = left.split(import_node_path.sep).length;
1612
- const rightDepth = right.split(import_node_path.sep).length;
1613
- if (leftDepth !== rightDepth) {
1614
- return leftDepth - rightDepth;
1615
3051
  }
1616
- return left.localeCompare(right);
1617
3052
  }
1618
- async function hashFile(filePath) {
1619
- return new Promise((resolve, reject) => {
1620
- const hash = (0, import_node_crypto.createHash)("sha256");
1621
- const stream = (0, import_node_fs.createReadStream)(filePath);
1622
- stream.on("data", (chunk) => {
1623
- hash.update(chunk);
3053
+ async function attachToSharedSession(reservation, options) {
3054
+ let browser = null;
3055
+ let page = null;
3056
+ try {
3057
+ const browserWsUrl = await resolveCdpWebSocketUrl(
3058
+ buildSharedSessionDiscoveryUrl(reservation.metadata.debugPort),
3059
+ options.timeoutMs
3060
+ );
3061
+ browser = await import_playwright.chromium.connectOverCDP(browserWsUrl, {
3062
+ timeout: options.timeoutMs
1624
3063
  });
1625
- stream.on("error", reject);
1626
- stream.on("end", () => {
1627
- resolve(hash.digest("hex"));
3064
+ const context = getPrimaryBrowserContext(browser);
3065
+ page = await getSharedSessionPage(context, reservation.reuseExistingPage);
3066
+ if (options.initialUrl) {
3067
+ await page.goto(options.initialUrl, {
3068
+ timeout: options.timeoutMs,
3069
+ waitUntil: "domcontentloaded"
3070
+ });
3071
+ }
3072
+ return {
3073
+ browser,
3074
+ clientId: reservation.client.clientId,
3075
+ context,
3076
+ page,
3077
+ persistentUserDataDir: reservation.metadata.persistentUserDataDir,
3078
+ sessionId: reservation.metadata.sessionId
3079
+ };
3080
+ } catch (error) {
3081
+ if (page) {
3082
+ await page.close().catch(() => void 0);
3083
+ }
3084
+ if (browser) {
3085
+ await browser.close().catch(() => void 0);
3086
+ }
3087
+ await cleanupFailedSharedSessionAttach({
3088
+ clientId: reservation.client.clientId,
3089
+ persistentUserDataDir: reservation.metadata.persistentUserDataDir,
3090
+ sessionId: reservation.metadata.sessionId
1628
3091
  });
1629
- });
3092
+ throw error;
3093
+ }
1630
3094
  }
1631
- async function ensurePersistentProfileMetadata(userDataDir, metadata) {
1632
- if ((0, import_node_fs.existsSync)((0, import_node_path.join)(userDataDir, OPENSTEER_META_FILE))) {
3095
+ async function releaseSharedSessionClient(context) {
3096
+ const releasePlan = await prepareSharedSessionCloseIfIdle(
3097
+ context.persistentUserDataDir,
3098
+ context.clientId,
3099
+ context.sessionId
3100
+ );
3101
+ if (releasePlan.closeBrowser) {
3102
+ await closeSharedSessionBrowser(
3103
+ context.persistentUserDataDir,
3104
+ releasePlan,
3105
+ context.browser
3106
+ );
1633
3107
  return;
1634
3108
  }
1635
- await writePersistentProfileMetadata(userDataDir, metadata);
3109
+ await context.page.close().catch(() => void 0);
3110
+ await context.browser.close().catch(() => void 0);
1636
3111
  }
1637
- async function readPersistentProfileMetadata(userDataDir) {
1638
- try {
1639
- const raw = await (0, import_promises.readFile)((0, import_node_path.join)(userDataDir, OPENSTEER_META_FILE), "utf8");
1640
- const parsed = JSON.parse(raw);
1641
- if (typeof parsed.createdAt !== "string" || typeof parsed.profileDirectory !== "string" || typeof parsed.source !== "string") {
1642
- return null;
3112
+ async function inspectSharedSessionState(options) {
3113
+ const persistentUserDataDir = options.persistentProfile.userDataDir;
3114
+ const liveClients = await listLiveSharedSessionClients(persistentUserDataDir);
3115
+ const metadata = await readSharedSessionMetadata(persistentUserDataDir);
3116
+ if (!metadata) {
3117
+ if (liveClients.length > 0) {
3118
+ throw new Error(
3119
+ `Shared real-browser session metadata for "${persistentUserDataDir}" is missing while clients are still attached.`
3120
+ );
1643
3121
  }
3122
+ await (0, import_promises5.rm)(buildSharedSessionDirPath(persistentUserDataDir), {
3123
+ force: true,
3124
+ recursive: true
3125
+ }).catch(() => void 0);
3126
+ return { kind: "missing" };
3127
+ }
3128
+ assertSharedSessionCompatibility(metadata, options);
3129
+ const browserState = await getProcessLiveness(metadata.browserOwner);
3130
+ if (browserState === "dead") {
3131
+ await (0, import_promises5.rm)(buildSharedSessionDirPath(persistentUserDataDir), {
3132
+ force: true,
3133
+ recursive: true
3134
+ }).catch(() => void 0);
3135
+ return { kind: "missing" };
3136
+ }
3137
+ if (metadata.state === "ready") {
1644
3138
  return {
1645
- createdAt: parsed.createdAt,
1646
- profileDirectory: parsed.profileDirectory,
1647
- source: parsed.source
3139
+ kind: "ready",
3140
+ metadata
1648
3141
  };
1649
- } catch {
1650
- return null;
1651
3142
  }
1652
- }
1653
- async function requirePersistentProfileMetadata(userDataDir) {
1654
- const metadata = await readPersistentProfileMetadata(userDataDir);
1655
- if (!metadata) {
1656
- throw new Error(
1657
- `Persistent profile metadata was not found for "${userDataDir}".`
1658
- );
3143
+ const stateOwnerState = await getProcessLiveness(metadata.stateOwner);
3144
+ if (stateOwnerState === "dead") {
3145
+ const recoveredMetadata = {
3146
+ ...metadata,
3147
+ state: "ready"
3148
+ };
3149
+ await writeSharedSessionMetadata(persistentUserDataDir, recoveredMetadata);
3150
+ return {
3151
+ kind: "ready",
3152
+ metadata: recoveredMetadata
3153
+ };
1659
3154
  }
1660
- return metadata;
1661
- }
1662
- function wasDirPublishedByAnotherProcess(error, targetDirPath) {
1663
- const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
1664
- return (0, import_node_fs.existsSync)(targetDirPath) && (code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM");
1665
- }
1666
- async function replaceProfileDirectory(targetUserDataDir, replacementUserDataDir) {
1667
- if (!(0, import_node_fs.existsSync)(targetUserDataDir)) {
1668
- await (0, import_promises.rename)(replacementUserDataDir, targetUserDataDir);
1669
- return;
3155
+ return { kind: "wait" };
3156
+ }
3157
+ async function launchSharedSession(options) {
3158
+ const persistentUserDataDir = options.persistentProfile.userDataDir;
3159
+ await clearPersistentProfileSingletons(persistentUserDataDir);
3160
+ const debugPort = await reserveDebugPort();
3161
+ const launchArgs = buildRealBrowserLaunchArgs({
3162
+ debugPort,
3163
+ headless: options.headless,
3164
+ profileDirectory: options.profileDirectory,
3165
+ userDataDir: persistentUserDataDir
3166
+ });
3167
+ const processPolicy = getOwnedRealBrowserProcessPolicy();
3168
+ const processHandle = (0, import_node_child_process3.spawn)(options.executablePath, launchArgs, {
3169
+ detached: processPolicy.detached,
3170
+ stdio: "ignore"
3171
+ });
3172
+ if (processPolicy.shouldUnref) {
3173
+ processHandle.unref();
1670
3174
  }
1671
- const backupUserDataDir = buildPersistentProfileBackupDirPath(targetUserDataDir);
1672
- let targetMovedToBackup = false;
1673
- let replacementPublished = false;
1674
3175
  try {
1675
- await (0, import_promises.rename)(targetUserDataDir, backupUserDataDir);
1676
- targetMovedToBackup = true;
1677
- await (0, import_promises.rename)(replacementUserDataDir, targetUserDataDir);
1678
- replacementPublished = true;
3176
+ const browserOwner = await waitForSpawnedProcessOwner(
3177
+ processHandle.pid,
3178
+ options.timeoutMs
3179
+ );
3180
+ const metadata = {
3181
+ browserOwner,
3182
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3183
+ debugPort,
3184
+ executablePath: options.executablePath,
3185
+ headless: options.headless,
3186
+ persistentUserDataDir,
3187
+ profileDirectory: options.profileDirectory,
3188
+ sessionId: (0, import_node_crypto4.randomUUID)(),
3189
+ state: "launching",
3190
+ stateOwner: CURRENT_PROCESS_OWNER
3191
+ };
3192
+ await writeSharedSessionMetadata(persistentUserDataDir, metadata);
3193
+ return {
3194
+ launchedBrowserOwner: browserOwner,
3195
+ metadata
3196
+ };
1679
3197
  } catch (error) {
1680
- if (targetMovedToBackup && !(0, import_node_fs.existsSync)(targetUserDataDir)) {
1681
- await (0, import_promises.rename)(backupUserDataDir, targetUserDataDir).catch(() => void 0);
1682
- }
3198
+ await killSpawnedBrowserProcess(processHandle);
3199
+ await (0, import_promises5.rm)(buildSharedSessionDirPath(persistentUserDataDir), {
3200
+ force: true,
3201
+ recursive: true
3202
+ }).catch(() => void 0);
1683
3203
  throw error;
1684
- } finally {
1685
- if (replacementPublished && targetMovedToBackup && (0, import_node_fs.existsSync)(backupUserDataDir)) {
1686
- await (0, import_promises.rm)(backupUserDataDir, {
1687
- recursive: true,
1688
- force: true
1689
- }).catch(() => void 0);
1690
- }
1691
3204
  }
1692
3205
  }
1693
- async function withPersistentProfileLock(targetUserDataDir, action) {
1694
- const lockDirPath = buildPersistentProfileLockDirPath(targetUserDataDir);
1695
- await (0, import_promises.mkdir)((0, import_node_path.dirname)(lockDirPath), { recursive: true });
1696
- while (true) {
1697
- const tempLockDirPath = `${lockDirPath}-${process.pid}-${PROCESS_STARTED_AT_MS}-${(0, import_node_crypto.randomUUID)()}`;
1698
- try {
1699
- await (0, import_promises.mkdir)(tempLockDirPath);
1700
- await writeLockOwner(tempLockDirPath, CURRENT_PROCESS_LOCK_OWNER);
1701
- try {
1702
- await (0, import_promises.rename)(tempLockDirPath, lockDirPath);
1703
- break;
1704
- } catch (error) {
1705
- if (!wasDirPublishedByAnotherProcess(error, lockDirPath)) {
1706
- throw error;
3206
+ async function cleanupFailedSharedSessionLaunch(reservation) {
3207
+ const shouldPreserveLiveBrowser = await withSharedSessionLock(
3208
+ reservation.metadata.persistentUserDataDir,
3209
+ async () => {
3210
+ const metadata = await readSharedSessionMetadata(
3211
+ reservation.metadata.persistentUserDataDir
3212
+ );
3213
+ if (metadata && metadata.sessionId === reservation.metadata.sessionId && processOwnersEqual(
3214
+ metadata.browserOwner,
3215
+ reservation.launchedBrowserOwner
3216
+ )) {
3217
+ if (await getProcessLiveness(metadata.browserOwner) !== "dead") {
3218
+ const readyMetadata = {
3219
+ ...metadata,
3220
+ state: "ready"
3221
+ };
3222
+ await writeSharedSessionMetadata(
3223
+ reservation.metadata.persistentUserDataDir,
3224
+ readyMetadata
3225
+ );
3226
+ return true;
1707
3227
  }
3228
+ await (0, import_promises5.rm)(
3229
+ buildSharedSessionDirPath(
3230
+ reservation.metadata.persistentUserDataDir
3231
+ ),
3232
+ {
3233
+ force: true,
3234
+ recursive: true
3235
+ }
3236
+ ).catch(() => void 0);
1708
3237
  }
1709
- } finally {
1710
- await (0, import_promises.rm)(tempLockDirPath, {
1711
- recursive: true,
1712
- force: true
1713
- }).catch(() => void 0);
1714
- }
1715
- const owner = await readLockOwner(lockDirPath);
1716
- if ((!owner || await getProcessLiveness(owner) === "dead") && await tryReclaimStaleLock(lockDirPath, owner)) {
1717
- continue;
3238
+ return false;
1718
3239
  }
1719
- await sleep(PROFILE_LOCK_RETRY_DELAY_MS);
1720
- }
1721
- try {
1722
- return await action();
1723
- } finally {
1724
- await (0, import_promises.rm)(lockDirPath, {
1725
- recursive: true,
1726
- force: true
1727
- }).catch(() => void 0);
3240
+ );
3241
+ if (shouldPreserveLiveBrowser) {
3242
+ return;
1728
3243
  }
3244
+ await killOwnedBrowserProcess(reservation.launchedBrowserOwner);
3245
+ await waitForProcessToExit(reservation.launchedBrowserOwner, 2e3);
1729
3246
  }
1730
- async function writeLockOwner(lockDirPath, owner) {
1731
- await writeLockParticipant((0, import_node_path.join)(lockDirPath, LOCK_OWNER_FILE), owner);
1732
- }
1733
- async function readLockOwner(lockDirPath) {
1734
- return await readLockParticipant((0, import_node_path.join)(lockDirPath, LOCK_OWNER_FILE));
1735
- }
1736
- async function writeLockParticipant(filePath, owner, options) {
1737
- await (0, import_promises.writeFile)(filePath, JSON.stringify(owner), options);
1738
- }
1739
- async function readLockParticipant(filePath) {
1740
- return (await readLockParticipantRecord(filePath)).owner;
3247
+ async function cleanupFailedSharedSessionAttach(options) {
3248
+ const closePlan = await prepareSharedSessionCloseIfIdle(
3249
+ options.persistentUserDataDir,
3250
+ options.clientId,
3251
+ options.sessionId
3252
+ );
3253
+ if (!closePlan.closeBrowser) {
3254
+ return;
3255
+ }
3256
+ await closeSharedSessionBrowser(options.persistentUserDataDir, closePlan);
1741
3257
  }
1742
- async function readLockReclaimerRecord(lockDirPath) {
1743
- return await readLockParticipantRecord(
1744
- (0, import_node_path.join)(buildLockReclaimerDirPath(lockDirPath), LOCK_OWNER_FILE)
3258
+ async function waitForSharedSessionReady(metadata, timeoutMs) {
3259
+ await resolveCdpWebSocketUrl(
3260
+ buildSharedSessionDiscoveryUrl(metadata.debugPort),
3261
+ timeoutMs
1745
3262
  );
1746
3263
  }
1747
- async function readLockParticipantRecord(filePath) {
1748
- try {
1749
- const raw = await (0, import_promises.readFile)(filePath, "utf8");
1750
- const parsed = JSON.parse(raw);
1751
- const pid = Number(parsed.pid);
1752
- const processStartedAtMs = Number(parsed.processStartedAtMs);
1753
- if (!Number.isInteger(pid) || !Number.isInteger(processStartedAtMs)) {
1754
- return {
1755
- exists: true,
1756
- owner: null
1757
- };
1758
- }
1759
- return {
1760
- exists: true,
1761
- owner: {
1762
- pid,
1763
- processStartedAtMs
1764
- }
1765
- };
1766
- } catch (error) {
1767
- return {
1768
- exists: getErrorCode(error) !== "ENOENT",
1769
- owner: null
1770
- };
3264
+ function buildRealBrowserLaunchArgs(options) {
3265
+ const args = [
3266
+ `--user-data-dir=${options.userDataDir}`,
3267
+ `--profile-directory=${options.profileDirectory}`,
3268
+ `--remote-debugging-port=${options.debugPort}`,
3269
+ "--disable-blink-features=AutomationControlled"
3270
+ ];
3271
+ if (options.headless) {
3272
+ args.push("--headless=new");
1771
3273
  }
3274
+ return args;
1772
3275
  }
1773
- async function tryReclaimStaleLock(lockDirPath, expectedOwner) {
1774
- if (!await tryAcquireLockReclaimer(lockDirPath)) {
1775
- return false;
1776
- }
1777
- let reclaimed = false;
3276
+ async function requestBrowserShutdown(browser) {
3277
+ let session = null;
1778
3278
  try {
1779
- const owner = await readLockOwner(lockDirPath);
1780
- if (!lockOwnersEqual(owner, expectedOwner)) {
1781
- return false;
1782
- }
1783
- if (owner && await getProcessLiveness(owner) !== "dead") {
1784
- return false;
1785
- }
1786
- await (0, import_promises.rm)(lockDirPath, {
1787
- recursive: true,
1788
- force: true
1789
- }).catch(() => void 0);
1790
- reclaimed = !(0, import_node_fs.existsSync)(lockDirPath);
1791
- return reclaimed;
3279
+ session = await browser.newBrowserCDPSession();
3280
+ await session.send("Browser.close");
3281
+ } catch {
1792
3282
  } finally {
1793
- if (!reclaimed) {
1794
- await (0, import_promises.rm)(buildLockReclaimerDirPath(lockDirPath), {
1795
- recursive: true,
1796
- force: true
1797
- }).catch(() => void 0);
1798
- }
3283
+ await session?.detach().catch(() => void 0);
3284
+ }
3285
+ }
3286
+ async function killOwnedBrowserProcess(owner) {
3287
+ if (await getProcessLiveness(owner) === "dead") {
3288
+ return;
3289
+ }
3290
+ await killOwnedBrowserProcessByPid(owner.pid);
3291
+ }
3292
+ async function killSpawnedBrowserProcess(processHandle) {
3293
+ const pid = processHandle.pid;
3294
+ if (!pid || processHandle.exitCode !== null) {
3295
+ return;
3296
+ }
3297
+ await killOwnedBrowserProcessByPid(pid);
3298
+ await waitForPidToExit(pid, 2e3);
3299
+ }
3300
+ async function killOwnedBrowserProcessByPid(pid) {
3301
+ const processPolicy = getOwnedRealBrowserProcessPolicy();
3302
+ if (processPolicy.killStrategy === "taskkill") {
3303
+ await new Promise((resolve) => {
3304
+ const killer = (0, import_node_child_process3.spawn)(
3305
+ "taskkill",
3306
+ ["/pid", String(pid), "/t", "/f"],
3307
+ {
3308
+ stdio: "ignore"
3309
+ }
3310
+ );
3311
+ killer.on("error", () => resolve());
3312
+ killer.on("exit", () => resolve());
3313
+ });
3314
+ return;
1799
3315
  }
1800
- }
1801
- async function tryAcquireLockReclaimer(lockDirPath) {
1802
- const reclaimerDirPath = buildLockReclaimerDirPath(lockDirPath);
1803
- while (true) {
1804
- const tempReclaimerDirPath = `${reclaimerDirPath}-${process.pid}-${PROCESS_STARTED_AT_MS}-${(0, import_node_crypto.randomUUID)()}`;
3316
+ if (processPolicy.killStrategy === "process-group") {
1805
3317
  try {
1806
- await (0, import_promises.mkdir)(tempReclaimerDirPath);
1807
- await writeLockOwner(tempReclaimerDirPath, CURRENT_PROCESS_LOCK_OWNER);
1808
- try {
1809
- await (0, import_promises.rename)(tempReclaimerDirPath, reclaimerDirPath);
1810
- return true;
1811
- } catch (error) {
1812
- if (getErrorCode(error) === "ENOENT") {
1813
- return false;
1814
- }
1815
- if (!wasDirPublishedByAnotherProcess(error, reclaimerDirPath)) {
1816
- throw error;
1817
- }
1818
- }
1819
- } catch (error) {
1820
- const code = getErrorCode(error);
1821
- if (code === "ENOENT") {
1822
- return false;
1823
- }
1824
- throw error;
1825
- } finally {
1826
- await (0, import_promises.rm)(tempReclaimerDirPath, {
1827
- recursive: true,
1828
- force: true
1829
- }).catch(() => void 0);
1830
- }
1831
- const reclaimerRecord = await readLockReclaimerRecord(lockDirPath);
1832
- if (!reclaimerRecord.exists || !reclaimerRecord.owner) {
1833
- return false;
3318
+ process.kill(-pid, "SIGKILL");
3319
+ return;
3320
+ } catch {
1834
3321
  }
1835
- if (await getProcessLiveness(reclaimerRecord.owner) !== "dead") {
1836
- return false;
3322
+ }
3323
+ try {
3324
+ process.kill(pid, "SIGKILL");
3325
+ } catch {
3326
+ }
3327
+ }
3328
+ async function waitForProcessToExit(owner, timeoutMs) {
3329
+ const deadline = Date.now() + timeoutMs;
3330
+ while (Date.now() < deadline) {
3331
+ if (await getProcessLiveness(owner) === "dead") {
3332
+ return;
1837
3333
  }
1838
- await (0, import_promises.rm)(reclaimerDirPath, {
1839
- recursive: true,
1840
- force: true
1841
- }).catch(() => void 0);
3334
+ await sleep5(50);
1842
3335
  }
1843
3336
  }
1844
- function lockOwnersEqual(left, right) {
1845
- if (!left || !right) {
1846
- return left === right;
3337
+ async function waitForPidToExit(pid, timeoutMs) {
3338
+ const deadline = Date.now() + timeoutMs;
3339
+ while (Date.now() < deadline) {
3340
+ if (!isProcessRunning(pid)) {
3341
+ return;
3342
+ }
3343
+ await sleep5(50);
1847
3344
  }
1848
- return left.pid === right.pid && left.processStartedAtMs === right.processStartedAtMs;
1849
3345
  }
1850
- async function getProcessLiveness(owner) {
1851
- if (owner.pid === process.pid && hasMatchingProcessStartTime(
1852
- owner.processStartedAtMs,
1853
- PROCESS_STARTED_AT_MS
1854
- )) {
1855
- return "live";
3346
+ async function waitForSpawnedProcessOwner(pid, timeoutMs) {
3347
+ if (!pid || pid <= 0) {
3348
+ throw new Error("Chrome did not expose a child process id.");
1856
3349
  }
1857
- const startedAtMs = await readProcessStartedAtMs(owner.pid);
1858
- if (typeof startedAtMs === "number") {
1859
- return hasMatchingProcessStartTime(
1860
- owner.processStartedAtMs,
1861
- startedAtMs
1862
- ) ? "live" : "dead";
3350
+ const deadline = Date.now() + timeoutMs;
3351
+ while (Date.now() < deadline) {
3352
+ const owner = await readProcessOwner(pid);
3353
+ if (owner) {
3354
+ return owner;
3355
+ }
3356
+ await sleep5(50);
1863
3357
  }
1864
- return isProcessRunning(owner.pid) ? "unknown" : "dead";
1865
- }
1866
- function hasMatchingProcessStartTime(expectedStartedAtMs, actualStartedAtMs) {
1867
- return Math.abs(expectedStartedAtMs - actualStartedAtMs) <= PROCESS_START_TIME_TOLERANCE_MS;
1868
- }
1869
- function buildPersistentProfileTempDirPrefix(targetUserDataDir) {
1870
- return (0, import_node_path.join)(
1871
- (0, import_node_path.dirname)(targetUserDataDir),
1872
- `${buildPersistentProfileTempDirNamePrefix(targetUserDataDir)}${process.pid}-${PROCESS_STARTED_AT_MS}-`
3358
+ throw new Error(
3359
+ `Chrome process ${pid} did not report a stable process start time.`
1873
3360
  );
1874
3361
  }
1875
- function buildPersistentProfileTempDirNamePrefix(targetUserDataDir) {
1876
- return `${(0, import_node_path.basename)(targetUserDataDir)}-tmp-`;
1877
- }
1878
- function buildPersistentProfileBackupDirPath(targetUserDataDir) {
1879
- return (0, import_node_path.join)(
1880
- (0, import_node_path.dirname)(targetUserDataDir),
1881
- `${buildPersistentProfileTempDirNamePrefix(targetUserDataDir)}${process.pid}-${PROCESS_STARTED_AT_MS}-backup-${Date.now()}`
3362
+ async function withSharedSessionLock(persistentUserDataDir, action) {
3363
+ return await withDirLock(
3364
+ buildSharedSessionLockPath(persistentUserDataDir),
3365
+ action
1882
3366
  );
1883
3367
  }
1884
- function buildPersistentProfileLockDirPath(targetUserDataDir) {
1885
- return (0, import_node_path.join)((0, import_node_path.dirname)(targetUserDataDir), `${(0, import_node_path.basename)(targetUserDataDir)}.lock`);
1886
- }
1887
- function buildLockReclaimerDirPath(lockDirPath) {
1888
- return (0, import_node_path.join)(lockDirPath, LOCK_RECLAIMER_DIR);
1889
- }
1890
- function buildRuntimeProfileKey(sourceUserDataDir) {
1891
- const hash = (0, import_node_crypto.createHash)("sha256").update(sourceUserDataDir).digest("hex").slice(0, 16);
1892
- return `${sanitizePathSegment((0, import_node_path.basename)(sourceUserDataDir) || "profile")}-${hash}`;
1893
- }
1894
- function buildRuntimeProfileDirNamePrefix(sourceUserDataDir) {
1895
- return `${buildRuntimeProfileKey(sourceUserDataDir)}-runtime-`;
1896
- }
1897
- function buildRuntimeProfileDirPrefix(runtimesRootDir, sourceUserDataDir) {
1898
- return (0, import_node_path.join)(
1899
- runtimesRootDir,
1900
- `${buildRuntimeProfileDirNamePrefix(sourceUserDataDir)}${process.pid}-${PROCESS_STARTED_AT_MS}-`
3368
+ async function registerSharedSessionClient(persistentUserDataDir, metadata) {
3369
+ const liveClients = await listLiveSharedSessionClients(persistentUserDataDir);
3370
+ const client = buildSharedSessionClientRegistration();
3371
+ await (0, import_promises5.mkdir)(buildSharedSessionClientsDirPath(persistentUserDataDir), {
3372
+ recursive: true
3373
+ });
3374
+ await (0, import_promises5.writeFile)(
3375
+ buildSharedSessionClientPath(persistentUserDataDir, client.clientId),
3376
+ JSON.stringify(client, null, 2),
3377
+ {
3378
+ flag: "wx"
3379
+ }
1901
3380
  );
3381
+ return {
3382
+ client,
3383
+ metadata,
3384
+ reuseExistingPage: liveClients.length === 0
3385
+ };
1902
3386
  }
1903
- async function cleanOrphanedOwnedDirs(rootDir, ownedDirNamePrefix) {
3387
+ async function removeSharedSessionClientRegistration(persistentUserDataDir, clientId) {
3388
+ await (0, import_promises5.rm)(buildSharedSessionClientPath(persistentUserDataDir, clientId), {
3389
+ force: true
3390
+ }).catch(() => void 0);
3391
+ }
3392
+ async function listLiveSharedSessionClients(persistentUserDataDir) {
3393
+ const clientsDirPath = buildSharedSessionClientsDirPath(persistentUserDataDir);
1904
3394
  let entries;
1905
3395
  try {
1906
- entries = await (0, import_promises.readdir)(rootDir, {
3396
+ entries = await (0, import_promises5.readdir)(clientsDirPath, {
1907
3397
  encoding: "utf8",
1908
3398
  withFileTypes: true
1909
3399
  });
1910
3400
  } catch {
1911
- return;
3401
+ return [];
1912
3402
  }
1913
- await Promise.all(
1914
- entries.map(async (entry) => {
1915
- if (!entry.isDirectory() || !entry.name.startsWith(ownedDirNamePrefix)) {
1916
- return;
1917
- }
1918
- if (await isOwnedDirByLiveProcess(entry.name, ownedDirNamePrefix)) {
1919
- return;
1920
- }
1921
- await (0, import_promises.rm)((0, import_node_path.join)(rootDir, entry.name), {
1922
- recursive: true,
1923
- force: true
1924
- }).catch(() => void 0);
1925
- })
1926
- );
1927
- }
1928
- async function isOwnedDirByLiveProcess(ownedDirName, ownedDirPrefix) {
1929
- const owner = parseOwnedDirOwner(ownedDirName, ownedDirPrefix);
1930
- return owner ? await getProcessLiveness(owner) !== "dead" : false;
3403
+ const liveClients = [];
3404
+ for (const entry of entries) {
3405
+ if (!entry.isFile()) {
3406
+ continue;
3407
+ }
3408
+ const filePath = (0, import_node_path5.join)(clientsDirPath, entry.name);
3409
+ const registration = await readSharedSessionClientRegistration(filePath);
3410
+ if (!registration) {
3411
+ await (0, import_promises5.rm)(filePath, { force: true }).catch(() => void 0);
3412
+ continue;
3413
+ }
3414
+ if (await getProcessLiveness(registration.owner) === "dead") {
3415
+ await (0, import_promises5.rm)(filePath, { force: true }).catch(() => void 0);
3416
+ continue;
3417
+ }
3418
+ liveClients.push(registration);
3419
+ }
3420
+ return liveClients;
1931
3421
  }
1932
- function parseOwnedDirOwner(ownedDirName, ownedDirPrefix) {
1933
- const remainder = ownedDirName.slice(ownedDirPrefix.length);
1934
- const firstDashIndex = remainder.indexOf("-");
1935
- const secondDashIndex = firstDashIndex === -1 ? -1 : remainder.indexOf("-", firstDashIndex + 1);
1936
- if (firstDashIndex === -1 || secondDashIndex === -1) {
3422
+ async function readSharedSessionClientRegistration(filePath) {
3423
+ try {
3424
+ const raw = await (0, import_promises5.readFile)(filePath, "utf8");
3425
+ return parseSharedSessionClientRegistration(JSON.parse(raw));
3426
+ } catch {
1937
3427
  return null;
1938
3428
  }
1939
- const pid = Number.parseInt(remainder.slice(0, firstDashIndex), 10);
1940
- const processStartedAtMs = Number.parseInt(
1941
- remainder.slice(firstDashIndex + 1, secondDashIndex),
1942
- 10
1943
- );
1944
- if (!Number.isInteger(pid) || pid <= 0) {
3429
+ }
3430
+ function buildSharedSessionClientRegistration() {
3431
+ return {
3432
+ clientId: (0, import_node_crypto4.randomUUID)(),
3433
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3434
+ owner: CURRENT_PROCESS_OWNER
3435
+ };
3436
+ }
3437
+ function parseSharedSessionClientRegistration(value) {
3438
+ if (!value || typeof value !== "object") {
1945
3439
  return null;
1946
3440
  }
1947
- if (!Number.isInteger(processStartedAtMs) || processStartedAtMs <= 0) {
3441
+ const parsed = value;
3442
+ const owner = parseProcessOwner(parsed.owner);
3443
+ if (!owner || typeof parsed.clientId !== "string" || typeof parsed.createdAt !== "string") {
1948
3444
  return null;
1949
3445
  }
1950
- return { pid, processStartedAtMs };
3446
+ return {
3447
+ clientId: parsed.clientId,
3448
+ createdAt: parsed.createdAt,
3449
+ owner
3450
+ };
1951
3451
  }
1952
- function isProcessRunning(pid) {
1953
- try {
1954
- process.kill(pid, 0);
1955
- return true;
1956
- } catch (error) {
1957
- const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
1958
- return code !== "ESRCH";
3452
+ function assertSharedSessionCompatibility(metadata, options) {
3453
+ if (metadata.executablePath !== options.executablePath) {
3454
+ throw new Error(
3455
+ `Chrome profile "${options.profileDirectory}" is already running with executable "${metadata.executablePath}", not "${options.executablePath}".`
3456
+ );
1959
3457
  }
1960
- }
1961
- async function readProcessStartedAtMs(pid) {
1962
- if (pid <= 0) {
1963
- return null;
3458
+ if (metadata.headless !== options.headless) {
3459
+ throw new Error(
3460
+ `Chrome profile "${options.profileDirectory}" is already running with headless=${metadata.headless}, not ${options.headless}.`
3461
+ );
1964
3462
  }
1965
- if (process.platform === "linux") {
1966
- return await readLinuxProcessStartedAtMs(pid);
3463
+ }
3464
+ async function prepareSharedSessionCloseIfIdle(persistentUserDataDir, clientId, sessionId) {
3465
+ return await withSharedSessionLock(persistentUserDataDir, async () => {
3466
+ const metadata = await readSharedSessionMetadata(persistentUserDataDir);
3467
+ await removeSharedSessionClientRegistration(
3468
+ persistentUserDataDir,
3469
+ clientId
3470
+ );
3471
+ if (!metadata || metadata.sessionId !== sessionId) {
3472
+ return {
3473
+ closeBrowser: false,
3474
+ sessionId
3475
+ };
3476
+ }
3477
+ const liveClients = await listLiveSharedSessionClients(
3478
+ persistentUserDataDir
3479
+ );
3480
+ if (liveClients.length > 0) {
3481
+ return {
3482
+ closeBrowser: false,
3483
+ sessionId: metadata.sessionId
3484
+ };
3485
+ }
3486
+ const closingMetadata = {
3487
+ ...metadata,
3488
+ state: "closing",
3489
+ stateOwner: CURRENT_PROCESS_OWNER
3490
+ };
3491
+ await writeSharedSessionMetadata(
3492
+ persistentUserDataDir,
3493
+ closingMetadata
3494
+ );
3495
+ return {
3496
+ browserOwner: closingMetadata.browserOwner,
3497
+ closeBrowser: true,
3498
+ sessionId: closingMetadata.sessionId
3499
+ };
3500
+ });
3501
+ }
3502
+ async function closeSharedSessionBrowser(persistentUserDataDir, closePlan, browser) {
3503
+ if (browser) {
3504
+ await requestBrowserShutdown(browser);
3505
+ await waitForProcessToExit(closePlan.browserOwner, 1e3);
1967
3506
  }
1968
- if (process.platform === "win32") {
1969
- return await readWindowsProcessStartedAtMs(pid);
3507
+ if (await getProcessLiveness(closePlan.browserOwner) !== "dead") {
3508
+ await killOwnedBrowserProcess(closePlan.browserOwner);
3509
+ await waitForProcessToExit(closePlan.browserOwner, 2e3);
1970
3510
  }
1971
- return await readPsProcessStartedAtMs(pid);
3511
+ await finalizeSharedSessionClose(persistentUserDataDir, closePlan.sessionId);
1972
3512
  }
1973
- async function readLinuxProcessStartedAtMs(pid) {
1974
- let statRaw;
1975
- try {
1976
- statRaw = await (0, import_promises.readFile)(`/proc/${pid}/stat`, "utf8");
1977
- } catch (error) {
1978
- return null;
1979
- }
1980
- const startTicks = parseLinuxProcessStartTicks(statRaw);
1981
- if (startTicks === null) {
1982
- return null;
3513
+ async function finalizeSharedSessionClose(persistentUserDataDir, sessionId) {
3514
+ await withSharedSessionLock(persistentUserDataDir, async () => {
3515
+ const metadata = await readSharedSessionMetadata(persistentUserDataDir);
3516
+ if (!metadata || metadata.sessionId !== sessionId) {
3517
+ return;
3518
+ }
3519
+ const liveClients = await listLiveSharedSessionClients(
3520
+ persistentUserDataDir
3521
+ );
3522
+ if (liveClients.length > 0) {
3523
+ const readyMetadata = {
3524
+ ...metadata,
3525
+ state: "ready"
3526
+ };
3527
+ await writeSharedSessionMetadata(
3528
+ persistentUserDataDir,
3529
+ readyMetadata
3530
+ );
3531
+ return;
3532
+ }
3533
+ if (await getProcessLiveness(metadata.browserOwner) !== "dead") {
3534
+ const readyMetadata = {
3535
+ ...metadata,
3536
+ state: "ready"
3537
+ };
3538
+ await writeSharedSessionMetadata(
3539
+ persistentUserDataDir,
3540
+ readyMetadata
3541
+ );
3542
+ return;
3543
+ }
3544
+ await (0, import_promises5.rm)(buildSharedSessionDirPath(persistentUserDataDir), {
3545
+ force: true,
3546
+ recursive: true
3547
+ }).catch(() => void 0);
3548
+ });
3549
+ }
3550
+ function getPrimaryBrowserContext(browser) {
3551
+ const contexts = browser.contexts();
3552
+ if (contexts.length === 0) {
3553
+ throw new Error(
3554
+ "Connection succeeded but no browser contexts were exposed."
3555
+ );
1983
3556
  }
1984
- const [bootTimeMs, clockTicksPerSecond] = await Promise.all([
1985
- readLinuxBootTimeMs(),
1986
- readLinuxClockTicksPerSecond()
1987
- ]);
1988
- if (bootTimeMs === null || clockTicksPerSecond === null) {
1989
- return null;
3557
+ return contexts[0];
3558
+ }
3559
+ async function getSharedSessionPage(context, reuseExistingPage) {
3560
+ if (reuseExistingPage) {
3561
+ return await getExistingPageOrCreate(context);
1990
3562
  }
1991
- return Math.floor(
1992
- bootTimeMs + startTicks * 1e3 / clockTicksPerSecond
1993
- );
3563
+ return await context.newPage();
1994
3564
  }
1995
- function parseLinuxProcessStartTicks(statRaw) {
1996
- const closingParenIndex = statRaw.lastIndexOf(")");
1997
- if (closingParenIndex === -1) {
1998
- return null;
3565
+ async function getExistingPageOrCreate(context) {
3566
+ const existingPage = context.pages()[0];
3567
+ if (existingPage) {
3568
+ return existingPage;
1999
3569
  }
2000
- const fields = statRaw.slice(closingParenIndex + 2).trim().split(/\s+/);
2001
- const startTicks = Number(fields[LINUX_STAT_START_TIME_FIELD_INDEX]);
2002
- return Number.isFinite(startTicks) && startTicks >= 0 ? startTicks : null;
3570
+ return await context.newPage();
2003
3571
  }
2004
- async function readLinuxBootTimeMs() {
2005
- try {
2006
- const statRaw = await (0, import_promises.readFile)("/proc/stat", "utf8");
2007
- const bootTimeLine = statRaw.split("\n").find((line) => line.startsWith("btime "));
2008
- if (!bootTimeLine) {
2009
- return null;
3572
+ function buildSharedSessionDiscoveryUrl(debugPort) {
3573
+ return `http://127.0.0.1:${debugPort}`;
3574
+ }
3575
+ async function resolveCdpWebSocketUrl(cdpUrl, timeoutMs) {
3576
+ if (cdpUrl.startsWith("ws://") || cdpUrl.startsWith("wss://")) {
3577
+ return cdpUrl;
3578
+ }
3579
+ const versionUrl = normalizeDiscoveryUrl(cdpUrl);
3580
+ const deadline = Date.now() + timeoutMs;
3581
+ let lastError = "CDP discovery did not respond.";
3582
+ while (Date.now() < deadline) {
3583
+ const remaining = Math.max(deadline - Date.now(), 1e3);
3584
+ try {
3585
+ const response = await fetch(versionUrl, {
3586
+ signal: AbortSignal.timeout(Math.min(remaining, 5e3))
3587
+ });
3588
+ if (!response.ok) {
3589
+ lastError = `${response.status} ${response.statusText}`;
3590
+ } else {
3591
+ const payload = await response.json();
3592
+ const wsUrl = payload && typeof payload === "object" && !Array.isArray(payload) && typeof payload.webSocketDebuggerUrl === "string" ? payload.webSocketDebuggerUrl : null;
3593
+ if (wsUrl && wsUrl.trim()) {
3594
+ return wsUrl;
3595
+ }
3596
+ lastError = "CDP discovery response did not include webSocketDebuggerUrl.";
3597
+ }
3598
+ } catch (error) {
3599
+ lastError = error instanceof Error ? error.message : "Unknown error";
2010
3600
  }
2011
- const bootTimeSeconds = Number.parseInt(
2012
- bootTimeLine.slice("btime ".length),
2013
- 10
2014
- );
2015
- return Number.isFinite(bootTimeSeconds) ? bootTimeSeconds * 1e3 : null;
2016
- } catch {
2017
- return null;
3601
+ await sleep5(100);
2018
3602
  }
2019
- }
2020
- async function readLinuxClockTicksPerSecond() {
2021
- linuxClockTicksPerSecondPromise ??= execFileAsync(
2022
- "getconf",
2023
- ["CLK_TCK"]
2024
- ).then(
2025
- ({ stdout }) => {
2026
- const value = Number.parseInt(stdout.trim(), 10);
2027
- return Number.isFinite(value) && value > 0 ? value : null;
2028
- },
2029
- () => null
3603
+ throw new Error(
3604
+ `Failed to resolve a CDP websocket URL from ${versionUrl.toString()}: ${lastError}`
2030
3605
  );
2031
- return await linuxClockTicksPerSecondPromise;
2032
3606
  }
2033
- async function readPsProcessStartedAtMs(pid) {
3607
+ function normalizeDiscoveryUrl(cdpUrl) {
3608
+ let parsed;
2034
3609
  try {
2035
- const { stdout } = await execFileAsync(
2036
- "ps",
2037
- ["-p", String(pid), "-o", "lstart="],
2038
- { env: PS_COMMAND_ENV }
3610
+ parsed = new URL(cdpUrl);
3611
+ } catch {
3612
+ throw new Error(
3613
+ `Invalid CDP URL "${cdpUrl}". Use an http(s) or ws(s) endpoint.`
2039
3614
  );
2040
- return parsePsStartedAtMs(stdout);
2041
- } catch (error) {
2042
- return null;
2043
3615
  }
2044
- }
2045
- function parsePsStartedAtMs(stdout) {
2046
- const raw = stdout.trim();
2047
- if (!raw) {
2048
- return null;
3616
+ if (parsed.protocol === "ws:" || parsed.protocol === "wss:") {
3617
+ return parsed;
2049
3618
  }
2050
- const startedAtMs = Date.parse(raw);
2051
- return Number.isNaN(startedAtMs) ? null : startedAtMs;
2052
- }
2053
- async function readWindowsProcessStartedAtMs(pid) {
2054
- const script = [
2055
- "$process = Get-Process -Id " + String(pid) + " -ErrorAction SilentlyContinue",
2056
- "if ($null -eq $process) { exit 3 }",
2057
- '$process.StartTime.ToUniversalTime().ToString("o")'
2058
- ].join("; ");
2059
- try {
2060
- const { stdout } = await execFileAsync(
2061
- "powershell.exe",
2062
- ["-NoLogo", "-NoProfile", "-Command", script]
3619
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
3620
+ throw new Error(
3621
+ `Unsupported CDP URL protocol "${parsed.protocol}". Use http(s) or ws(s).`
2063
3622
  );
2064
- return parsePsStartedAtMs(stdout);
2065
- } catch (error) {
2066
- return null;
2067
3623
  }
3624
+ const normalized = new URL(parsed.toString());
3625
+ normalized.pathname = "/json/version";
3626
+ normalized.search = "";
3627
+ normalized.hash = "";
3628
+ return normalized;
2068
3629
  }
2069
- function getErrorCode(error) {
2070
- return typeof error === "object" && error !== null && "code" in error && (typeof error.code === "string" || typeof error.code === "number") ? error.code : void 0;
3630
+ async function reserveDebugPort() {
3631
+ return await new Promise((resolve, reject) => {
3632
+ const server = (0, import_node_net.createServer)();
3633
+ server.unref();
3634
+ server.on("error", reject);
3635
+ server.listen(0, "127.0.0.1", () => {
3636
+ const address = server.address();
3637
+ if (!address || typeof address === "string") {
3638
+ server.close();
3639
+ reject(new Error("Failed to reserve a local debug port."));
3640
+ return;
3641
+ }
3642
+ server.close((error) => {
3643
+ if (error) {
3644
+ reject(error);
3645
+ return;
3646
+ }
3647
+ resolve(address.port);
3648
+ });
3649
+ });
3650
+ });
2071
3651
  }
2072
- async function sleep(ms) {
3652
+ async function sleep5(ms) {
2073
3653
  await new Promise((resolve) => setTimeout(resolve, ms));
2074
3654
  }
2075
3655
 
2076
3656
  // src/browser/pool.ts
2077
3657
  var BrowserPool = class {
2078
3658
  browser = null;
2079
- cdpProxy = null;
2080
- launchedProcess = null;
2081
- managedRuntimeProfile = null;
3659
+ activeSessionClose = null;
3660
+ closeInFlight = null;
2082
3661
  defaults;
2083
3662
  constructor(defaults = {}) {
2084
3663
  this.defaults = defaults;
2085
3664
  }
2086
3665
  async launch(options = {}) {
2087
- if (this.browser || this.cdpProxy || this.launchedProcess || this.managedRuntimeProfile) {
3666
+ if (this.browser || this.activeSessionClose) {
2088
3667
  await this.close();
2089
3668
  }
2090
3669
  const mode = options.mode ?? this.defaults.mode ?? "chromium";
@@ -2131,30 +3710,26 @@ var BrowserPool = class {
2131
3710
  return this.launchSandbox(options);
2132
3711
  }
2133
3712
  async close() {
2134
- const browser = this.browser;
2135
- const cdpProxy = this.cdpProxy;
2136
- const launchedProcess = this.launchedProcess;
2137
- const managedRuntimeProfile = this.managedRuntimeProfile;
2138
- this.browser = null;
2139
- this.cdpProxy = null;
2140
- this.launchedProcess = null;
3713
+ if (this.closeInFlight) {
3714
+ await this.closeInFlight;
3715
+ return;
3716
+ }
3717
+ const closeOperation = this.closeCurrent();
3718
+ this.closeInFlight = closeOperation;
2141
3719
  try {
2142
- if (browser) {
2143
- await browser.close().catch(() => void 0);
2144
- }
3720
+ await closeOperation;
3721
+ this.browser = null;
3722
+ this.activeSessionClose = null;
2145
3723
  } finally {
2146
- cdpProxy?.close();
2147
- await killProcessTree(launchedProcess);
2148
- if (managedRuntimeProfile) {
2149
- await persistIsolatedRuntimeProfile(
2150
- managedRuntimeProfile.userDataDir,
2151
- managedRuntimeProfile.persistentUserDataDir
2152
- );
2153
- if (this.managedRuntimeProfile === managedRuntimeProfile) {
2154
- this.managedRuntimeProfile = null;
2155
- }
2156
- }
3724
+ this.closeInFlight = null;
3725
+ }
3726
+ }
3727
+ async closeCurrent() {
3728
+ if (this.activeSessionClose) {
3729
+ await this.activeSessionClose();
3730
+ return;
2157
3731
  }
3732
+ await this.browser?.close().catch(() => void 0);
2158
3733
  }
2159
3734
  async connectToRunning(cdpUrl, timeout) {
2160
3735
  let browser = null;
@@ -2169,11 +3744,14 @@ var BrowserPool = class {
2169
3744
  }
2170
3745
  cdpProxy = new CDPProxy(browserWsUrl, targetId);
2171
3746
  const proxyWsUrl = await cdpProxy.start();
2172
- browser = await import_playwright.chromium.connectOverCDP(proxyWsUrl, {
3747
+ browser = await import_playwright2.chromium.connectOverCDP(proxyWsUrl, {
2173
3748
  timeout: timeout ?? 3e4
2174
3749
  });
2175
3750
  this.browser = browser;
2176
- this.cdpProxy = cdpProxy;
3751
+ this.activeSessionClose = async () => {
3752
+ await browser?.close().catch(() => void 0);
3753
+ cdpProxy?.close();
3754
+ };
2177
3755
  const { context, page } = await pickBrowserContextAndPage(browser);
2178
3756
  return { browser, context, page, isExternal: true };
2179
3757
  } catch (error) {
@@ -2182,7 +3760,7 @@ var BrowserPool = class {
2182
3760
  }
2183
3761
  cdpProxy?.close();
2184
3762
  this.browser = null;
2185
- this.cdpProxy = null;
3763
+ this.activeSessionClose = null;
2186
3764
  throw error;
2187
3765
  }
2188
3766
  }
@@ -2202,60 +3780,29 @@ var BrowserPool = class {
2202
3780
  sourceUserDataDir,
2203
3781
  profileDirectory
2204
3782
  );
2205
- const runtimeProfile = await createIsolatedRuntimeProfile(
2206
- persistentProfile.userDataDir
2207
- );
2208
- const debugPort = await reserveDebugPort();
2209
- const headless = resolveLaunchHeadless(
2210
- "real",
2211
- options.headless,
2212
- this.defaults.headless
2213
- );
2214
- const launchArgs = buildRealBrowserLaunchArgs({
2215
- userDataDir: runtimeProfile.userDataDir,
3783
+ const sharedSession = await acquireSharedRealBrowserSession({
3784
+ executablePath,
3785
+ headless: resolveLaunchHeadless(
3786
+ "real",
3787
+ options.headless,
3788
+ this.defaults.headless
3789
+ ),
3790
+ initialUrl: options.initialUrl,
3791
+ persistentProfile,
2216
3792
  profileDirectory,
2217
- debugPort,
2218
- headless
3793
+ timeoutMs: options.timeout ?? 3e4
2219
3794
  });
2220
- const processHandle = (0, import_node_child_process2.spawn)(executablePath, launchArgs, {
2221
- detached: process.platform !== "win32",
2222
- stdio: "ignore"
2223
- });
2224
- processHandle.unref();
2225
- let browser = null;
2226
- try {
2227
- const wsUrl = await resolveCdpWebSocketUrl(
2228
- `http://127.0.0.1:${debugPort}`,
2229
- options.timeout ?? 3e4
2230
- );
2231
- browser = await import_playwright.chromium.connectOverCDP(wsUrl, {
2232
- timeout: options.timeout ?? 3e4
2233
- });
2234
- const { context, page } = await createOwnedBrowserContextAndPage(
2235
- browser
2236
- );
2237
- if (options.initialUrl) {
2238
- await page.goto(options.initialUrl, {
2239
- waitUntil: "domcontentloaded",
2240
- timeout: options.timeout ?? 3e4
2241
- });
2242
- }
2243
- this.browser = browser;
2244
- this.launchedProcess = processHandle;
2245
- this.managedRuntimeProfile = runtimeProfile;
2246
- return { browser, context, page, isExternal: false };
2247
- } catch (error) {
2248
- await browser?.close().catch(() => void 0);
2249
- await killProcessTree(processHandle);
2250
- await (0, import_promises2.rm)(runtimeProfile.userDataDir, {
2251
- recursive: true,
2252
- force: true
2253
- }).catch(() => void 0);
2254
- throw error;
2255
- }
3795
+ this.browser = sharedSession.browser;
3796
+ this.activeSessionClose = sharedSession.close;
3797
+ return {
3798
+ browser: sharedSession.browser,
3799
+ context: sharedSession.context,
3800
+ page: sharedSession.page,
3801
+ isExternal: false
3802
+ };
2256
3803
  }
2257
3804
  async launchSandbox(options) {
2258
- const browser = await import_playwright.chromium.launch({
3805
+ const browser = await import_playwright2.chromium.launch({
2259
3806
  headless: resolveLaunchHeadless(
2260
3807
  "chromium",
2261
3808
  options.headless,
@@ -2268,11 +3815,14 @@ var BrowserPool = class {
2268
3815
  const context = await browser.newContext(options.context || {});
2269
3816
  const page = await context.newPage();
2270
3817
  this.browser = browser;
3818
+ this.activeSessionClose = async () => {
3819
+ await browser.close().catch(() => void 0);
3820
+ };
2271
3821
  return { browser, context, page, isExternal: false };
2272
3822
  }
2273
3823
  };
2274
3824
  async function pickBrowserContextAndPage(browser) {
2275
- const context = getPrimaryBrowserContext(browser);
3825
+ const context = getPrimaryBrowserContext2(browser);
2276
3826
  const page = await getAttachedPageOrCreate(context);
2277
3827
  return { context, page };
2278
3828
  }
@@ -2285,11 +3835,6 @@ function resolveLaunchHeadless(mode, requestedHeadless, defaultHeadless) {
2285
3835
  }
2286
3836
  return mode === "real";
2287
3837
  }
2288
- async function createOwnedBrowserContextAndPage(browser) {
2289
- const context = getPrimaryBrowserContext(browser);
2290
- const page = await getExistingPageOrCreate(context);
2291
- return { context, page };
2292
- }
2293
3838
  async function getAttachedPageOrCreate(context) {
2294
3839
  const pages = context.pages();
2295
3840
  const inspectablePage = pages.find(
@@ -2304,14 +3849,7 @@ async function getAttachedPageOrCreate(context) {
2304
3849
  }
2305
3850
  return await context.newPage();
2306
3851
  }
2307
- async function getExistingPageOrCreate(context) {
2308
- const existingPage = context.pages()[0];
2309
- if (existingPage) {
2310
- return existingPage;
2311
- }
2312
- return await context.newPage();
2313
- }
2314
- function getPrimaryBrowserContext(browser) {
3852
+ function getPrimaryBrowserContext2(browser) {
2315
3853
  const contexts = browser.contexts();
2316
3854
  if (contexts.length === 0) {
2317
3855
  throw new Error(
@@ -2330,125 +3868,6 @@ function safePageUrl(page) {
2330
3868
  return "";
2331
3869
  }
2332
3870
  }
2333
- function normalizeDiscoveryUrl(cdpUrl) {
2334
- let parsed;
2335
- try {
2336
- parsed = new URL(cdpUrl);
2337
- } catch {
2338
- throw new Error(
2339
- `Invalid CDP URL "${cdpUrl}". Use an http(s) or ws(s) endpoint.`
2340
- );
2341
- }
2342
- if (parsed.protocol === "ws:" || parsed.protocol === "wss:") {
2343
- return parsed;
2344
- }
2345
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2346
- throw new Error(
2347
- `Unsupported CDP URL protocol "${parsed.protocol}". Use http(s) or ws(s).`
2348
- );
2349
- }
2350
- const normalized = new URL(parsed.toString());
2351
- normalized.pathname = "/json/version";
2352
- normalized.search = "";
2353
- normalized.hash = "";
2354
- return normalized;
2355
- }
2356
- async function resolveCdpWebSocketUrl(cdpUrl, timeoutMs) {
2357
- if (cdpUrl.startsWith("ws://") || cdpUrl.startsWith("wss://")) {
2358
- return cdpUrl;
2359
- }
2360
- const versionUrl = normalizeDiscoveryUrl(cdpUrl);
2361
- const deadline = Date.now() + timeoutMs;
2362
- let lastError = "CDP discovery did not respond.";
2363
- while (Date.now() < deadline) {
2364
- const remaining = Math.max(deadline - Date.now(), 1e3);
2365
- try {
2366
- const response = await fetch(versionUrl, {
2367
- signal: AbortSignal.timeout(Math.min(remaining, 5e3))
2368
- });
2369
- if (!response.ok) {
2370
- lastError = `${response.status} ${response.statusText}`;
2371
- } else {
2372
- const payload = await response.json();
2373
- const wsUrl = payload && typeof payload === "object" && !Array.isArray(payload) && typeof payload.webSocketDebuggerUrl === "string" ? payload.webSocketDebuggerUrl : null;
2374
- if (wsUrl && wsUrl.trim()) {
2375
- return wsUrl;
2376
- }
2377
- lastError = "CDP discovery response did not include webSocketDebuggerUrl.";
2378
- }
2379
- } catch (error) {
2380
- lastError = error instanceof Error ? error.message : "Unknown error";
2381
- }
2382
- await sleep2(100);
2383
- }
2384
- throw new Error(
2385
- `Failed to resolve a CDP websocket URL from ${versionUrl.toString()}: ${lastError}`
2386
- );
2387
- }
2388
- async function reserveDebugPort() {
2389
- return await new Promise((resolve, reject) => {
2390
- const server = (0, import_node_net.createServer)();
2391
- server.unref();
2392
- server.on("error", reject);
2393
- server.listen(0, "127.0.0.1", () => {
2394
- const address = server.address();
2395
- if (!address || typeof address === "string") {
2396
- server.close();
2397
- reject(new Error("Failed to reserve a local debug port."));
2398
- return;
2399
- }
2400
- server.close((error) => {
2401
- if (error) {
2402
- reject(error);
2403
- return;
2404
- }
2405
- resolve(address.port);
2406
- });
2407
- });
2408
- });
2409
- }
2410
- function buildRealBrowserLaunchArgs(options) {
2411
- const args = [
2412
- `--user-data-dir=${options.userDataDir}`,
2413
- `--profile-directory=${options.profileDirectory}`,
2414
- `--remote-debugging-port=${options.debugPort}`,
2415
- "--disable-blink-features=AutomationControlled"
2416
- ];
2417
- if (options.headless) {
2418
- args.push("--headless=new");
2419
- }
2420
- return args;
2421
- }
2422
- async function killProcessTree(processHandle) {
2423
- if (!processHandle || processHandle.pid == null || processHandle.exitCode !== null) {
2424
- return;
2425
- }
2426
- if (process.platform === "win32") {
2427
- await new Promise((resolve) => {
2428
- const killer = (0, import_node_child_process2.spawn)(
2429
- "taskkill",
2430
- ["/pid", String(processHandle.pid), "/t", "/f"],
2431
- {
2432
- stdio: "ignore"
2433
- }
2434
- );
2435
- killer.on("error", () => resolve());
2436
- killer.on("exit", () => resolve());
2437
- });
2438
- return;
2439
- }
2440
- try {
2441
- process.kill(-processHandle.pid, "SIGKILL");
2442
- } catch {
2443
- try {
2444
- processHandle.kill("SIGKILL");
2445
- } catch {
2446
- }
2447
- }
2448
- }
2449
- async function sleep2(ms) {
2450
- await new Promise((resolve) => setTimeout(resolve, ms));
2451
- }
2452
3871
 
2453
3872
  // src/config.ts
2454
3873
  var import_fs2 = __toESM(require("fs"), 1);
@@ -3720,7 +5139,7 @@ var StealthCdpRuntime = class _StealthCdpRuntime {
3720
5139
  TRANSIENT_CONTEXT_RETRY_DELAY_MS,
3721
5140
  Math.max(0, deadline - Date.now())
3722
5141
  );
3723
- await sleep3(retryDelay);
5142
+ await sleep6(retryDelay);
3724
5143
  }
3725
5144
  }
3726
5145
  }
@@ -3753,7 +5172,7 @@ var StealthCdpRuntime = class _StealthCdpRuntime {
3753
5172
  () => ({ kind: "resolved" }),
3754
5173
  (error) => ({ kind: "rejected", error })
3755
5174
  );
3756
- const timeoutPromise = sleep3(
5175
+ const timeoutPromise = sleep6(
3757
5176
  timeout + FRAME_EVALUATE_GRACE_MS
3758
5177
  ).then(() => ({ kind: "timeout" }));
3759
5178
  const result = await Promise.race([
@@ -3895,7 +5314,7 @@ function isIgnorableFrameError(error) {
3895
5314
  const message = error.message;
3896
5315
  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");
3897
5316
  }
3898
- function sleep3(ms) {
5317
+ function sleep6(ms) {
3899
5318
  return new Promise((resolve) => {
3900
5319
  setTimeout(resolve, ms);
3901
5320
  });
@@ -8156,7 +9575,7 @@ async function closeTab(context, activePage, index) {
8156
9575
  }
8157
9576
 
8158
9577
  // src/actions/cookies.ts
8159
- var import_promises3 = require("fs/promises");
9578
+ var import_promises6 = require("fs/promises");
8160
9579
  async function getCookies(context, url) {
8161
9580
  return context.cookies(url ? [url] : void 0);
8162
9581
  }
@@ -8168,10 +9587,10 @@ async function clearCookies(context) {
8168
9587
  }
8169
9588
  async function exportCookies(context, filePath, url) {
8170
9589
  const cookies = await context.cookies(url ? [url] : void 0);
8171
- await (0, import_promises3.writeFile)(filePath, JSON.stringify(cookies, null, 2), "utf-8");
9590
+ await (0, import_promises6.writeFile)(filePath, JSON.stringify(cookies, null, 2), "utf-8");
8172
9591
  }
8173
9592
  async function importCookies(context, filePath) {
8174
- const raw = await (0, import_promises3.readFile)(filePath, "utf-8");
9593
+ const raw = await (0, import_promises6.readFile)(filePath, "utf-8");
8175
9594
  const cookies = JSON.parse(raw);
8176
9595
  await context.addCookies(cookies);
8177
9596
  }
@@ -8482,7 +9901,7 @@ var AdaptiveNetworkTracker = class {
8482
9901
  this.idleSince = 0;
8483
9902
  }
8484
9903
  const remaining = Math.max(1, options.deadline - now);
8485
- await sleep4(Math.min(NETWORK_POLL_MS, remaining));
9904
+ await sleep7(Math.min(NETWORK_POLL_MS, remaining));
8486
9905
  }
8487
9906
  }
8488
9907
  handleRequestStarted = (request) => {
@@ -8527,7 +9946,7 @@ var AdaptiveNetworkTracker = class {
8527
9946
  return false;
8528
9947
  }
8529
9948
  };
8530
- async function sleep4(ms) {
9949
+ async function sleep7(ms) {
8531
9950
  await new Promise((resolve) => {
8532
9951
  setTimeout(resolve, ms);
8533
9952
  });
@@ -10232,13 +11651,13 @@ function dedupeNewest(entries) {
10232
11651
  }
10233
11652
 
10234
11653
  // src/cloud/cdp-client.ts
10235
- var import_playwright2 = require("playwright");
11654
+ var import_playwright3 = require("playwright");
10236
11655
  var CloudCdpClient = class {
10237
11656
  async connect(args) {
10238
11657
  const endpoint = withTokenQuery(args.wsUrl, args.token);
10239
11658
  let browser;
10240
11659
  try {
10241
- browser = await import_playwright2.chromium.connectOverCDP(endpoint);
11660
+ browser = await import_playwright3.chromium.connectOverCDP(endpoint);
10242
11661
  } catch (error) {
10243
11662
  const message = error instanceof Error ? error.message : "Failed to connect to cloud CDP endpoint.";
10244
11663
  throw new OpensteerCloudError("CLOUD_TRANSPORT_ERROR", message);
@@ -12055,7 +13474,7 @@ async function executeAgentAction(page, action) {
12055
13474
  }
12056
13475
  case "wait": {
12057
13476
  const ms = numberOr(action.timeMs, action.time_ms, 1e3);
12058
- await sleep5(ms);
13477
+ await sleep8(ms);
12059
13478
  return;
12060
13479
  }
12061
13480
  case "goto": {
@@ -12220,7 +13639,7 @@ async function pressKeyCombo(page, combo) {
12220
13639
  }
12221
13640
  }
12222
13641
  }
12223
- function sleep5(ms) {
13642
+ function sleep8(ms) {
12224
13643
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
12225
13644
  }
12226
13645
 
@@ -12251,7 +13670,7 @@ var OpensteerCuaAgentHandler = class {
12251
13670
  if (isMutatingAgentAction(action)) {
12252
13671
  this.onMutatingAction?.(action);
12253
13672
  }
12254
- await sleep6(this.config.waitBetweenActionsMs);
13673
+ await sleep9(this.config.waitBetweenActionsMs);
12255
13674
  });
12256
13675
  try {
12257
13676
  const result = await this.client.execute({
@@ -12313,7 +13732,7 @@ var OpensteerCuaAgentHandler = class {
12313
13732
  await this.cursorController.preview({ x, y }, "agent");
12314
13733
  }
12315
13734
  };
12316
- function sleep6(ms) {
13735
+ function sleep9(ms) {
12317
13736
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
12318
13737
  }
12319
13738
 
@@ -12749,7 +14168,7 @@ var CursorController = class {
12749
14168
  for (const step of motion.points) {
12750
14169
  await this.renderer.move(step, this.style);
12751
14170
  if (motion.stepDelayMs > 0) {
12752
- await sleep7(motion.stepDelayMs);
14171
+ await sleep10(motion.stepDelayMs);
12753
14172
  }
12754
14173
  }
12755
14174
  if (shouldPulse(intent)) {
@@ -12907,7 +14326,7 @@ function clamp2(value, min, max) {
12907
14326
  function shouldPulse(intent) {
12908
14327
  return intent === "click" || intent === "dblclick" || intent === "rightclick" || intent === "agent";
12909
14328
  }
12910
- function sleep7(ms) {
14329
+ function sleep10(ms) {
12911
14330
  return new Promise((resolve) => setTimeout(resolve, ms));
12912
14331
  }
12913
14332
 
@@ -13381,15 +14800,19 @@ var Opensteer = class _Opensteer {
13381
14800
  }
13382
14801
  return;
13383
14802
  }
14803
+ let closedOwnedBrowser = false;
13384
14804
  try {
13385
14805
  if (this.ownsBrowser) {
13386
14806
  await this.pool.close();
14807
+ closedOwnedBrowser = true;
13387
14808
  }
13388
14809
  } finally {
13389
14810
  this.browser = null;
13390
14811
  this.pageRef = null;
13391
14812
  this.contextRef = null;
13392
- this.ownsBrowser = false;
14813
+ if (!this.ownsBrowser || closedOwnedBrowser) {
14814
+ this.ownsBrowser = false;
14815
+ }
13393
14816
  if (this.cursorController) {
13394
14817
  await this.cursorController.dispose().catch(() => void 0);
13395
14818
  }
@@ -15846,7 +17269,7 @@ var CdpOverlayCursorRenderer = class {
15846
17269
  outlineColor: toProtocolRgba(pulseOutline)
15847
17270
  });
15848
17271
  });
15849
- await sleep8(PULSE_DELAY_MS);
17272
+ await sleep11(PULSE_DELAY_MS);
15850
17273
  await this.move(point, style);
15851
17274
  }
15852
17275
  async clear() {
@@ -15970,7 +17393,7 @@ function clampAlpha(value) {
15970
17393
  function roundPointValue(value) {
15971
17394
  return Math.round(value * 100) / 100;
15972
17395
  }
15973
- function sleep8(ms) {
17396
+ function sleep11(ms) {
15974
17397
  return new Promise((resolve) => setTimeout(resolve, ms));
15975
17398
  }
15976
17399
  // Annotate the CommonJS export names for ESM import in node:
@@ -16046,8 +17469,10 @@ function sleep8(ms) {
16046
17469
  getElementValue,
16047
17470
  getModelProvider,
16048
17471
  getOrCreatePersistentProfile,
17472
+ getOwnedRealBrowserProcessPolicy,
16049
17473
  getPageHtml,
16050
17474
  getPageTitle,
17475
+ hasActiveRuntimeProfileCreations,
16051
17476
  importCookies,
16052
17477
  isCloudActionMethod,
16053
17478
  isCloudErrorCode,