@tinycloud/operations 0.5.0-beta.5 → 0.5.0-beta.7

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.
@@ -3654,10 +3654,10 @@ function evaluateOperationAuthority(granted, required, resolveSpace) {
3654
3654
  const canonicalRequired = canonicalizeOperationCapabilities(required, resolveSpace);
3655
3655
  const missing = [];
3656
3656
  for (const requested of canonicalRequired) {
3657
- const covered = canonicalGranted.some(
3658
- (candidate) => candidate.service === requested.service && operationSpacesEqual(candidate.space, requested.space) && isCapabilitySubset([requested], [candidate]).subset
3657
+ const sameScope = canonicalGranted.filter(
3658
+ (candidate) => candidate.service === requested.service && operationSpacesEqual(candidate.space, requested.space)
3659
3659
  );
3660
- if (!covered) missing.push(requested);
3660
+ if (!isCapabilitySubset([requested], sameScope).subset) missing.push(requested);
3661
3661
  }
3662
3662
  return {
3663
3663
  satisfied: missing.length === 0,
@@ -3734,7 +3734,9 @@ function canonicalizeCaveats(caveats) {
3734
3734
  // src/state.ts
3735
3735
  var import_node_crypto = require("crypto");
3736
3736
  var import_node_async_hooks = require("async_hooks");
3737
+ var import_node_fs = require("fs");
3737
3738
  var import_promises = require("fs/promises");
3739
+ var import_node_net = require("net");
3738
3740
  var import_node_os = require("os");
3739
3741
  var import_node_path = require("path");
3740
3742
  var DEFAULT_LOCK_TIMEOUT_MS = 2e3;
@@ -3754,13 +3756,171 @@ var TEST_LOCK_MOVED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_MOVED_BARRIER_DIR";
3754
3756
  var TEST_LOCK_TURN_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_PUBLISH_BARRIER_DIR";
3755
3757
  var TEST_LOCK_TURN_SETTLE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_SETTLE_BARRIER_DIR";
3756
3758
  var TEST_LOCK_TURN_COLLECT_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_COLLECT_BARRIER_DIR";
3759
+ var TEST_LOCK_LIVENESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_LIVENESS_BARRIER_DIR";
3757
3760
  function processWideContext(key) {
3758
- var _a3;
3761
+ var _a4;
3759
3762
  const registry = globalThis;
3760
- return registry[_a3 = Symbol.for(key)] ?? (registry[_a3] = new import_node_async_hooks.AsyncLocalStorage());
3763
+ return registry[_a4 = Symbol.for(key)] ?? (registry[_a4] = new import_node_async_hooks.AsyncLocalStorage());
3761
3764
  }
3762
3765
  var invocationStateRoot = processWideContext("tinycloud.operations.invocationStateRoot.v1");
3763
3766
  var heldProfileLocks = processWideContext("tinycloud.operations.heldProfileLocks.v2");
3767
+ var _a2;
3768
+ var processInstanceId = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a2] = (0, import_node_crypto.randomUUID)());
3769
+ var cachedProcessOrigin;
3770
+ function processOrigin() {
3771
+ if (cachedProcessOrigin !== void 0) return cachedProcessOrigin;
3772
+ if (process.platform !== "linux") return cachedProcessOrigin = null;
3773
+ try {
3774
+ const bootId = (0, import_node_fs.readFileSync)("/proc/sys/kernel/random/boot_id", "utf8").trim();
3775
+ const pidNamespace = (0, import_node_fs.readlinkSync)("/proc/self/ns/pid");
3776
+ const processStat = (0, import_node_fs.readFileSync)("/proc/self/stat", "utf8");
3777
+ const closingParen = processStat.lastIndexOf(") ");
3778
+ const processStartTime = closingParen < 0 ? void 0 : processStat.slice(closingParen + 2).trim().split(/\s+/)[19];
3779
+ if (!/^[0-9a-f-]{36}$/.test(bootId) || !/^pid:\[[0-9]+\]$/.test(pidNamespace) || !processStartTime || !/^[0-9]+$/.test(processStartTime)) {
3780
+ return cachedProcessOrigin = null;
3781
+ }
3782
+ return cachedProcessOrigin = { bootId, pidNamespace, processStartTime };
3783
+ } catch {
3784
+ return cachedProcessOrigin = null;
3785
+ }
3786
+ }
3787
+ function processOwner(liveness) {
3788
+ return { instanceId: processInstanceId, ...processOrigin(), ...liveness ? { liveness: liveness.record } : {} };
3789
+ }
3790
+ var LIVENESS_SOCKET = /^\.live-([0-9a-f]+)-([0-9a-f]+)-[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}(\.new)?$/;
3791
+ var LIVENESS_PROBE_TIMEOUT_MS = 1e3;
3792
+ function socketDirectory(name) {
3793
+ const match = LIVENESS_SOCKET.exec(name);
3794
+ return match ? `${match[1]}-${match[2]}` : null;
3795
+ }
3796
+ async function pinDirectory(directory) {
3797
+ const handle = await (0, import_promises.open)(directory, "r");
3798
+ try {
3799
+ const { dev, ino } = await handle.stat({ bigint: true });
3800
+ return {
3801
+ identity: `${dev.toString(16)}-${ino.toString(16)}`,
3802
+ path: (name) => `/proc/self/fd/${handle.fd}/${name}`,
3803
+ close: () => handle.close()
3804
+ };
3805
+ } catch (error) {
3806
+ await handle.close();
3807
+ throw error;
3808
+ }
3809
+ }
3810
+ async function bindLiveness(directory) {
3811
+ if (!processOrigin()) return null;
3812
+ let pinned;
3813
+ try {
3814
+ pinned = await pinDirectory(directory);
3815
+ } catch {
3816
+ return null;
3817
+ }
3818
+ const socket = `.live-${pinned.identity}-${(0, import_node_crypto.randomUUID)()}`;
3819
+ const server = (0, import_node_net.createServer)((connection) => connection.destroy());
3820
+ let bound = false;
3821
+ try {
3822
+ await new Promise((resolve2, reject) => {
3823
+ server.once("error", reject);
3824
+ server.listen({ path: pinned.path(`${socket}.new`), exclusive: true }, () => {
3825
+ server.off("error", reject);
3826
+ resolve2();
3827
+ });
3828
+ });
3829
+ bound = true;
3830
+ server.on("error", () => void 0);
3831
+ server.unref();
3832
+ await (0, import_promises.rename)(pinned.path(`${socket}.new`), pinned.path(socket));
3833
+ const stats = await (0, import_promises.lstat)(pinned.path(socket), { bigint: true });
3834
+ if (!stats.isSocket()) throw new Error(`${socket} is not a socket`);
3835
+ await waitForTestBarrier(TEST_LOCK_LIVENESS_BARRIER_DIR, (0, import_node_path.basename)(directory));
3836
+ return { record: { socket, dev: String(stats.dev), ino: String(stats.ino) }, path: (0, import_node_path.join)(directory, socket), server };
3837
+ } catch {
3838
+ if (bound) {
3839
+ server.close();
3840
+ for (const name of [`${socket}.new`, socket]) await (0, import_promises.unlink)(pinned.path(name)).catch(() => void 0);
3841
+ }
3842
+ return null;
3843
+ } finally {
3844
+ await pinned.close();
3845
+ }
3846
+ }
3847
+ async function ensureLiveness(acquisition) {
3848
+ if (acquisition.held === void 0) acquisition.held = await bindLiveness(acquisition.directory);
3849
+ return acquisition.held;
3850
+ }
3851
+ async function closeLiveness(held) {
3852
+ held.server.close();
3853
+ await (0, import_promises.unlink)(held.path).catch(() => void 0);
3854
+ }
3855
+ function parseLiveness(value) {
3856
+ if (typeof value !== "object" || value === null) return null;
3857
+ const { socket, dev, ino } = value;
3858
+ const name = typeof socket === "string" ? LIVENESS_SOCKET.exec(socket) : null;
3859
+ return name !== null && name[3] === void 0 && typeof dev === "string" && /^[0-9]+$/.test(dev) && typeof ino === "string" && /^[0-9]+$/.test(ino) ? { socket: name[0], dev, ino } : null;
3860
+ }
3861
+ async function controlIn(pinned, acquisition) {
3862
+ const own = await ensureLiveness(acquisition);
3863
+ if (!own || socketDirectory(own.record.socket) !== pinned.identity) return null;
3864
+ const stats = await (0, import_promises.lstat)(pinned.path(own.record.socket), { bigint: true }).catch(() => null);
3865
+ return stats?.isSocket() && String(stats.dev) === own.record.dev && String(stats.ino) === own.record.ino ? own : null;
3866
+ }
3867
+ async function tryConnect(path) {
3868
+ return new Promise((resolve2) => {
3869
+ const probe = (0, import_node_net.connect)({ path });
3870
+ const settle = (outcome) => {
3871
+ clearTimeout(timer);
3872
+ probe.destroy();
3873
+ resolve2(outcome);
3874
+ };
3875
+ const timer = setTimeout(() => settle("other"), LIVENESS_PROBE_TIMEOUT_MS);
3876
+ probe.once("connect", () => settle("connected"));
3877
+ probe.once("error", (error) => settle(isErrno(error, "ECONNREFUSED") ? "refused" : "other"));
3878
+ });
3879
+ }
3880
+ async function kernelRefuses(pinned, name, control) {
3881
+ if (process.versions.bun !== void 0) return false;
3882
+ return await tryConnect(pinned.path(name)) === "refused" && await tryConnect(pinned.path(control.record.socket)) === "connected";
3883
+ }
3884
+ async function probeLiveness(liveness, acquisition) {
3885
+ let pinned;
3886
+ try {
3887
+ pinned = await pinDirectory(acquisition.directory);
3888
+ } catch {
3889
+ return "unknown";
3890
+ }
3891
+ try {
3892
+ if (socketDirectory(liveness.socket) !== pinned.identity) return "unknown";
3893
+ const control = await controlIn(pinned, acquisition);
3894
+ if (!control) return "unknown";
3895
+ let stats;
3896
+ try {
3897
+ stats = await (0, import_promises.lstat)(pinned.path(liveness.socket), { bigint: true });
3898
+ } catch (error) {
3899
+ return isErrno(error, "ENOENT") ? "gone" : "unknown";
3900
+ }
3901
+ if (!stats.isSocket() || String(stats.dev) !== liveness.dev || String(stats.ino) !== liveness.ino) return "unknown";
3902
+ return await kernelRefuses(pinned, liveness.socket, control) ? "refused" : "unknown";
3903
+ } finally {
3904
+ await pinned.close();
3905
+ }
3906
+ }
3907
+ async function sweepLiveness(acquisition) {
3908
+ if (process.versions.bun !== void 0 || !acquisition.held) return;
3909
+ const pinned = await pinDirectory(acquisition.directory);
3910
+ try {
3911
+ const control = await controlIn(pinned, acquisition);
3912
+ if (!control) return;
3913
+ for (const name of await (0, import_promises.readdir)(pinned.path("."))) {
3914
+ if (name === control.record.socket || socketDirectory(name) !== pinned.identity) continue;
3915
+ const stats = await (0, import_promises.lstat)(pinned.path(name)).catch(() => null);
3916
+ if (stats?.isSocket() && await kernelRefuses(pinned, name, control)) {
3917
+ await (0, import_promises.unlink)(pinned.path(name)).catch(() => void 0);
3918
+ }
3919
+ }
3920
+ } finally {
3921
+ await pinned.close();
3922
+ }
3923
+ }
3764
3924
  var ProfileLockTimeoutError = class extends Error {
3765
3925
  /** `detail` names the cause when it is not simply another process holding the lock. */
3766
3926
  constructor(profile, timeoutMs, detail) {
@@ -3932,7 +4092,18 @@ async function acquireProfileLock(profile, options) {
3932
4092
  if (path !== directory || !isErrno(error, "ENOENT")) throw error;
3933
4093
  });
3934
4094
  }
3935
- const { turn, firstWaitedTurn } = await acquireTurn(profile, turnDirectory, deadline);
4095
+ const liveness = { directory: turnDirectory, held: void 0 };
4096
+ const closeAcquisitionLiveness = async () => {
4097
+ if (liveness.held) await closeLiveness(liveness.held);
4098
+ };
4099
+ let taken;
4100
+ try {
4101
+ taken = await acquireTurn(profile, turnDirectory, deadline, liveness);
4102
+ } catch (error) {
4103
+ await closeAcquisitionLiveness();
4104
+ throw error;
4105
+ }
4106
+ const { turn, firstWaitedTurn } = taken;
3936
4107
  try {
3937
4108
  if (options.requireProfile && !await exists(profileConfigPath(profile))) {
3938
4109
  await (0, import_promises.rmdir)(directory).catch(() => void 0);
@@ -3942,7 +4113,7 @@ async function acquireProfileLock(profile, options) {
3942
4113
  const deletedWhileWaiting = typeof deletion?.slot === "number" && deletion.slot >= firstWaitedTurn;
3943
4114
  while (true) {
3944
4115
  const token = (0, import_node_crypto.randomUUID)();
3945
- if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true)) {
4116
+ if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true, liveness.held ?? null)) {
3946
4117
  return {
3947
4118
  deletedWhileWaiting,
3948
4119
  turnSlot: turn.slot,
@@ -3950,17 +4121,19 @@ async function acquireProfileLock(profile, options) {
3950
4121
  try {
3951
4122
  await releaseProfileLock(profile, lockPath, token);
3952
4123
  } finally {
3953
- await releaseTurn(profile, turn);
4124
+ await releaseTurn(profile, turn, liveness);
4125
+ await closeAcquisitionLiveness();
3954
4126
  }
3955
4127
  }
3956
4128
  };
3957
4129
  }
3958
4130
  await signalTestLockContention(profile);
3959
- if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
4131
+ if (await recoverStaleLock(profile, lockPath, liveness, staleAfterMs)) continue;
3960
4132
  await waitOrTimeOut(profile, deadline);
3961
4133
  }
3962
4134
  } catch (error) {
3963
- await releaseTurn(profile, turn);
4135
+ await releaseTurn(profile, turn, liveness);
4136
+ await closeAcquisitionLiveness();
3964
4137
  throw error;
3965
4138
  }
3966
4139
  }
