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

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.
@@ -4234,9 +4234,11 @@ var init_authority = __esm({
4234
4234
  // src/state.ts
4235
4235
  import { randomUUID } from "crypto";
4236
4236
  import { AsyncLocalStorage } from "async_hooks";
4237
+ import { readFileSync, readlinkSync } from "fs";
4237
4238
  import {
4238
4239
  chmod,
4239
4240
  link,
4241
+ lstat,
4240
4242
  mkdir,
4241
4243
  open,
4242
4244
  readFile,
@@ -4248,12 +4250,165 @@ import {
4248
4250
  unlink,
4249
4251
  writeFile
4250
4252
  } from "fs/promises";
4253
+ import { connect, createServer } from "net";
4251
4254
  import { homedir } from "os";
4252
4255
  import { basename, dirname, isAbsolute, join, relative, resolve } from "path";
4253
4256
  function processWideContext(key) {
4254
- var _a3;
4257
+ var _a4;
4255
4258
  const registry = globalThis;
4256
- return registry[_a3 = Symbol.for(key)] ?? (registry[_a3] = new AsyncLocalStorage());
4259
+ return registry[_a4 = Symbol.for(key)] ?? (registry[_a4] = new AsyncLocalStorage());
4260
+ }
4261
+ function processOrigin() {
4262
+ if (cachedProcessOrigin !== void 0) return cachedProcessOrigin;
4263
+ if (process.platform !== "linux") return cachedProcessOrigin = null;
4264
+ try {
4265
+ const bootId = readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
4266
+ const pidNamespace = readlinkSync("/proc/self/ns/pid");
4267
+ const processStat = readFileSync("/proc/self/stat", "utf8");
4268
+ const closingParen = processStat.lastIndexOf(") ");
4269
+ const processStartTime = closingParen < 0 ? void 0 : processStat.slice(closingParen + 2).trim().split(/\s+/)[19];
4270
+ if (!/^[0-9a-f-]{36}$/.test(bootId) || !/^pid:\[[0-9]+\]$/.test(pidNamespace) || !processStartTime || !/^[0-9]+$/.test(processStartTime)) {
4271
+ return cachedProcessOrigin = null;
4272
+ }
4273
+ return cachedProcessOrigin = { bootId, pidNamespace, processStartTime };
4274
+ } catch {
4275
+ return cachedProcessOrigin = null;
4276
+ }
4277
+ }
4278
+ function processOwner(liveness) {
4279
+ return { instanceId: processInstanceId, ...processOrigin(), ...liveness ? { liveness: liveness.record } : {} };
4280
+ }
4281
+ function socketDirectory(name) {
4282
+ const match = LIVENESS_SOCKET.exec(name);
4283
+ return match ? `${match[1]}-${match[2]}` : null;
4284
+ }
4285
+ async function pinDirectory(directory) {
4286
+ const handle = await open(directory, "r");
4287
+ try {
4288
+ const { dev, ino } = await handle.stat({ bigint: true });
4289
+ return {
4290
+ identity: `${dev.toString(16)}-${ino.toString(16)}`,
4291
+ path: (name) => `/proc/self/fd/${handle.fd}/${name}`,
4292
+ close: () => handle.close()
4293
+ };
4294
+ } catch (error) {
4295
+ await handle.close();
4296
+ throw error;
4297
+ }
4298
+ }
4299
+ async function bindLiveness(directory) {
4300
+ if (!processOrigin()) return null;
4301
+ let pinned;
4302
+ try {
4303
+ pinned = await pinDirectory(directory);
4304
+ } catch {
4305
+ return null;
4306
+ }
4307
+ const socket = `.live-${pinned.identity}-${randomUUID()}`;
4308
+ const server = createServer((connection) => connection.destroy());
4309
+ let bound = false;
4310
+ try {
4311
+ await new Promise((resolve2, reject) => {
4312
+ server.once("error", reject);
4313
+ server.listen({ path: pinned.path(`${socket}.new`), exclusive: true }, () => {
4314
+ server.off("error", reject);
4315
+ resolve2();
4316
+ });
4317
+ });
4318
+ bound = true;
4319
+ server.on("error", () => void 0);
4320
+ server.unref();
4321
+ await rename(pinned.path(`${socket}.new`), pinned.path(socket));
4322
+ const stats = await lstat(pinned.path(socket), { bigint: true });
4323
+ if (!stats.isSocket()) throw new Error(`${socket} is not a socket`);
4324
+ await waitForTestBarrier(TEST_LOCK_LIVENESS_BARRIER_DIR, basename(directory));
4325
+ return { record: { socket, dev: String(stats.dev), ino: String(stats.ino) }, path: join(directory, socket), server };
4326
+ } catch {
4327
+ if (bound) {
4328
+ server.close();
4329
+ for (const name of [`${socket}.new`, socket]) await unlink(pinned.path(name)).catch(() => void 0);
4330
+ }
4331
+ return null;
4332
+ } finally {
4333
+ await pinned.close();
4334
+ }
4335
+ }
4336
+ async function ensureLiveness(acquisition) {
4337
+ if (acquisition.held === void 0) acquisition.held = await bindLiveness(acquisition.directory);
4338
+ return acquisition.held;
4339
+ }
4340
+ async function closeLiveness(held) {
4341
+ held.server.close();
4342
+ await unlink(held.path).catch(() => void 0);
4343
+ }
4344
+ function parseLiveness(value) {
4345
+ if (typeof value !== "object" || value === null) return null;
4346
+ const { socket, dev, ino } = value;
4347
+ const name = typeof socket === "string" ? LIVENESS_SOCKET.exec(socket) : null;
4348
+ 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;
4349
+ }
4350
+ async function controlIn(pinned, acquisition) {
4351
+ const own = await ensureLiveness(acquisition);
4352
+ if (!own || socketDirectory(own.record.socket) !== pinned.identity) return null;
4353
+ const stats = await lstat(pinned.path(own.record.socket), { bigint: true }).catch(() => null);
4354
+ return stats?.isSocket() && String(stats.dev) === own.record.dev && String(stats.ino) === own.record.ino ? own : null;
4355
+ }
4356
+ async function tryConnect(path) {
4357
+ return new Promise((resolve2) => {
4358
+ const probe = connect({ path });
4359
+ const settle = (outcome) => {
4360
+ clearTimeout(timer);
4361
+ probe.destroy();
4362
+ resolve2(outcome);
4363
+ };
4364
+ const timer = setTimeout(() => settle("other"), LIVENESS_PROBE_TIMEOUT_MS);
4365
+ probe.once("connect", () => settle("connected"));
4366
+ probe.once("error", (error) => settle(isErrno(error, "ECONNREFUSED") ? "refused" : "other"));
4367
+ });
4368
+ }
4369
+ async function kernelRefuses(pinned, name, control) {
4370
+ if (process.versions.bun !== void 0) return false;
4371
+ return await tryConnect(pinned.path(name)) === "refused" && await tryConnect(pinned.path(control.record.socket)) === "connected";
4372
+ }
4373
+ async function probeLiveness(liveness, acquisition) {
4374
+ let pinned;
4375
+ try {
4376
+ pinned = await pinDirectory(acquisition.directory);
4377
+ } catch {
4378
+ return "unknown";
4379
+ }
4380
+ try {
4381
+ if (socketDirectory(liveness.socket) !== pinned.identity) return "unknown";
4382
+ const control = await controlIn(pinned, acquisition);
4383
+ if (!control) return "unknown";
4384
+ let stats;
4385
+ try {
4386
+ stats = await lstat(pinned.path(liveness.socket), { bigint: true });
4387
+ } catch (error) {
4388
+ return isErrno(error, "ENOENT") ? "gone" : "unknown";
4389
+ }
4390
+ if (!stats.isSocket() || String(stats.dev) !== liveness.dev || String(stats.ino) !== liveness.ino) return "unknown";
4391
+ return await kernelRefuses(pinned, liveness.socket, control) ? "refused" : "unknown";
4392
+ } finally {
4393
+ await pinned.close();
4394
+ }
4395
+ }
4396
+ async function sweepLiveness(acquisition) {
4397
+ if (process.versions.bun !== void 0 || !acquisition.held) return;
4398
+ const pinned = await pinDirectory(acquisition.directory);
4399
+ try {
4400
+ const control = await controlIn(pinned, acquisition);
4401
+ if (!control) return;
4402
+ for (const name of await readdir(pinned.path("."))) {
4403
+ if (name === control.record.socket || socketDirectory(name) !== pinned.identity) continue;
4404
+ const stats = await lstat(pinned.path(name)).catch(() => null);
4405
+ if (stats?.isSocket() && await kernelRefuses(pinned, name, control)) {
4406
+ await unlink(pinned.path(name)).catch(() => void 0);
4407
+ }
4408
+ }
4409
+ } finally {
4410
+ await pinned.close();
4411
+ }
4257
4412
  }
4258
4413
  function tinycloudHomePath() {
4259
4414
  const home = invocationStateRoot.getStore() ?? process.env.TC_HOME ?? process.env.HOME ?? process.env.USERPROFILE ?? homedir();
@@ -4430,7 +4585,18 @@ async function acquireProfileLock(profile, options) {
4430
4585
  if (path !== directory || !isErrno(error, "ENOENT")) throw error;
4431
4586
  });
4432
4587
  }
4433
- const { turn, firstWaitedTurn } = await acquireTurn(profile, turnDirectory, deadline);
4588
+ const liveness = { directory: turnDirectory, held: void 0 };
4589
+ const closeAcquisitionLiveness = async () => {
4590
+ if (liveness.held) await closeLiveness(liveness.held);
4591
+ };
4592
+ let taken;
4593
+ try {
4594
+ taken = await acquireTurn(profile, turnDirectory, deadline, liveness);
4595
+ } catch (error) {
4596
+ await closeAcquisitionLiveness();
4597
+ throw error;
4598
+ }
4599
+ const { turn, firstWaitedTurn } = taken;
4434
4600
  try {
4435
4601
  if (options.requireProfile && !await exists(profileConfigPath(profile))) {
4436
4602
  await rmdir(directory).catch(() => void 0);
@@ -4440,7 +4606,7 @@ async function acquireProfileLock(profile, options) {
4440
4606
  const deletedWhileWaiting = typeof deletion?.slot === "number" && deletion.slot >= firstWaitedTurn;
4441
4607
  while (true) {
4442
4608
  const token = randomUUID();
4443
- if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true)) {
4609
+ if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true, liveness.held ?? null)) {
4444
4610
  return {
4445
4611
  deletedWhileWaiting,
4446
4612
  turnSlot: turn.slot,
@@ -4448,17 +4614,19 @@ async function acquireProfileLock(profile, options) {
4448
4614
  try {
4449
4615
  await releaseProfileLock(profile, lockPath, token);
4450
4616
  } finally {
4451
- await releaseTurn(profile, turn);
4617
+ await releaseTurn(profile, turn, liveness);
4618
+ await closeAcquisitionLiveness();
4452
4619
  }
4453
4620
  }
4454
4621
  };
4455
4622
  }
4456
4623
  await signalTestLockContention(profile);
4457
- if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
4624
+ if (await recoverStaleLock(profile, lockPath, liveness, staleAfterMs)) continue;
4458
4625
  await waitOrTimeOut(profile, deadline);
4459
4626
  }
4460
4627
  } catch (error) {
4461
- await releaseTurn(profile, turn);
4628
+ await releaseTurn(profile, turn, liveness);
4629
+ await closeAcquisitionLiveness();
4462
4630
  throw error;
4463
4631
  }
4464
4632
  }
