@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.
@@ -4109,10 +4109,10 @@ function evaluateOperationAuthority(granted, required, resolveSpace2) {
4109
4109
  const canonicalRequired = canonicalizeOperationCapabilities(required, resolveSpace2);
4110
4110
  const missing = [];
4111
4111
  for (const requested of canonicalRequired) {
4112
- const covered = canonicalGranted.some(
4113
- (candidate) => candidate.service === requested.service && operationSpacesEqual(candidate.space, requested.space) && isCapabilitySubset([requested], [candidate]).subset
4112
+ const sameScope = canonicalGranted.filter(
4113
+ (candidate) => candidate.service === requested.service && operationSpacesEqual(candidate.space, requested.space)
4114
4114
  );
4115
- if (!covered) missing.push(requested);
4115
+ if (!isCapabilitySubset([requested], sameScope).subset) missing.push(requested);
4116
4116
  }
4117
4117
  return {
4118
4118
  satisfied: missing.length === 0,
@@ -4235,9 +4235,161 @@ var init_authority = __esm({
4235
4235
 
4236
4236
  // src/state.ts
4237
4237
  function processWideContext(key) {
4238
- var _a3;
4238
+ var _a4;
4239
4239
  const registry = globalThis;
4240
- return registry[_a3 = Symbol.for(key)] ?? (registry[_a3] = new import_node_async_hooks.AsyncLocalStorage());
4240
+ return registry[_a4 = Symbol.for(key)] ?? (registry[_a4] = new import_node_async_hooks.AsyncLocalStorage());
4241
+ }
4242
+ function processOrigin() {
4243
+ if (cachedProcessOrigin !== void 0) return cachedProcessOrigin;
4244
+ if (process.platform !== "linux") return cachedProcessOrigin = null;
4245
+ try {
4246
+ const bootId = (0, import_node_fs.readFileSync)("/proc/sys/kernel/random/boot_id", "utf8").trim();
4247
+ const pidNamespace = (0, import_node_fs.readlinkSync)("/proc/self/ns/pid");
4248
+ const processStat = (0, import_node_fs.readFileSync)("/proc/self/stat", "utf8");
4249
+ const closingParen = processStat.lastIndexOf(") ");
4250
+ const processStartTime = closingParen < 0 ? void 0 : processStat.slice(closingParen + 2).trim().split(/\s+/)[19];
4251
+ if (!/^[0-9a-f-]{36}$/.test(bootId) || !/^pid:\[[0-9]+\]$/.test(pidNamespace) || !processStartTime || !/^[0-9]+$/.test(processStartTime)) {
4252
+ return cachedProcessOrigin = null;
4253
+ }
4254
+ return cachedProcessOrigin = { bootId, pidNamespace, processStartTime };
4255
+ } catch {
4256
+ return cachedProcessOrigin = null;
4257
+ }
4258
+ }
4259
+ function processOwner(liveness) {
4260
+ return { instanceId: processInstanceId, ...processOrigin(), ...liveness ? { liveness: liveness.record } : {} };
4261
+ }
4262
+ function socketDirectory(name) {
4263
+ const match = LIVENESS_SOCKET.exec(name);
4264
+ return match ? `${match[1]}-${match[2]}` : null;
4265
+ }
4266
+ async function pinDirectory(directory) {
4267
+ const handle = await (0, import_promises.open)(directory, "r");
4268
+ try {
4269
+ const { dev, ino } = await handle.stat({ bigint: true });
4270
+ return {
4271
+ identity: `${dev.toString(16)}-${ino.toString(16)}`,
4272
+ path: (name) => `/proc/self/fd/${handle.fd}/${name}`,
4273
+ close: () => handle.close()
4274
+ };
4275
+ } catch (error) {
4276
+ await handle.close();
4277
+ throw error;
4278
+ }
4279
+ }
4280
+ async function bindLiveness(directory) {
4281
+ if (!processOrigin()) return null;
4282
+ let pinned;
4283
+ try {
4284
+ pinned = await pinDirectory(directory);
4285
+ } catch {
4286
+ return null;
4287
+ }
4288
+ const socket = `.live-${pinned.identity}-${(0, import_node_crypto.randomUUID)()}`;
4289
+ const server = (0, import_node_net.createServer)((connection) => connection.destroy());
4290
+ let bound = false;
4291
+ try {
4292
+ await new Promise((resolve2, reject) => {
4293
+ server.once("error", reject);
4294
+ server.listen({ path: pinned.path(`${socket}.new`), exclusive: true }, () => {
4295
+ server.off("error", reject);
4296
+ resolve2();
4297
+ });
4298
+ });
4299
+ bound = true;
4300
+ server.on("error", () => void 0);
4301
+ server.unref();
4302
+ await (0, import_promises.rename)(pinned.path(`${socket}.new`), pinned.path(socket));
4303
+ const stats = await (0, import_promises.lstat)(pinned.path(socket), { bigint: true });
4304
+ if (!stats.isSocket()) throw new Error(`${socket} is not a socket`);
4305
+ await waitForTestBarrier(TEST_LOCK_LIVENESS_BARRIER_DIR, (0, import_node_path.basename)(directory));
4306
+ return { record: { socket, dev: String(stats.dev), ino: String(stats.ino) }, path: (0, import_node_path.join)(directory, socket), server };
4307
+ } catch {
4308
+ if (bound) {
4309
+ server.close();
4310
+ for (const name of [`${socket}.new`, socket]) await (0, import_promises.unlink)(pinned.path(name)).catch(() => void 0);
4311
+ }
4312
+ return null;
4313
+ } finally {
4314
+ await pinned.close();
4315
+ }
4316
+ }
4317
+ async function ensureLiveness(acquisition) {
4318
+ if (acquisition.held === void 0) acquisition.held = await bindLiveness(acquisition.directory);
4319
+ return acquisition.held;
4320
+ }
4321
+ async function closeLiveness(held) {
4322
+ held.server.close();
4323
+ await (0, import_promises.unlink)(held.path).catch(() => void 0);
4324
+ }
4325
+ function parseLiveness(value) {
4326
+ if (typeof value !== "object" || value === null) return null;
4327
+ const { socket, dev, ino } = value;
4328
+ const name = typeof socket === "string" ? LIVENESS_SOCKET.exec(socket) : null;
4329
+ 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;
4330
+ }
4331
+ async function controlIn(pinned, acquisition) {
4332
+ const own = await ensureLiveness(acquisition);
4333
+ if (!own || socketDirectory(own.record.socket) !== pinned.identity) return null;
4334
+ const stats = await (0, import_promises.lstat)(pinned.path(own.record.socket), { bigint: true }).catch(() => null);
4335
+ return stats?.isSocket() && String(stats.dev) === own.record.dev && String(stats.ino) === own.record.ino ? own : null;
4336
+ }
4337
+ async function tryConnect(path) {
4338
+ return new Promise((resolve2) => {
4339
+ const probe = (0, import_node_net.connect)({ path });
4340
+ const settle = (outcome) => {
4341
+ clearTimeout(timer);
4342
+ probe.destroy();
4343
+ resolve2(outcome);
4344
+ };
4345
+ const timer = setTimeout(() => settle("other"), LIVENESS_PROBE_TIMEOUT_MS);
4346
+ probe.once("connect", () => settle("connected"));
4347
+ probe.once("error", (error) => settle(isErrno(error, "ECONNREFUSED") ? "refused" : "other"));
4348
+ });
4349
+ }
4350
+ async function kernelRefuses(pinned, name, control) {
4351
+ if (process.versions.bun !== void 0) return false;
4352
+ return await tryConnect(pinned.path(name)) === "refused" && await tryConnect(pinned.path(control.record.socket)) === "connected";
4353
+ }
4354
+ async function probeLiveness(liveness, acquisition) {
4355
+ let pinned;
4356
+ try {
4357
+ pinned = await pinDirectory(acquisition.directory);
4358
+ } catch {
4359
+ return "unknown";
4360
+ }
4361
+ try {
4362
+ if (socketDirectory(liveness.socket) !== pinned.identity) return "unknown";
4363
+ const control = await controlIn(pinned, acquisition);
4364
+ if (!control) return "unknown";
4365
+ let stats;
4366
+ try {
4367
+ stats = await (0, import_promises.lstat)(pinned.path(liveness.socket), { bigint: true });
4368
+ } catch (error) {
4369
+ return isErrno(error, "ENOENT") ? "gone" : "unknown";
4370
+ }
4371
+ if (!stats.isSocket() || String(stats.dev) !== liveness.dev || String(stats.ino) !== liveness.ino) return "unknown";
4372
+ return await kernelRefuses(pinned, liveness.socket, control) ? "refused" : "unknown";
4373
+ } finally {
4374
+ await pinned.close();
4375
+ }
4376
+ }
4377
+ async function sweepLiveness(acquisition) {
4378
+ if (process.versions.bun !== void 0 || !acquisition.held) return;
4379
+ const pinned = await pinDirectory(acquisition.directory);
4380
+ try {
4381
+ const control = await controlIn(pinned, acquisition);
4382
+ if (!control) return;
4383
+ for (const name of await (0, import_promises.readdir)(pinned.path("."))) {
4384
+ if (name === control.record.socket || socketDirectory(name) !== pinned.identity) continue;
4385
+ const stats = await (0, import_promises.lstat)(pinned.path(name)).catch(() => null);
4386
+ if (stats?.isSocket() && await kernelRefuses(pinned, name, control)) {
4387
+ await (0, import_promises.unlink)(pinned.path(name)).catch(() => void 0);
4388
+ }
4389
+ }
4390
+ } finally {
4391
+ await pinned.close();
4392
+ }
4241
4393
  }
4242
4394
  function tinycloudHomePath() {
4243
4395
  const home = invocationStateRoot.getStore() ?? process.env.TC_HOME ?? process.env.HOME ?? process.env.USERPROFILE ?? (0, import_node_os.homedir)();
@@ -4414,7 +4566,18 @@ async function acquireProfileLock(profile, options) {
4414
4566
  if (path !== directory || !isErrno(error, "ENOENT")) throw error;
4415
4567
  });
4416
4568
  }
4417
- const { turn, firstWaitedTurn } = await acquireTurn(profile, turnDirectory, deadline);
4569
+ const liveness = { directory: turnDirectory, held: void 0 };
4570
+ const closeAcquisitionLiveness = async () => {
4571
+ if (liveness.held) await closeLiveness(liveness.held);
4572
+ };
4573
+ let taken;
4574
+ try {
4575
+ taken = await acquireTurn(profile, turnDirectory, deadline, liveness);
4576
+ } catch (error) {
4577
+ await closeAcquisitionLiveness();
4578
+ throw error;
4579
+ }
4580
+ const { turn, firstWaitedTurn } = taken;
4418
4581
  try {
4419
4582
  if (options.requireProfile && !await exists(profileConfigPath(profile))) {
4420
4583
  await (0, import_promises.rmdir)(directory).catch(() => void 0);
@@ -4424,7 +4587,7 @@ async function acquireProfileLock(profile, options) {
4424
4587
  const deletedWhileWaiting = typeof deletion?.slot === "number" && deletion.slot >= firstWaitedTurn;
4425
4588
  while (true) {
4426
4589
  const token = (0, import_node_crypto.randomUUID)();
4427
- if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true)) {
4590
+ if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true, liveness.held ?? null)) {
4428
4591
  return {
4429
4592
  deletedWhileWaiting,
4430
4593
  turnSlot: turn.slot,
@@ -4432,17 +4595,19 @@ async function acquireProfileLock(profile, options) {
4432
4595
  try {
4433
4596
  await releaseProfileLock(profile, lockPath, token);
4434
4597
  } finally {
4435
- await releaseTurn(profile, turn);
4598
+ await releaseTurn(profile, turn, liveness);
4599
+ await closeAcquisitionLiveness();
4436
4600
  }
4437
4601
  }
4438
4602
  };
4439
4603
  }
4440
4604
  await signalTestLockContention(profile);
4441
- if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
4605
+ if (await recoverStaleLock(profile, lockPath, liveness, staleAfterMs)) continue;
4442
4606
  await waitOrTimeOut(profile, deadline);
4443
4607
  }
4444
4608
  } catch (error) {
4445
- await releaseTurn(profile, turn);
4609
+ await releaseTurn(profile, turn, liveness);
4610
+ await closeAcquisitionLiveness();
4446
4611
  throw error;
4447
4612
  }
4448
4613
  }
@@ -4455,7 +4620,7 @@ async function waitOrTimeOut(profile, deadline, detail) {
4455
4620
  checkDeadline(profile, deadline, detail);
4456
4621
  await sleep(Math.min(deadline.retryMs, deadline.timeoutMs - (Date.now() - deadline.startedAt)));
4457
4622
  }
4458
- async function acquireTurn(profile, directory, deadline) {
4623
+ async function acquireTurn(profile, directory, deadline, liveness) {
4459
4624
  let firstWaitedTurn;
4460
4625
  let problem;
4461
4626
  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.`;
@@ -4480,13 +4645,13 @@ async function acquireTurn(profile, directory, deadline) {
4480
4645
  }
4481
4646
  firstWaitedTurn ?? (firstWaitedTurn = latest.done ? latest.slot + 1 : latest.slot);
4482
4647
  if (latest.held && latest.done) {
4483
- const turn = await takeTurn(profile, directory, latest);
4648
+ const turn = await takeTurn(profile, directory, latest, await ensureLiveness(liveness));
4484
4649
  if (turn !== null) return { turn, firstWaitedTurn };
4485
4650
  await waitOrTimeOut(profile, deadline);
4486
4651
  } else if (latest.done) {
4487
4652
  problem = damaged(`ends with turn ${latest.slot}, which was never granted`);
4488
4653
  await waitOrTimeOut(profile, deadline, problem);
4489
- } else if (latest.pid === process.pid ? relinquishedTurns.has(latest.token) : !isProcessAlive(latest.pid)) {
4654
+ } else if (latest.pid === process.pid && relinquishedTurns.has(latest.token) || !await isRecordedProcessAlive(latest, liveness)) {
4490
4655
  await waitForTestBarrier(TEST_LOCK_TURN_SETTLE_BARRIER_DIR, profile);
4491
4656
  await settleAbandonedTurn(directory, latest);
4492
4657
  relinquishedTurns.delete(latest.token);
@@ -4531,6 +4696,7 @@ async function readTurn(directory, slot) {
4531
4696
  const done = await exists((0, import_node_path.join)(path, `${owner.token}.done`));
4532
4697
  const held = await exists((0, import_node_path.join)(path, `${owner.token}.held`));
4533
4698
  return {
4699
+ ...owner,
4534
4700
  slot,
4535
4701
  token: owner.token,
4536
4702
  pid: owner.pid,
@@ -4544,11 +4710,11 @@ async function isFinishedTurn(directory, slot, token) {
4544
4710
  if (turn === "damaged") throw new Error(`Turn ${slot} of the turn lock ${directory} is unreadable.`);
4545
4711
  return turn !== null && turn.token === token && turn.held && turn.done;
4546
4712
  }
4547
- async function takeTurn(profile, directory, latest) {
4713
+ async function takeTurn(profile, directory, latest, liveness) {
4548
4714
  const token = (0, import_node_crypto.randomUUID)();
4549
4715
  const slot = latest.slot + 1;
4550
4716
  const path = (0, import_node_path.join)(directory, String(slot));
4551
- const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: latest.token };
4717
+ const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: latest.token, ...processOwner(liveness) };
4552
4718
  let published = false;
4553
4719
  try {
4554
4720
  published = await publishDirectory(
@@ -4602,7 +4768,7 @@ async function markTurn(path, token, kind) {
4602
4768
  }
4603
4769
  async function createTurnDirectory(directory) {
4604
4770
  const token = (0, import_node_crypto.randomUUID)();
4605
- const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: null };
4771
+ const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: null, ...processOwner(null) };
4606
4772
  await publishDirectory((0, import_node_path.join)((0, import_node_path.dirname)(directory), `.stage-${(0, import_node_path.basename)(directory)}-${process.pid}-${token}`), directory, {
4607
4773
  "0/owner.json": `${JSON.stringify(owner, null, 2)}
4608
4774
  `,
@@ -4626,31 +4792,39 @@ async function publishDirectory(stage, target, files, beforeRename) {
4626
4792
  await (0, import_promises.rm)(stage, { recursive: true, force: true }).catch(() => void 0);
4627
4793
  }
4628
4794
  }
4629
- async function releaseTurn(profile, turn) {
4795
+ async function releaseTurn(profile, turn, liveness) {
4630
4796
  try {
4631
4797
  await markTurn((0, import_node_path.join)(turn.directory, String(turn.slot)), turn.token, "done");
4632
4798
  } catch {
4633
4799
  settleOwnTurn(turn.directory, turn.slot, turn.token);
4634
4800
  return;
4635
4801
  }
4636
- await collectTurns(profile, turn).catch(() => void 0);
4802
+ await collectTurns(profile, turn, liveness).catch(() => void 0);
4637
4803
  }
4638
- function isAbandonedTurnEntry(name) {
4804
+ async function isAbandonedTurnEntry(directory, name, liveness) {
4639
4805
  if (name.startsWith(".trash-")) return true;
4640
4806
  const writer = /^\.(?:stage|deleted\.json)[-.]([0-9]+)[-.]/.exec(name)?.[1];
4641
- return writer !== void 0 && !isProcessAlive(Number(writer));
4807
+ if (writer === void 0) return false;
4808
+ if (Number(writer) !== process.pid) return !await isRecordedProcessAlive({ pid: Number(writer) }, liveness);
4809
+ return name.startsWith(".stage-") && await isStagedOwnerGone((0, import_node_path.join)(directory, name, "owner.json"), liveness);
4642
4810
  }
4643
- function isAbandonedTurnDirectoryStage(name) {
4811
+ async function isAbandonedTurnDirectoryStage(directory, name, liveness) {
4644
4812
  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];
4645
- return writer !== void 0 && !isProcessAlive(Number(writer));
4813
+ if (writer === void 0) return false;
4814
+ if (Number(writer) !== process.pid) return !await isRecordedProcessAlive({ pid: Number(writer) }, liveness);
4815
+ return isStagedOwnerGone((0, import_node_path.join)(directory, name, "0", "owner.json"), liveness);
4646
4816
  }
4647
- async function collectTurns(profile, turn) {
4817
+ async function isStagedOwnerGone(recordPath, liveness) {
4818
+ const owner = await readJson(recordPath).catch(() => null);
4819
+ return typeof owner === "object" && owner !== null && owner.pid === process.pid && !await isRecordedProcessAlive(owner, liveness);
4820
+ }
4821
+ async function collectTurns(profile, turn, liveness) {
4648
4822
  for (const [directory, abandoned] of [
4649
4823
  [turn.directory, isAbandonedTurnEntry],
4650
4824
  [(0, import_node_path.dirname)(turn.directory), isAbandonedTurnDirectoryStage]
4651
4825
  ]) {
4652
- for (const name of (await (0, import_promises.readdir)(directory)).filter(abandoned)) {
4653
- await (0, import_promises.rm)((0, import_node_path.join)(directory, name), { recursive: true, force: true }).catch(() => void 0);
4826
+ for (const name of await (0, import_promises.readdir)(directory)) {
4827
+ 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);
4654
4828
  }
4655
4829
  }
4656
4830
  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);
@@ -4668,8 +4842,9 @@ async function collectTurns(profile, turn) {
4668
4842
  await (0, import_promises.rm)(trash, { recursive: true, force: true }).catch(() => void 0);
4669
4843
  }
4670
4844
  }
4845
+ await sweepLiveness(liveness);
4671
4846
  }
4672
- async function publishProfileLock(profile, lockPath, token, requireProfile) {
4847
+ async function publishProfileLock(profile, lockPath, token, requireProfile, liveness) {
4673
4848
  try {
4674
4849
  await (0, import_promises.mkdir)(lockPath, { mode: PRIVATE_DIR_MODE });
4675
4850
  } catch (error) {
@@ -4682,7 +4857,7 @@ async function publishProfileLock(profile, lockPath, token, requireProfile) {
4682
4857
  await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);
4683
4858
  const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
4684
4859
  const staged = (0, import_node_path.join)((0, import_node_path.dirname)(lockPath), `.lock-owner-${token}.tmp`);
4685
- const owner = `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token }, null, 2)}
4860
+ const owner = `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, ...processOwner(liveness) }, null, 2)}
4686
4861
  `;
4687
4862
  try {
4688
4863
  await (0, import_promises.writeFile)(staged, owner, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
@@ -4726,7 +4901,7 @@ async function signalTestLockContention(profile) {
4726
4901
  await (0, import_promises.writeFile)(signalPath, `${profile}
4727
4902
  `, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
4728
4903
  }
4729
- async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
4904
+ async function recoverOwnerlessLock(profile, lockPath, liveness, staleAfterMs) {
4730
4905
  let aged;
4731
4906
  try {
4732
4907
  const { mtimeMs } = await (0, import_promises.stat)(lockPath);
@@ -4743,7 +4918,7 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
4743
4918
  if (!onlyOrphanedClaims && (!aged || recoveryFenced(checkedAt, staleAfterMs))) return false;
4744
4919
  for (const name of entries.filter((entry) => RECOVERY_CLAIM.test(entry))) {
4745
4920
  const claimPath = (0, import_node_path.join)(lockPath, name);
4746
- if (await hasLiveRecoveryOwner(claimPath)) {
4921
+ if (await hasLiveRecoveryOwner(claimPath, liveness)) {
4747
4922
  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"));
4748
4923
  return false;
4749
4924
  }
@@ -4759,7 +4934,7 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
4759
4934
  await removeAgedOwnerFiles(lockPath, staleAfterMs);
4760
4935
  return true;
4761
4936
  }
4762
- async function hasLiveRecoveryOwner(claimPath) {
4937
+ async function hasLiveRecoveryOwner(claimPath, liveness) {
4763
4938
  let contents;
4764
4939
  try {
4765
4940
  contents = await (0, import_promises.readFile)(claimPath, "utf8");
@@ -4773,8 +4948,8 @@ async function hasLiveRecoveryOwner(claimPath) {
4773
4948
  } catch {
4774
4949
  return false;
4775
4950
  }
4776
- const pid = typeof owner === "object" && owner !== null && "pid" in owner ? owner.pid : null;
4777
- return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && isProcessAlive(pid);
4951
+ const record = typeof owner === "object" && owner !== null && "pid" in owner ? owner : null;
4952
+ return typeof record?.pid === "number" && Number.isInteger(record.pid) && record.pid > 0 && await isRecordedProcessAlive(record, liveness);
4778
4953
  }
4779
4954
  function recoveryFenced(since, staleAfterMs) {
4780
4955
  return performance.now() - since >= staleAfterMs / 2;
@@ -4788,11 +4963,11 @@ async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
4788
4963
  if (aged) await (0, import_promises.rm)(path, { force: true }).catch(() => void 0);
4789
4964
  }
4790
4965
  }
4791
- async function recoverStaleLock(profile, lockPath, staleAfterMs) {
4966
+ async function recoverStaleLock(profile, lockPath, liveness, staleAfterMs) {
4792
4967
  const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
4793
4968
  const observed = await (0, import_promises.readFile)(ownerPath, "utf8").catch(() => null);
4794
- if (observed === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
4795
- if (!await isAbandonedOwner(observed, ownerPath, staleAfterMs)) return false;
4969
+ if (observed === null) return recoverOwnerlessLock(profile, lockPath, liveness, staleAfterMs);
4970
+ if (!await isAbandonedOwner(observed, ownerPath, liveness, staleAfterMs)) return false;
4796
4971
  await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
4797
4972
  const claimPath = (0, import_node_path.join)(lockPath, `.recover-${(0, import_node_crypto.randomUUID)()}.json`);
4798
4973
  let moved = false;
@@ -4828,7 +5003,7 @@ async function recoverStaleLock(profile, lockPath, staleAfterMs) {
4828
5003
  await removeAgedOwnerFiles(lockPath, staleAfterMs);
4829
5004
  return true;
4830
5005
  }
4831
- async function isAbandonedOwner(observed, ownerPath, staleAfterMs) {
5006
+ async function isAbandonedOwner(observed, ownerPath, liveness, staleAfterMs) {
4832
5007
  let owner;
4833
5008
  try {
4834
5009
  owner = JSON.parse(observed);
@@ -4836,7 +5011,7 @@ async function isAbandonedOwner(observed, ownerPath, staleAfterMs) {
4836
5011
  owner = null;
4837
5012
  }
4838
5013
  if (typeof owner === "object" && owner !== null && "pid" in owner) {
4839
- return isStaleOwner(owner, staleAfterMs);
5014
+ return isStaleOwner(owner, liveness, staleAfterMs);
4840
5015
  }
4841
5016
  const modifiedAt = await (0, import_promises.stat)(ownerPath).then(({ mtimeMs }) => mtimeMs, () => Number.NaN);
4842
5017
  return Date.now() - modifiedAt >= staleAfterMs;
@@ -4858,14 +5033,14 @@ async function waitForTestBarrier(environmentName, profile) {
4858
5033
  }
4859
5034
  }
4860
5035
  }
4861
- function isStaleOwner(owner, staleAfterMs) {
5036
+ async function isStaleOwner(owner, liveness, staleAfterMs) {
4862
5037
  const now = Date.now();
4863
5038
  const createdAt = typeof owner?.createdAt === "string" ? Date.parse(owner.createdAt) : Number.NaN;
4864
5039
  if (!Number.isFinite(createdAt) || now - createdAt < staleAfterMs) return false;
4865
5040
  if (typeof owner?.pid !== "number" || !Number.isInteger(owner.pid) || owner.pid <= 0) {
4866
5041
  return false;
4867
5042
  }
4868
- return !isProcessAlive(owner.pid);
5043
+ return !await isRecordedProcessAlive(owner, liveness);
4869
5044
  }
4870
5045
  async function exists(path) {
4871
5046
  try {
@@ -4876,13 +5051,22 @@ async function exists(path) {
4876
5051
  throw error;
4877
5052
  }
4878
5053
  }
4879
- function isProcessAlive(pid) {
5054
+ async function isRecordedProcessAlive(owner, acquisition) {
5055
+ const pid = owner.pid;
5056
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return true;
4880
5057
  try {
4881
5058
  process.kill(pid, 0);
4882
- return true;
4883
5059
  } catch (error) {
4884
5060
  return !isErrno(error, "ESRCH");
4885
5061
  }
5062
+ 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;
5063
+ const origin = processOrigin();
5064
+ if (!origin || owner.bootId !== origin.bootId || typeof owner.pidNamespace !== "string" || !/^pid:\[[0-9]+\]$/.test(owner.pidNamespace)) return true;
5065
+ if (owner.pidNamespace === origin.pidNamespace) {
5066
+ return typeof owner.processStartTime !== "string" || !/^[0-9]+$/.test(owner.processStartTime) || owner.processStartTime === origin.processStartTime;
5067
+ }
5068
+ const liveness = parseLiveness(owner.liveness);
5069
+ return liveness === null || await probeLiveness(liveness, acquisition) === "unknown";
4886
5070
  }
4887
5071
  function validateProfileName(profile) {
4888
5072
  if (!profile || profile === "." || profile === ".." || profile.includes("/") || profile.includes("\\") || profile.includes("\0")) {
@@ -4896,13 +5080,15 @@ function isErrno(error, code) {
4896
5080
  function sleep(milliseconds) {
4897
5081
  return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
4898
5082
  }
4899
- var import_node_crypto, import_node_async_hooks, import_promises, import_node_os, import_node_path, 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;
5083
+ var import_node_crypto, import_node_async_hooks, import_node_fs, import_promises, import_node_net, import_node_os, import_node_path, 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;
4900
5084
  var init_state = __esm({
4901
5085
  "src/state.ts"() {
4902
5086
  "use strict";
4903
5087
  import_node_crypto = require("crypto");
4904
5088
  import_node_async_hooks = require("async_hooks");
5089
+ import_node_fs = require("fs");
4905
5090
  import_promises = require("fs/promises");
5091
+ import_node_net = require("net");
4906
5092
  import_node_os = require("os");
4907
5093
  import_node_path = require("path");
4908
5094
  DEFAULT_PROFILE = "default";
@@ -4923,8 +5109,12 @@ var init_state = __esm({
4923
5109
  TEST_LOCK_TURN_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_PUBLISH_BARRIER_DIR";
4924
5110
  TEST_LOCK_TURN_SETTLE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_SETTLE_BARRIER_DIR";
4925
5111
  TEST_LOCK_TURN_COLLECT_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_COLLECT_BARRIER_DIR";
5112
+ TEST_LOCK_LIVENESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_LIVENESS_BARRIER_DIR";
4926
5113
  invocationStateRoot = processWideContext("tinycloud.operations.invocationStateRoot.v1");
4927
5114
  heldProfileLocks = processWideContext("tinycloud.operations.heldProfileLocks.v2");
5115
+ processInstanceId = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a2] = (0, import_node_crypto.randomUUID)());
5116
+ LIVENESS_SOCKET = /^\.live-([0-9a-f]+)-([0-9a-f]+)-[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}(\.new)?$/;
5117
+ LIVENESS_PROBE_TIMEOUT_MS = 1e3;
4928
5118
  ProfileLockTimeoutError = class extends Error {
4929
5119
  /** `detail` names the cause when it is not simply another process holding the lock. */
4930
5120
  constructor(profile, timeoutMs, detail) {
@@ -4941,7 +5131,7 @@ var init_state = __esm({
4941
5131
  }
4942
5132
  };
4943
5133
  TURN = /^(?:0|[1-9][0-9]*)$/;
4944
- relinquishedTurns = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a2] = /* @__PURE__ */ new Set());
5134
+ relinquishedTurns = globalThis[_a3 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a3] = /* @__PURE__ */ new Set());
4945
5135
  MARKER_RETRY_DELAYS_MS = [10, 50, 250];
4946
5136
  SETTLE_RETRY_MS = 1e3;
4947
5137
  SETTLE_RETRIES = 300;