@@ -3974,12 +4147,12 @@ async function waitOrTimeOut(profile, deadline, detail) {
3974
4147
  await sleep(Math.min(deadline.retryMs, deadline.timeoutMs - (Date.now() - deadline.startedAt)));
3975
4148
  }
3976
4149
  var TURN = /^(?:0|[1-9][0-9]*)$/;
3977
- var _a2;
3978
- var relinquishedTurns = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a2] = /* @__PURE__ */ new Set());
4150
+ var _a3;
4151
+ var relinquishedTurns = globalThis[_a3 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a3] = /* @__PURE__ */ new Set());
3979
4152
  var MARKER_RETRY_DELAYS_MS = [10, 50, 250];
3980
4153
  var SETTLE_RETRY_MS = 1e3;
3981
4154
  var SETTLE_RETRIES = 300;
3982
- async function acquireTurn(profile, directory, deadline) {
4155
+ async function acquireTurn(profile, directory, deadline, liveness) {
3983
4156
  let firstWaitedTurn;
3984
4157
  let problem;
3985
4158
  const damaged = (what) => `The turn lock ${directory} ${what}, so no process can take it. If no tc or MCP process is using profile "${profile}", remove that directory.`;
@@ -4004,13 +4177,13 @@ async function acquireTurn(profile, directory, deadline) {
4004
4177
  }
4005
4178
  firstWaitedTurn ?? (firstWaitedTurn = latest.done ? latest.slot + 1 : latest.slot);
4006
4179
  if (latest.held && latest.done) {
4007
- const turn = await takeTurn(profile, directory, latest);
4180
+ const turn = await takeTurn(profile, directory, latest, await ensureLiveness(liveness));
4008
4181
  if (turn !== null) return { turn, firstWaitedTurn };
4009
4182
  await waitOrTimeOut(profile, deadline);
4010
4183
  } else if (latest.done) {
4011
4184
  problem = damaged(`ends with turn ${latest.slot}, which was never granted`);
4012
4185
  await waitOrTimeOut(profile, deadline, problem);
4013
- } else if (latest.pid === process.pid ? relinquishedTurns.has(latest.token) : !isProcessAlive(latest.pid)) {
4186
+ } else if (latest.pid === process.pid && relinquishedTurns.has(latest.token) || !await isRecordedProcessAlive(latest, liveness)) {
4014
4187
  await waitForTestBarrier(TEST_LOCK_TURN_SETTLE_BARRIER_DIR, profile);
4015
4188
  await settleAbandonedTurn(directory, latest);
4016
4189
  relinquishedTurns.delete(latest.token);
@@ -4055,6 +4228,7 @@ async function readTurn(directory, slot) {
4055
4228
  const done = await exists((0, import_node_path.join)(path, `${owner.token}.done`));
4056
4229
  const held = await exists((0, import_node_path.join)(path, `${owner.token}.held`));
4057
4230
  return {
4231
+ ...owner,
4058
4232
  slot,
4059
4233
  token: owner.token,
4060
4234
  pid: owner.pid,
@@ -4068,11 +4242,11 @@ async function isFinishedTurn(directory, slot, token) {
4068
4242
  if (turn === "damaged") throw new Error(`Turn ${slot} of the turn lock ${directory} is unreadable.`);
4069
4243
  return turn !== null && turn.token === token && turn.held && turn.done;
4070
4244
  }
4071
- async function takeTurn(profile, directory, latest) {
4245
+ async function takeTurn(profile, directory, latest, liveness) {
4072
4246
  const token = (0, import_node_crypto.randomUUID)();
4073
4247
  const slot = latest.slot + 1;
4074
4248
  const path = (0, import_node_path.join)(directory, String(slot));
4075
- const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: latest.token };
4249
+ const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: latest.token, ...processOwner(liveness) };
4076
4250
  let published = false;
4077
4251
  try {
4078
4252
  published = await publishDirectory(
@@ -4126,7 +4300,7 @@ async function markTurn(path, token, kind) {
4126
4300
  }
4127
4301
  async function createTurnDirectory(directory) {
4128
4302
  const token = (0, import_node_crypto.randomUUID)();
4129
- const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: null };
4303
+ const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: null, ...processOwner(null) };
4130
4304
  await publishDirectory((0, import_node_path.join)((0, import_node_path.dirname)(directory), `.stage-${(0, import_node_path.basename)(directory)}-${process.pid}-${token}`), directory, {
4131
4305
  "0/owner.json": `${JSON.stringify(owner, null, 2)}
4132
4306
  `,
@@ -4150,31 +4324,39 @@ async function publishDirectory(stage, target, files, beforeRename) {
4150
4324
  await (0, import_promises.rm)(stage, { recursive: true, force: true }).catch(() => void 0);
4151
4325
  }
4152
4326
  }
4153
- async function releaseTurn(profile, turn) {
4327
+ async function releaseTurn(profile, turn, liveness) {
4154
4328
  try {
4155
4329
  await markTurn((0, import_node_path.join)(turn.directory, String(turn.slot)), turn.token, "done");
4156
4330
  } catch {
4157
4331
  settleOwnTurn(turn.directory, turn.slot, turn.token);
4158
4332
  return;
4159
4333
  }
4160
- await collectTurns(profile, turn).catch(() => void 0);
4334
+ await collectTurns(profile, turn, liveness).catch(() => void 0);
4161
4335
  }
4162
- function isAbandonedTurnEntry(name) {
4336
+ async function isAbandonedTurnEntry(directory, name, liveness) {
4163
4337
  if (name.startsWith(".trash-")) return true;
4164
4338
  const writer = /^\.(?:stage|deleted\.json)[-.]([0-9]+)[-.]/.exec(name)?.[1];
4165
- return writer !== void 0 && !isProcessAlive(Number(writer));
4339
+ if (writer === void 0) return false;
4340
+ if (Number(writer) !== process.pid) return !await isRecordedProcessAlive({ pid: Number(writer) }, liveness);
4341
+ return name.startsWith(".stage-") && await isStagedOwnerGone((0, import_node_path.join)(directory, name, "owner.json"), liveness);
4166
4342
  }
4167
- function isAbandonedTurnDirectoryStage(name) {
4343
+ async function isAbandonedTurnDirectoryStage(directory, name, liveness) {
4168
4344
  const writer = /^\.stage-.+-([0-9]+)-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.exec(name)?.[1];
4169
- return writer !== void 0 && !isProcessAlive(Number(writer));
4345
+ if (writer === void 0) return false;
4346
+ if (Number(writer) !== process.pid) return !await isRecordedProcessAlive({ pid: Number(writer) }, liveness);
4347
+ return isStagedOwnerGone((0, import_node_path.join)(directory, name, "0", "owner.json"), liveness);
4170
4348
  }
4171
- async function collectTurns(profile, turn) {
4349
+ async function isStagedOwnerGone(recordPath, liveness) {
4350
+ const owner = await readJson(recordPath).catch(() => null);
4351
+ return typeof owner === "object" && owner !== null && owner.pid === process.pid && !await isRecordedProcessAlive(owner, liveness);
4352
+ }
4353
+ async function collectTurns(profile, turn, liveness) {
4172
4354
  for (const [directory, abandoned] of [
4173
4355
  [turn.directory, isAbandonedTurnEntry],
4174
4356
  [(0, import_node_path.dirname)(turn.directory), isAbandonedTurnDirectoryStage]
4175
4357
  ]) {
4176
- for (const name of (await (0, import_promises.readdir)(directory)).filter(abandoned)) {
4177
- await (0, import_promises.rm)((0, import_node_path.join)(directory, name), { recursive: true, force: true }).catch(() => void 0);
4358
+ for (const name of await (0, import_promises.readdir)(directory)) {
4359
+ if (await abandoned(directory, name, liveness)) await (0, import_promises.rm)((0, import_node_path.join)(directory, name), { recursive: true, force: true }).catch(() => void 0);
4178
4360
  }
4179
4361
  }
4180
4362
  const older = (await (0, import_promises.readdir)(turn.directory)).filter((name) => TURN.test(name)).map(Number).filter((slot) => slot < turn.slot).sort((left, right) => left - right);
@@ -4192,8 +4374,9 @@ async function collectTurns(profile, turn) {
4192
4374
  await (0, import_promises.rm)(trash, { recursive: true, force: true }).catch(() => void 0);
4193
4375
  }
4194
4376
  }
4377
+ await sweepLiveness(liveness);
4195
4378
  }
4196
- async function publishProfileLock(profile, lockPath, token, requireProfile) {
4379
+ async function publishProfileLock(profile, lockPath, token, requireProfile, liveness) {
4197
4380
  try {
4198
4381
  await (0, import_promises.mkdir)(lockPath, { mode: PRIVATE_DIR_MODE });
4199
4382
  } catch (error) {
@@ -4206,7 +4389,7 @@ async function publishProfileLock(profile, lockPath, token, requireProfile) {
4206
4389
  await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);
4207
4390
  const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
4208
4391
  const staged = (0, import_node_path.join)((0, import_node_path.dirname)(lockPath), `.lock-owner-${token}.tmp`);
4209
- const owner = `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token }, null, 2)}
4392
+ const owner = `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, ...processOwner(liveness) }, null, 2)}
4210
4393
  `;
4211
4394
  try {
4212
4395
  await (0, import_promises.writeFile)(staged, owner, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
@@ -4251,7 +4434,7 @@ async function signalTestLockContention(profile) {
4251
4434
  await (0, import_promises.writeFile)(signalPath, `${profile}
4252
4435
  `, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
4253
4436
  }
4254
- async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
4437
+ async function recoverOwnerlessLock(profile, lockPath, liveness, staleAfterMs) {
4255
4438
  let aged;
4256
4439
  try {
4257
4440
  const { mtimeMs } = await (0, import_promises.stat)(lockPath);
@@ -4268,7 +4451,7 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
4268
4451
  if (!onlyOrphanedClaims && (!aged || recoveryFenced(checkedAt, staleAfterMs))) return false;
4269
4452
  for (const name of entries.filter((entry) => RECOVERY_CLAIM.test(entry))) {
4270
4453
  const claimPath = (0, import_node_path.join)(lockPath, name);
4271
- if (await hasLiveRecoveryOwner(claimPath)) {
4454
+ if (await hasLiveRecoveryOwner(claimPath, liveness)) {
4272
4455
  if (!await exists((0, import_node_path.join)(lockPath, "owner.json"))) await (0, import_promises.rename)(claimPath, (0, import_node_path.join)(lockPath, "owner.json"));
4273
4456
  return false;
4274
4457
  }
@@ -4284,7 +4467,7 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
4284
4467
  await removeAgedOwnerFiles(lockPath, staleAfterMs);
4285
4468
  return true;
4286
4469
  }
4287
- async function hasLiveRecoveryOwner(claimPath) {
4470
+ async function hasLiveRecoveryOwner(claimPath, liveness) {
4288
4471
  let contents;
4289
4472
  try {
4290
4473
  contents = await (0, import_promises.readFile)(claimPath, "utf8");
@@ -4298,8 +4481,8 @@ async function hasLiveRecoveryOwner(claimPath) {
4298
4481
  } catch {
4299
4482
  return false;
4300
4483
  }
4301
- const pid = typeof owner === "object" && owner !== null && "pid" in owner ? owner.pid : null;
4302
- return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && isProcessAlive(pid);
4484
+ const record = typeof owner === "object" && owner !== null && "pid" in owner ? owner : null;
4485
+ return typeof record?.pid === "number" && Number.isInteger(record.pid) && record.pid > 0 && await isRecordedProcessAlive(record, liveness);
4303
4486
  }
4304
4487
  function recoveryFenced(since, staleAfterMs) {
4305
4488
  return performance.now() - since >= staleAfterMs / 2;
@@ -4316,11 +4499,11 @@ async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
4316
4499
  if (aged) await (0, import_promises.rm)(path, { force: true }).catch(() => void 0);
4317
4500
  }
4318
4501
  }
4319
- async function recoverStaleLock(profile, lockPath, staleAfterMs) {
4502
+ async function recoverStaleLock(profile, lockPath, liveness, staleAfterMs) {
4320
4503
  const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
4321
4504
  const observed = await (0, import_promises.readFile)(ownerPath, "utf8").catch(() => null);
4322
- if (observed === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
4323
- if (!await isAbandonedOwner(observed, ownerPath, staleAfterMs)) return false;
4505
+ if (observed === null) return recoverOwnerlessLock(profile, lockPath, liveness, staleAfterMs);
4506
+ if (!await isAbandonedOwner(observed, ownerPath, liveness, staleAfterMs)) return false;
4324
4507
  await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
4325
4508
  const claimPath = (0, import_node_path.join)(lockPath, `.recover-${(0, import_node_crypto.randomUUID)()}.json`);
4326
4509
  let moved = false;
@@ -4356,7 +4539,7 @@ async function recoverStaleLock(profile, lockPath, staleAfterMs) {
4356
4539
  await removeAgedOwnerFiles(lockPath, staleAfterMs);
4357
4540
  return true;
4358
4541
  }
4359
- async function isAbandonedOwner(observed, ownerPath, staleAfterMs) {
4542
+ async function isAbandonedOwner(observed, ownerPath, liveness, staleAfterMs) {
4360
4543
  let owner;
4361
4544
  try {
4362
4545
  owner = JSON.parse(observed);
@@ -4364,7 +4547,7 @@ async function isAbandonedOwner(observed, ownerPath, staleAfterMs) {
4364
4547
  owner = null;
4365
4548
  }
4366
4549
  if (typeof owner === "object" && owner !== null && "pid" in owner) {
4367
- return isStaleOwner(owner, staleAfterMs);
4550
+ return isStaleOwner(owner, liveness, staleAfterMs);
4368
4551
  }
4369
4552
  const modifiedAt = await (0, import_promises.stat)(ownerPath).then(({ mtimeMs }) => mtimeMs, () => Number.NaN);
4370
4553
  return Date.now() - modifiedAt >= staleAfterMs;
@@ -4386,14 +4569,14 @@ async function waitForTestBarrier(environmentName, profile) {
4386
4569
  }
4387
4570
  }
4388
4571
  }
4389
- function isStaleOwner(owner, staleAfterMs) {
4572
+ async function isStaleOwner(owner, liveness, staleAfterMs) {
4390
4573
  const now = Date.now();
4391
4574
  const createdAt = typeof owner?.createdAt === "string" ? Date.parse(owner.createdAt) : Number.NaN;
4392
4575
  if (!Number.isFinite(createdAt) || now - createdAt < staleAfterMs) return false;
4393
4576
  if (typeof owner?.pid !== "number" || !Number.isInteger(owner.pid) || owner.pid <= 0) {
4394
4577
  return false;
4395
4578
  }
4396
- return !isProcessAlive(owner.pid);
4579
+ return !await isRecordedProcessAlive(owner, liveness);
4397
4580
  }
4398
4581
  async function exists(path) {
4399
4582
  try {
@@ -4404,13 +4587,22 @@ async function exists(path) {
4404
4587
  throw error;
4405
4588
  }
4406
4589
  }
4407
- function isProcessAlive(pid) {
4590
+ async function isRecordedProcessAlive(owner, acquisition) {
4591
+ const pid = owner.pid;
4592
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return true;
4408
4593
  try {
4409
4594
  process.kill(pid, 0);
4410
- return true;
4411
4595
  } catch (error) {
4412
4596
  return !isErrno(error, "ESRCH");
4413
4597
  }
4598
+ if (pid !== process.pid || typeof owner.instanceId !== "string" || !/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/.test(owner.instanceId) || owner.instanceId === processInstanceId) return true;
4599
+ const origin = processOrigin();
4600
+ if (!origin || owner.bootId !== origin.bootId || typeof owner.pidNamespace !== "string" || !/^pid:\[[0-9]+\]$/.test(owner.pidNamespace)) return true;
4601
+ if (owner.pidNamespace === origin.pidNamespace) {
4602
+ return typeof owner.processStartTime !== "string" || !/^[0-9]+$/.test(owner.processStartTime) || owner.processStartTime === origin.processStartTime;
4603
+ }
4604
+ const liveness = parseLiveness(owner.liveness);
4605
+ return liveness === null || await probeLiveness(liveness, acquisition) === "unknown";
4414
4606
  }
4415
4607
  function validateProfileName(profile) {
4416
4608
  if (!profile || profile === "." || profile === ".." || profile.includes("/") || profile.includes("\\") || profile.includes("\0")) {