@@ -4471,7 +4639,7 @@ async function waitOrTimeOut(profile, deadline, detail) {
4471
4639
  checkDeadline(profile, deadline, detail);
4472
4640
  await sleep(Math.min(deadline.retryMs, deadline.timeoutMs - (Date.now() - deadline.startedAt)));
4473
4641
  }
4474
- async function acquireTurn(profile, directory, deadline) {
4642
+ async function acquireTurn(profile, directory, deadline, liveness) {
4475
4643
  let firstWaitedTurn;
4476
4644
  let problem;
4477
4645
  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.`;
@@ -4496,13 +4664,13 @@ async function acquireTurn(profile, directory, deadline) {
4496
4664
  }
4497
4665
  firstWaitedTurn ?? (firstWaitedTurn = latest.done ? latest.slot + 1 : latest.slot);
4498
4666
  if (latest.held && latest.done) {
4499
- const turn = await takeTurn(profile, directory, latest);
4667
+ const turn = await takeTurn(profile, directory, latest, await ensureLiveness(liveness));
4500
4668
  if (turn !== null) return { turn, firstWaitedTurn };
4501
4669
  await waitOrTimeOut(profile, deadline);
4502
4670
  } else if (latest.done) {
4503
4671
  problem = damaged(`ends with turn ${latest.slot}, which was never granted`);
4504
4672
  await waitOrTimeOut(profile, deadline, problem);
4505
- } else if (latest.pid === process.pid ? relinquishedTurns.has(latest.token) : !isProcessAlive(latest.pid)) {
4673
+ } else if (latest.pid === process.pid && relinquishedTurns.has(latest.token) || !await isRecordedProcessAlive(latest, liveness)) {
4506
4674
  await waitForTestBarrier(TEST_LOCK_TURN_SETTLE_BARRIER_DIR, profile);
4507
4675
  await settleAbandonedTurn(directory, latest);
4508
4676
  relinquishedTurns.delete(latest.token);
@@ -4547,6 +4715,7 @@ async function readTurn(directory, slot) {
4547
4715
  const done = await exists(join(path, `${owner.token}.done`));
4548
4716
  const held = await exists(join(path, `${owner.token}.held`));
4549
4717
  return {
4718
+ ...owner,
4550
4719
  slot,
4551
4720
  token: owner.token,
4552
4721
  pid: owner.pid,
@@ -4560,11 +4729,11 @@ async function isFinishedTurn(directory, slot, token) {
4560
4729
  if (turn === "damaged") throw new Error(`Turn ${slot} of the turn lock ${directory} is unreadable.`);
4561
4730
  return turn !== null && turn.token === token && turn.held && turn.done;
4562
4731
  }
4563
- async function takeTurn(profile, directory, latest) {
4732
+ async function takeTurn(profile, directory, latest, liveness) {
4564
4733
  const token = randomUUID();
4565
4734
  const slot = latest.slot + 1;
4566
4735
  const path = join(directory, String(slot));
4567
- const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: latest.token };
4736
+ const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: latest.token, ...processOwner(liveness) };
4568
4737
  let published = false;
4569
4738
  try {
4570
4739
  published = await publishDirectory(
@@ -4618,7 +4787,7 @@ async function markTurn(path, token, kind) {
4618
4787
  }
4619
4788
  async function createTurnDirectory(directory) {
4620
4789
  const token = randomUUID();
4621
- const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: null };
4790
+ const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: null, ...processOwner(null) };
4622
4791
  await publishDirectory(join(dirname(directory), `.stage-${basename(directory)}-${process.pid}-${token}`), directory, {
4623
4792
  "0/owner.json": `${JSON.stringify(owner, null, 2)}
4624
4793
  `,
@@ -4642,31 +4811,39 @@ async function publishDirectory(stage, target, files, beforeRename) {
4642
4811
  await rm(stage, { recursive: true, force: true }).catch(() => void 0);
4643
4812
  }
4644
4813
  }
4645
- async function releaseTurn(profile, turn) {
4814
+ async function releaseTurn(profile, turn, liveness) {
4646
4815
  try {
4647
4816
  await markTurn(join(turn.directory, String(turn.slot)), turn.token, "done");
4648
4817
  } catch {
4649
4818
  settleOwnTurn(turn.directory, turn.slot, turn.token);
4650
4819
  return;
4651
4820
  }
4652
- await collectTurns(profile, turn).catch(() => void 0);
4821
+ await collectTurns(profile, turn, liveness).catch(() => void 0);
4653
4822
  }
4654
- function isAbandonedTurnEntry(name) {
4823
+ async function isAbandonedTurnEntry(directory, name, liveness) {
4655
4824
  if (name.startsWith(".trash-")) return true;
4656
4825
  const writer = /^\.(?:stage|deleted\.json)[-.]([0-9]+)[-.]/.exec(name)?.[1];
4657
- return writer !== void 0 && !isProcessAlive(Number(writer));
4826
+ if (writer === void 0) return false;
4827
+ if (Number(writer) !== process.pid) return !await isRecordedProcessAlive({ pid: Number(writer) }, liveness);
4828
+ return name.startsWith(".stage-") && await isStagedOwnerGone(join(directory, name, "owner.json"), liveness);
4658
4829
  }
4659
- function isAbandonedTurnDirectoryStage(name) {
4830
+ async function isAbandonedTurnDirectoryStage(directory, name, liveness) {
4660
4831
  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];
4661
- return writer !== void 0 && !isProcessAlive(Number(writer));
4832
+ if (writer === void 0) return false;
4833
+ if (Number(writer) !== process.pid) return !await isRecordedProcessAlive({ pid: Number(writer) }, liveness);
4834
+ return isStagedOwnerGone(join(directory, name, "0", "owner.json"), liveness);
4662
4835
  }
4663
- async function collectTurns(profile, turn) {
4836
+ async function isStagedOwnerGone(recordPath, liveness) {
4837
+ const owner = await readJson(recordPath).catch(() => null);
4838
+ return typeof owner === "object" && owner !== null && owner.pid === process.pid && !await isRecordedProcessAlive(owner, liveness);
4839
+ }
4840
+ async function collectTurns(profile, turn, liveness) {
4664
4841
  for (const [directory, abandoned] of [
4665
4842
  [turn.directory, isAbandonedTurnEntry],
4666
4843
  [dirname(turn.directory), isAbandonedTurnDirectoryStage]
4667
4844
  ]) {
4668
- for (const name of (await readdir(directory)).filter(abandoned)) {
4669
- await rm(join(directory, name), { recursive: true, force: true }).catch(() => void 0);
4845
+ for (const name of await readdir(directory)) {
4846
+ if (await abandoned(directory, name, liveness)) await rm(join(directory, name), { recursive: true, force: true }).catch(() => void 0);
4670
4847
  }
4671
4848
  }
4672
4849
  const older = (await readdir(turn.directory)).filter((name) => TURN.test(name)).map(Number).filter((slot) => slot < turn.slot).sort((left, right) => left - right);
@@ -4684,8 +4861,9 @@ async function collectTurns(profile, turn) {
4684
4861
  await rm(trash, { recursive: true, force: true }).catch(() => void 0);
4685
4862
  }
4686
4863
  }
4864
+ await sweepLiveness(liveness);
4687
4865
  }
4688
- async function publishProfileLock(profile, lockPath, token, requireProfile) {
4866
+ async function publishProfileLock(profile, lockPath, token, requireProfile, liveness) {
4689
4867
  try {
4690
4868
  await mkdir(lockPath, { mode: PRIVATE_DIR_MODE });
4691
4869
  } catch (error) {
@@ -4698,7 +4876,7 @@ async function publishProfileLock(profile, lockPath, token, requireProfile) {
4698
4876
  await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);
4699
4877
  const ownerPath = join(lockPath, "owner.json");
4700
4878
  const staged = join(dirname(lockPath), `.lock-owner-${token}.tmp`);
4701
- const owner = `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token }, null, 2)}
4879
+ const owner = `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, ...processOwner(liveness) }, null, 2)}
4702
4880
  `;
4703
4881
  try {
4704
4882
  await writeFile(staged, owner, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
@@ -4742,7 +4920,7 @@ async function signalTestLockContention(profile) {
4742
4920
  await writeFile(signalPath, `${profile}
4743
4921
  `, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
4744
4922
  }
4745
- async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
4923
+ async function recoverOwnerlessLock(profile, lockPath, liveness, staleAfterMs) {
4746
4924
  let aged;
4747
4925
  try {
4748
4926
  const { mtimeMs } = await stat(lockPath);
@@ -4759,7 +4937,7 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
4759
4937
  if (!onlyOrphanedClaims && (!aged || recoveryFenced(checkedAt, staleAfterMs))) return false;
4760
4938
  for (const name of entries.filter((entry) => RECOVERY_CLAIM.test(entry))) {
4761
4939
  const claimPath = join(lockPath, name);
4762
- if (await hasLiveRecoveryOwner(claimPath)) {
4940
+ if (await hasLiveRecoveryOwner(claimPath, liveness)) {
4763
4941
  if (!await exists(join(lockPath, "owner.json"))) await rename(claimPath, join(lockPath, "owner.json"));
4764
4942
  return false;
4765
4943
  }
@@ -4775,7 +4953,7 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
4775
4953
  await removeAgedOwnerFiles(lockPath, staleAfterMs);
4776
4954
  return true;
4777
4955
  }
4778
- async function hasLiveRecoveryOwner(claimPath) {
4956
+ async function hasLiveRecoveryOwner(claimPath, liveness) {
4779
4957
  let contents;
4780
4958
  try {
4781
4959
  contents = await readFile(claimPath, "utf8");
@@ -4789,8 +4967,8 @@ async function hasLiveRecoveryOwner(claimPath) {
4789
4967
  } catch {
4790
4968
  return false;
4791
4969
  }
4792
- const pid = typeof owner === "object" && owner !== null && "pid" in owner ? owner.pid : null;
4793
- return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && isProcessAlive(pid);
4970
+ const record = typeof owner === "object" && owner !== null && "pid" in owner ? owner : null;
4971
+ return typeof record?.pid === "number" && Number.isInteger(record.pid) && record.pid > 0 && await isRecordedProcessAlive(record, liveness);
4794
4972
  }
4795
4973
  function recoveryFenced(since, staleAfterMs) {
4796
4974
  return performance.now() - since >= staleAfterMs / 2;
@@ -4804,11 +4982,11 @@ async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
4804
4982
  if (aged) await rm(path, { force: true }).catch(() => void 0);
4805
4983
  }
4806
4984
  }
4807
- async function recoverStaleLock(profile, lockPath, staleAfterMs) {
4985
+ async function recoverStaleLock(profile, lockPath, liveness, staleAfterMs) {
4808
4986
  const ownerPath = join(lockPath, "owner.json");
4809
4987
  const observed = await readFile(ownerPath, "utf8").catch(() => null);
4810
- if (observed === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
4811
- if (!await isAbandonedOwner(observed, ownerPath, staleAfterMs)) return false;
4988
+ if (observed === null) return recoverOwnerlessLock(profile, lockPath, liveness, staleAfterMs);
4989
+ if (!await isAbandonedOwner(observed, ownerPath, liveness, staleAfterMs)) return false;
4812
4990
  await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
4813
4991
  const claimPath = join(lockPath, `.recover-${randomUUID()}.json`);
4814
4992
  let moved = false;
@@ -4844,7 +5022,7 @@ async function recoverStaleLock(profile, lockPath, staleAfterMs) {
4844
5022
  await removeAgedOwnerFiles(lockPath, staleAfterMs);
4845
5023
  return true;
4846
5024
  }
4847
- async function isAbandonedOwner(observed, ownerPath, staleAfterMs) {
5025
+ async function isAbandonedOwner(observed, ownerPath, liveness, staleAfterMs) {
4848
5026
  let owner;
4849
5027
  try {
4850
5028
  owner = JSON.parse(observed);
@@ -4852,7 +5030,7 @@ async function isAbandonedOwner(observed, ownerPath, staleAfterMs) {
4852
5030
  owner = null;
4853
5031
  }
4854
5032
  if (typeof owner === "object" && owner !== null && "pid" in owner) {
4855
- return isStaleOwner(owner, staleAfterMs);
5033
+ return isStaleOwner(owner, liveness, staleAfterMs);
4856
5034
  }
4857
5035
  const modifiedAt = await stat(ownerPath).then(({ mtimeMs }) => mtimeMs, () => Number.NaN);
4858
5036
  return Date.now() - modifiedAt >= staleAfterMs;
@@ -4874,14 +5052,14 @@ async function waitForTestBarrier(environmentName, profile) {
4874
5052
  }
4875
5053
  }
4876
5054
  }
4877
- function isStaleOwner(owner, staleAfterMs) {
5055
+ async function isStaleOwner(owner, liveness, staleAfterMs) {
4878
5056
  const now = Date.now();
4879
5057
  const createdAt = typeof owner?.createdAt === "string" ? Date.parse(owner.createdAt) : Number.NaN;
4880
5058
  if (!Number.isFinite(createdAt) || now - createdAt < staleAfterMs) return false;
4881
5059
  if (typeof owner?.pid !== "number" || !Number.isInteger(owner.pid) || owner.pid <= 0) {
4882
5060
  return false;
4883
5061
  }
4884
- return !isProcessAlive(owner.pid);
5062
+ return !await isRecordedProcessAlive(owner, liveness);
4885
5063
  }
4886
5064
  async function exists(path) {
4887
5065
  try {
@@ -4892,13 +5070,22 @@ async function exists(path) {
4892
5070
  throw error;
4893
5071
  }
4894
5072
  }
4895
- function isProcessAlive(pid) {
5073
+ async function isRecordedProcessAlive(owner, acquisition) {
5074
+ const pid = owner.pid;
5075
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return true;
4896
5076
  try {
4897
5077
  process.kill(pid, 0);
4898
- return true;
4899
5078
  } catch (error) {
4900
5079
  return !isErrno(error, "ESRCH");
4901
5080
  }
5081
+ 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;
5082
+ const origin = processOrigin();
5083
+ if (!origin || owner.bootId !== origin.bootId || typeof owner.pidNamespace !== "string" || !/^pid:\[[0-9]+\]$/.test(owner.pidNamespace)) return true;
5084
+ if (owner.pidNamespace === origin.pidNamespace) {
5085
+ return typeof owner.processStartTime !== "string" || !/^[0-9]+$/.test(owner.processStartTime) || owner.processStartTime === origin.processStartTime;
5086
+ }
5087
+ const liveness = parseLiveness(owner.liveness);
5088
+ return liveness === null || await probeLiveness(liveness, acquisition) === "unknown";
4902
5089
  }
4903
5090
  function validateProfileName(profile) {
4904
5091
  if (!profile || profile === "." || profile === ".." || profile.includes("/") || profile.includes("\\") || profile.includes("\0")) {
@@ -4912,7 +5099,7 @@ function isErrno(error, code) {
4912
5099
  function sleep(milliseconds) {
4913
5100
  return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
4914
5101
  }
4915
- var DEFAULT_PROFILE, DEFAULT_LOCK_TIMEOUT_MS, DEFAULT_LOCK_RETRY_MS, DEFAULT_STALE_LOCK_MS, TEST_LOCK_CONTENTION_SIGNAL_PATH, PRIVATE_FILE_MODE, PRIVATE_DIR_MODE, TEST_LOCK_RECOVERY_BARRIER_DIR, TEST_LOCK_OWNERLESS_BARRIER_DIR, TEST_LOCK_PUBLISH_BARRIER_DIR, TEST_LOCK_RELEASE_BARRIER_DIR, TEST_LOCK_CLAIM_BARRIER_DIR, TEST_LOCK_CLAIMED_BARRIER_DIR, TEST_LOCK_VERIFIED_BARRIER_DIR, TEST_LOCK_MOVED_BARRIER_DIR, TEST_LOCK_TURN_PUBLISH_BARRIER_DIR, TEST_LOCK_TURN_SETTLE_BARRIER_DIR, TEST_LOCK_TURN_COLLECT_BARRIER_DIR, invocationStateRoot, heldProfileLocks, ProfileLockTimeoutError, ProfileDeletedError, TURN, _a2, relinquishedTurns, MARKER_RETRY_DELAYS_MS, SETTLE_RETRY_MS, SETTLE_RETRIES, NO_HARD_LINK_ERRORS, ABANDONED_CLAIM, RECOVERY_CLAIM, STAGED_OWNER;
5102
+ var DEFAULT_PROFILE, DEFAULT_LOCK_TIMEOUT_MS, DEFAULT_LOCK_RETRY_MS, DEFAULT_STALE_LOCK_MS, TEST_LOCK_CONTENTION_SIGNAL_PATH, PRIVATE_FILE_MODE, PRIVATE_DIR_MODE, TEST_LOCK_RECOVERY_BARRIER_DIR, TEST_LOCK_OWNERLESS_BARRIER_DIR, TEST_LOCK_PUBLISH_BARRIER_DIR, TEST_LOCK_RELEASE_BARRIER_DIR, TEST_LOCK_CLAIM_BARRIER_DIR, TEST_LOCK_CLAIMED_BARRIER_DIR, TEST_LOCK_VERIFIED_BARRIER_DIR, TEST_LOCK_MOVED_BARRIER_DIR, TEST_LOCK_TURN_PUBLISH_BARRIER_DIR, TEST_LOCK_TURN_SETTLE_BARRIER_DIR, TEST_LOCK_TURN_COLLECT_BARRIER_DIR, TEST_LOCK_LIVENESS_BARRIER_DIR, invocationStateRoot, heldProfileLocks, _a2, processInstanceId, cachedProcessOrigin, LIVENESS_SOCKET, LIVENESS_PROBE_TIMEOUT_MS, ProfileLockTimeoutError, ProfileDeletedError, TURN, _a3, relinquishedTurns, MARKER_RETRY_DELAYS_MS, SETTLE_RETRY_MS, SETTLE_RETRIES, NO_HARD_LINK_ERRORS, ABANDONED_CLAIM, RECOVERY_CLAIM, STAGED_OWNER;
4916
5103
  var init_state = __esm({
4917
5104
  "src/state.ts"() {
4918
5105
  "use strict";
@@ -4934,8 +5121,12 @@ var init_state = __esm({
4934
5121
  TEST_LOCK_TURN_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_PUBLISH_BARRIER_DIR";
4935
5122
  TEST_LOCK_TURN_SETTLE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_SETTLE_BARRIER_DIR";
4936
5123
  TEST_LOCK_TURN_COLLECT_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_COLLECT_BARRIER_DIR";
5124
+ TEST_LOCK_LIVENESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_LIVENESS_BARRIER_DIR";
4937
5125
  invocationStateRoot = processWideContext("tinycloud.operations.invocationStateRoot.v1");
4938
5126
  heldProfileLocks = processWideContext("tinycloud.operations.heldProfileLocks.v2");
5127
+ processInstanceId = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a2] = randomUUID());
5128
+ LIVENESS_SOCKET = /^\.live-([0-9a-f]+)-([0-9a-f]+)-[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}(\.new)?$/;
5129
+ LIVENESS_PROBE_TIMEOUT_MS = 1e3;
4939
5130
  ProfileLockTimeoutError = class extends Error {
4940
5131
  /** `detail` names the cause when it is not simply another process holding the lock. */
4941
5132
  constructor(profile, timeoutMs, detail) {
@@ -4952,7 +5143,7 @@ var init_state = __esm({
4952
5143
  }
4953
5144
  };
4954
5145
  TURN = /^(?:0|[1-9][0-9]*)$/;
4955
- relinquishedTurns = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a2] = /* @__PURE__ */ new Set());
5146
+ relinquishedTurns = globalThis[_a3 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a3] = /* @__PURE__ */ new Set());
4956
5147
  MARKER_RETRY_DELAYS_MS = [10, 50, 250];
4957
5148
  SETTLE_RETRY_MS = 1e3;
4958
5149
  SETTLE_RETRIES = 300;