@tinycloud/operations 0.5.0-beta.1 → 0.5.0-beta.10

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.
Files changed (51) hide show
  1. package/coverage.json +11 -1
  2. package/dist/artifacts.cjs +235 -43
  3. package/dist/artifacts.cjs.map +1 -1
  4. package/dist/artifacts.d.cts +1 -1
  5. package/dist/artifacts.d.ts +1 -1
  6. package/dist/artifacts.js +236 -43
  7. package/dist/artifacts.js.map +1 -1
  8. package/dist/cli-runtime.cjs +961 -178
  9. package/dist/cli-runtime.cjs.map +1 -1
  10. package/dist/cli-runtime.d.cts +1 -1
  11. package/dist/cli-runtime.d.ts +1 -1
  12. package/dist/cli-runtime.js +960 -177
  13. package/dist/cli-runtime.js.map +1 -1
  14. package/dist/{contract-DfyrluLB.d.cts → contract-zRklonbR.d.cts} +6 -3
  15. package/dist/{contract-DfyrluLB.d.ts → contract-zRklonbR.d.ts} +6 -3
  16. package/dist/delegation-binding.cjs +242 -43
  17. package/dist/delegation-binding.cjs.map +1 -1
  18. package/dist/delegation-binding.d.cts +15 -3
  19. package/dist/delegation-binding.d.ts +15 -3
  20. package/dist/delegation-binding.js +242 -43
  21. package/dist/delegation-binding.js.map +1 -1
  22. package/dist/index.cjs +989 -179
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +1 -1
  25. package/dist/index.d.ts +1 -1
  26. package/dist/index.js +986 -176
  27. package/dist/index.js.map +1 -1
  28. package/dist/profile.cjs +13 -4
  29. package/dist/profile.cjs.map +1 -1
  30. package/dist/profile.d.cts +14 -2
  31. package/dist/profile.d.ts +14 -2
  32. package/dist/profile.js +13 -4
  33. package/dist/profile.js.map +1 -1
  34. package/dist/recovery.cjs +67 -0
  35. package/dist/recovery.cjs.map +1 -0
  36. package/dist/recovery.d.cts +38 -0
  37. package/dist/recovery.d.ts +38 -0
  38. package/dist/recovery.js +39 -0
  39. package/dist/recovery.js.map +1 -0
  40. package/dist/secret-request.cjs +271 -0
  41. package/dist/secret-request.cjs.map +1 -0
  42. package/dist/secret-request.d.cts +119 -0
  43. package/dist/secret-request.d.ts +119 -0
  44. package/dist/secret-request.js +222 -0
  45. package/dist/secret-request.js.map +1 -0
  46. package/dist/state.cjs +232 -40
  47. package/dist/state.cjs.map +1 -1
  48. package/dist/state.js +233 -40
  49. package/dist/state.js.map +1 -1
  50. package/generated/operations.json +1838 -89
  51. package/package.json +15 -5
@@ -2,7 +2,7 @@ import { PermissionEntry } from '@tinycloud/sdk-core';
2
2
  import { z } from 'zod';
3
3
  import { O as OperationSpaceResolver } from './authority-ofwbEqmj.cjs';
4
4
  export { e as evaluateAuthority } from './authority-ofwbEqmj.cjs';
5
- import { T as TinyCloudPosture, O as OperationOperatorType } from './contract-DfyrluLB.cjs';
5
+ import { T as TinyCloudPosture, O as OperationOperatorType } from './contract-zRklonbR.cjs';
6
6
 
7
7
  /** The shared wire schema for SDK PermissionEntry values, including signed ReCap caveats. */
8
8
  declare const PermissionEntrySchema: z.ZodObject<{
@@ -2,7 +2,7 @@ import { PermissionEntry } from '@tinycloud/sdk-core';
2
2
  import { z } from 'zod';
3
3
  import { O as OperationSpaceResolver } from './authority-ofwbEqmj.js';
4
4
  export { e as evaluateAuthority } from './authority-ofwbEqmj.js';
5
- import { T as TinyCloudPosture, O as OperationOperatorType } from './contract-DfyrluLB.js';
5
+ import { T as TinyCloudPosture, O as OperationOperatorType } from './contract-zRklonbR.js';
6
6
 
7
7
  /** The shared wire schema for SDK PermissionEntry values, including signed ReCap caveats. */
8
8
  declare const PermissionEntrySchema: z.ZodObject<{
package/dist/artifacts.js CHANGED
@@ -304,10 +304,10 @@ function evaluateOperationAuthority(granted, required, resolveSpace) {
304
304
  const canonicalRequired = canonicalizeOperationCapabilities(required, resolveSpace);
305
305
  const missing = [];
306
306
  for (const requested of canonicalRequired) {
307
- const covered = canonicalGranted.some(
308
- (candidate) => candidate.service === requested.service && operationSpacesEqual(candidate.space, requested.space) && isCapabilitySubset([requested], [candidate]).subset
307
+ const sameScope = canonicalGranted.filter(
308
+ (candidate) => candidate.service === requested.service && operationSpacesEqual(candidate.space, requested.space)
309
309
  );
310
- if (!covered) missing.push(requested);
310
+ if (!isCapabilitySubset([requested], sameScope).subset) missing.push(requested);
311
311
  }
312
312
  return {
313
313
  satisfied: missing.length === 0,
@@ -381,9 +381,11 @@ function canonicalizeCaveats(caveats) {
381
381
  // src/state.ts
382
382
  import { randomUUID } from "crypto";
383
383
  import { AsyncLocalStorage } from "async_hooks";
384
+ import { readFileSync, readlinkSync } from "fs";
384
385
  import {
385
386
  chmod,
386
387
  link,
388
+ lstat,
387
389
  mkdir,
388
390
  open,
389
391
  readFile,
@@ -395,6 +397,7 @@ import {
395
397
  unlink,
396
398
  writeFile
397
399
  } from "fs/promises";
400
+ import { connect, createServer } from "net";
398
401
  import { homedir } from "os";
399
402
  import { basename, dirname, isAbsolute, join, relative, resolve } from "path";
400
403
  var DEFAULT_LOCK_TIMEOUT_MS = 2e3;
@@ -414,13 +417,171 @@ var TEST_LOCK_MOVED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_MOVED_BARRIER_DIR";
414
417
  var TEST_LOCK_TURN_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_PUBLISH_BARRIER_DIR";
415
418
  var TEST_LOCK_TURN_SETTLE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_SETTLE_BARRIER_DIR";
416
419
  var TEST_LOCK_TURN_COLLECT_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_COLLECT_BARRIER_DIR";
420
+ var TEST_LOCK_LIVENESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_LIVENESS_BARRIER_DIR";
417
421
  function processWideContext(key) {
418
- var _a2;
422
+ var _a3;
419
423
  const registry = globalThis;
420
- return registry[_a2 = Symbol.for(key)] ?? (registry[_a2] = new AsyncLocalStorage());
424
+ return registry[_a3 = Symbol.for(key)] ?? (registry[_a3] = new AsyncLocalStorage());
421
425
  }
422
426
  var invocationStateRoot = processWideContext("tinycloud.operations.invocationStateRoot.v1");
423
427
  var heldProfileLocks = processWideContext("tinycloud.operations.heldProfileLocks.v2");
428
+ var _a;
429
+ var processInstanceId = globalThis[_a = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a] = randomUUID());
430
+ var cachedProcessOrigin;
431
+ function processOrigin() {
432
+ if (cachedProcessOrigin !== void 0) return cachedProcessOrigin;
433
+ if (process.platform !== "linux") return cachedProcessOrigin = null;
434
+ try {
435
+ const bootId = readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
436
+ const pidNamespace = readlinkSync("/proc/self/ns/pid");
437
+ const processStat = readFileSync("/proc/self/stat", "utf8");
438
+ const closingParen = processStat.lastIndexOf(") ");
439
+ const processStartTime = closingParen < 0 ? void 0 : processStat.slice(closingParen + 2).trim().split(/\s+/)[19];
440
+ if (!/^[0-9a-f-]{36}$/.test(bootId) || !/^pid:\[[0-9]+\]$/.test(pidNamespace) || !processStartTime || !/^[0-9]+$/.test(processStartTime)) {
441
+ return cachedProcessOrigin = null;
442
+ }
443
+ return cachedProcessOrigin = { bootId, pidNamespace, processStartTime };
444
+ } catch {
445
+ return cachedProcessOrigin = null;
446
+ }
447
+ }
448
+ function processOwner(liveness) {
449
+ return { instanceId: processInstanceId, ...processOrigin(), ...liveness ? { liveness: liveness.record } : {} };
450
+ }
451
+ var LIVENESS_SOCKET = /^\.live-([0-9a-f]+)-([0-9a-f]+)-[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}(\.new)?$/;
452
+ var LIVENESS_PROBE_TIMEOUT_MS = 1e3;
453
+ function socketDirectory(name) {
454
+ const match = LIVENESS_SOCKET.exec(name);
455
+ return match ? `${match[1]}-${match[2]}` : null;
456
+ }
457
+ async function pinDirectory(directory) {
458
+ const handle = await open(directory, "r");
459
+ try {
460
+ const { dev, ino } = await handle.stat({ bigint: true });
461
+ return {
462
+ identity: `${dev.toString(16)}-${ino.toString(16)}`,
463
+ path: (name) => `/proc/self/fd/${handle.fd}/${name}`,
464
+ close: () => handle.close()
465
+ };
466
+ } catch (error) {
467
+ await handle.close();
468
+ throw error;
469
+ }
470
+ }
471
+ async function bindLiveness(directory) {
472
+ if (!processOrigin()) return null;
473
+ let pinned;
474
+ try {
475
+ pinned = await pinDirectory(directory);
476
+ } catch {
477
+ return null;
478
+ }
479
+ const socket = `.live-${pinned.identity}-${randomUUID()}`;
480
+ const server = createServer((connection) => connection.destroy());
481
+ let bound = false;
482
+ try {
483
+ await new Promise((resolve2, reject) => {
484
+ server.once("error", reject);
485
+ server.listen({ path: pinned.path(`${socket}.new`), exclusive: true }, () => {
486
+ server.off("error", reject);
487
+ resolve2();
488
+ });
489
+ });
490
+ bound = true;
491
+ server.on("error", () => void 0);
492
+ server.unref();
493
+ await rename(pinned.path(`${socket}.new`), pinned.path(socket));
494
+ const stats = await lstat(pinned.path(socket), { bigint: true });
495
+ if (!stats.isSocket()) throw new Error(`${socket} is not a socket`);
496
+ await waitForTestBarrier(TEST_LOCK_LIVENESS_BARRIER_DIR, basename(directory));
497
+ return { record: { socket, dev: String(stats.dev), ino: String(stats.ino) }, path: join(directory, socket), server };
498
+ } catch {
499
+ if (bound) {
500
+ server.close();
501
+ for (const name of [`${socket}.new`, socket]) await unlink(pinned.path(name)).catch(() => void 0);
502
+ }
503
+ return null;
504
+ } finally {
505
+ await pinned.close();
506
+ }
507
+ }
508
+ async function ensureLiveness(acquisition) {
509
+ if (acquisition.held === void 0) acquisition.held = await bindLiveness(acquisition.directory);
510
+ return acquisition.held;
511
+ }
512
+ async function closeLiveness(held) {
513
+ held.server.close();
514
+ await unlink(held.path).catch(() => void 0);
515
+ }
516
+ function parseLiveness(value) {
517
+ if (typeof value !== "object" || value === null) return null;
518
+ const { socket, dev, ino } = value;
519
+ const name = typeof socket === "string" ? LIVENESS_SOCKET.exec(socket) : null;
520
+ 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;
521
+ }
522
+ async function controlIn(pinned, acquisition) {
523
+ const own = await ensureLiveness(acquisition);
524
+ if (!own || socketDirectory(own.record.socket) !== pinned.identity) return null;
525
+ const stats = await lstat(pinned.path(own.record.socket), { bigint: true }).catch(() => null);
526
+ return stats?.isSocket() && String(stats.dev) === own.record.dev && String(stats.ino) === own.record.ino ? own : null;
527
+ }
528
+ async function tryConnect(path) {
529
+ return new Promise((resolve2) => {
530
+ const probe = connect({ path });
531
+ const settle = (outcome) => {
532
+ clearTimeout(timer);
533
+ probe.destroy();
534
+ resolve2(outcome);
535
+ };
536
+ const timer = setTimeout(() => settle("other"), LIVENESS_PROBE_TIMEOUT_MS);
537
+ probe.once("connect", () => settle("connected"));
538
+ probe.once("error", (error) => settle(isErrno(error, "ECONNREFUSED") ? "refused" : "other"));
539
+ });
540
+ }
541
+ async function kernelRefuses(pinned, name, control) {
542
+ if (process.versions.bun !== void 0) return false;
543
+ return await tryConnect(pinned.path(name)) === "refused" && await tryConnect(pinned.path(control.record.socket)) === "connected";
544
+ }
545
+ async function probeLiveness(liveness, acquisition) {
546
+ let pinned;
547
+ try {
548
+ pinned = await pinDirectory(acquisition.directory);
549
+ } catch {
550
+ return "unknown";
551
+ }
552
+ try {
553
+ if (socketDirectory(liveness.socket) !== pinned.identity) return "unknown";
554
+ const control = await controlIn(pinned, acquisition);
555
+ if (!control) return "unknown";
556
+ let stats;
557
+ try {
558
+ stats = await lstat(pinned.path(liveness.socket), { bigint: true });
559
+ } catch (error) {
560
+ return isErrno(error, "ENOENT") ? "gone" : "unknown";
561
+ }
562
+ if (!stats.isSocket() || String(stats.dev) !== liveness.dev || String(stats.ino) !== liveness.ino) return "unknown";
563
+ return await kernelRefuses(pinned, liveness.socket, control) ? "refused" : "unknown";
564
+ } finally {
565
+ await pinned.close();
566
+ }
567
+ }
568
+ async function sweepLiveness(acquisition) {
569
+ if (process.versions.bun !== void 0 || !acquisition.held) return;
570
+ const pinned = await pinDirectory(acquisition.directory);
571
+ try {
572
+ const control = await controlIn(pinned, acquisition);
573
+ if (!control) return;
574
+ for (const name of await readdir(pinned.path("."))) {
575
+ if (name === control.record.socket || socketDirectory(name) !== pinned.identity) continue;
576
+ const stats = await lstat(pinned.path(name)).catch(() => null);
577
+ if (stats?.isSocket() && await kernelRefuses(pinned, name, control)) {
578
+ await unlink(pinned.path(name)).catch(() => void 0);
579
+ }
580
+ }
581
+ } finally {
582
+ await pinned.close();
583
+ }
584
+ }
424
585
  var ProfileLockTimeoutError = class extends Error {
425
586
  /** `detail` names the cause when it is not simply another process holding the lock. */
426
587
  constructor(profile, timeoutMs, detail) {
@@ -585,7 +746,18 @@ async function acquireProfileLock(profile, options) {
585
746
  if (path !== directory || !isErrno(error, "ENOENT")) throw error;
586
747
  });
587
748
  }
588
- const { turn, firstWaitedTurn } = await acquireTurn(profile, turnDirectory, deadline);
749
+ const liveness = { directory: turnDirectory, held: void 0 };
750
+ const closeAcquisitionLiveness = async () => {
751
+ if (liveness.held) await closeLiveness(liveness.held);
752
+ };
753
+ let taken;
754
+ try {
755
+ taken = await acquireTurn(profile, turnDirectory, deadline, liveness);
756
+ } catch (error) {
757
+ await closeAcquisitionLiveness();
758
+ throw error;
759
+ }
760
+ const { turn, firstWaitedTurn } = taken;
589
761
  try {
590
762
  if (options.requireProfile && !await exists(profileConfigPath(profile))) {
591
763
  await rmdir(directory).catch(() => void 0);
@@ -595,7 +767,7 @@ async function acquireProfileLock(profile, options) {
595
767
  const deletedWhileWaiting = typeof deletion?.slot === "number" && deletion.slot >= firstWaitedTurn;
596
768
  while (true) {
597
769
  const token = randomUUID();
598
- if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true)) {
770
+ if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true, liveness.held ?? null)) {
599
771
  return {
600
772
  deletedWhileWaiting,
601
773
  turnSlot: turn.slot,
@@ -603,17 +775,19 @@ async function acquireProfileLock(profile, options) {
603
775
  try {
604
776
  await releaseProfileLock(profile, lockPath, token);
605
777
  } finally {
606
- await releaseTurn(profile, turn);
778
+ await releaseTurn(profile, turn, liveness);
779
+ await closeAcquisitionLiveness();
607
780
  }
608
781
  }
609
782
  };
610
783
  }
611
784
  await signalTestLockContention(profile);
612
- if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
785
+ if (await recoverStaleLock(profile, lockPath, liveness, staleAfterMs)) continue;
613
786
  await waitOrTimeOut(profile, deadline);
614
787
  }
615
788
  } catch (error) {
616
- await releaseTurn(profile, turn);
789
+ await releaseTurn(profile, turn, liveness);
790
+ await closeAcquisitionLiveness();
617
791
  throw error;
618
792
  }
619
793
  }
@@ -627,12 +801,12 @@ async function waitOrTimeOut(profile, deadline, detail) {
627
801
  await sleep(Math.min(deadline.retryMs, deadline.timeoutMs - (Date.now() - deadline.startedAt)));
628
802
  }
629
803
  var TURN = /^(?:0|[1-9][0-9]*)$/;
630
- var _a;
631
- var relinquishedTurns = globalThis[_a = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a] = /* @__PURE__ */ new Set());
804
+ var _a2;
805
+ var relinquishedTurns = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a2] = /* @__PURE__ */ new Set());
632
806
  var MARKER_RETRY_DELAYS_MS = [10, 50, 250];
633
807
  var SETTLE_RETRY_MS = 1e3;
634
808
  var SETTLE_RETRIES = 300;
635
- async function acquireTurn(profile, directory, deadline) {
809
+ async function acquireTurn(profile, directory, deadline, liveness) {
636
810
  let firstWaitedTurn;
637
811
  let problem;
638
812
  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.`;
@@ -657,13 +831,13 @@ async function acquireTurn(profile, directory, deadline) {
657
831
  }
658
832
  firstWaitedTurn ?? (firstWaitedTurn = latest.done ? latest.slot + 1 : latest.slot);
659
833
  if (latest.held && latest.done) {
660
- const turn = await takeTurn(profile, directory, latest);
834
+ const turn = await takeTurn(profile, directory, latest, await ensureLiveness(liveness));
661
835
  if (turn !== null) return { turn, firstWaitedTurn };
662
836
  await waitOrTimeOut(profile, deadline);
663
837
  } else if (latest.done) {
664
838
  problem = damaged(`ends with turn ${latest.slot}, which was never granted`);
665
839
  await waitOrTimeOut(profile, deadline, problem);
666
- } else if (latest.pid === process.pid ? relinquishedTurns.has(latest.token) : !isProcessAlive(latest.pid)) {
840
+ } else if (latest.pid === process.pid && relinquishedTurns.has(latest.token) || !await isRecordedProcessAlive(latest, liveness)) {
667
841
  await waitForTestBarrier(TEST_LOCK_TURN_SETTLE_BARRIER_DIR, profile);
668
842
  await settleAbandonedTurn(directory, latest);
669
843
  relinquishedTurns.delete(latest.token);
@@ -708,6 +882,7 @@ async function readTurn(directory, slot) {
708
882
  const done = await exists(join(path, `${owner.token}.done`));
709
883
  const held = await exists(join(path, `${owner.token}.held`));
710
884
  return {
885
+ ...owner,
711
886
  slot,
712
887
  token: owner.token,
713
888
  pid: owner.pid,
@@ -721,11 +896,11 @@ async function isFinishedTurn(directory, slot, token) {
721
896
  if (turn === "damaged") throw new Error(`Turn ${slot} of the turn lock ${directory} is unreadable.`);
722
897
  return turn !== null && turn.token === token && turn.held && turn.done;
723
898
  }
724
- async function takeTurn(profile, directory, latest) {
899
+ async function takeTurn(profile, directory, latest, liveness) {
725
900
  const token = randomUUID();
726
901
  const slot = latest.slot + 1;
727
902
  const path = join(directory, String(slot));
728
- const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: latest.token };
903
+ const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: latest.token, ...processOwner(liveness) };
729
904
  let published = false;
730
905
  try {
731
906
  published = await publishDirectory(
@@ -779,7 +954,7 @@ async function markTurn(path, token, kind) {
779
954
  }
780
955
  async function createTurnDirectory(directory) {
781
956
  const token = randomUUID();
782
- const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: null };
957
+ const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: null, ...processOwner(null) };
783
958
  await publishDirectory(join(dirname(directory), `.stage-${basename(directory)}-${process.pid}-${token}`), directory, {
784
959
  "0/owner.json": `${JSON.stringify(owner, null, 2)}
785
960
  `,
@@ -803,31 +978,39 @@ async function publishDirectory(stage, target, files, beforeRename) {
803
978
  await rm(stage, { recursive: true, force: true }).catch(() => void 0);
804
979
  }
805
980
  }
806
- async function releaseTurn(profile, turn) {
981
+ async function releaseTurn(profile, turn, liveness) {
807
982
  try {
808
983
  await markTurn(join(turn.directory, String(turn.slot)), turn.token, "done");
809
984
  } catch {
810
985
  settleOwnTurn(turn.directory, turn.slot, turn.token);
811
986
  return;
812
987
  }
813
- await collectTurns(profile, turn).catch(() => void 0);
988
+ await collectTurns(profile, turn, liveness).catch(() => void 0);
814
989
  }
815
- function isAbandonedTurnEntry(name) {
990
+ async function isAbandonedTurnEntry(directory, name, liveness) {
816
991
  if (name.startsWith(".trash-")) return true;
817
992
  const writer = /^\.(?:stage|deleted\.json)[-.]([0-9]+)[-.]/.exec(name)?.[1];
818
- return writer !== void 0 && !isProcessAlive(Number(writer));
993
+ if (writer === void 0) return false;
994
+ if (Number(writer) !== process.pid) return !await isRecordedProcessAlive({ pid: Number(writer) }, liveness);
995
+ return name.startsWith(".stage-") && await isStagedOwnerGone(join(directory, name, "owner.json"), liveness);
819
996
  }
820
- function isAbandonedTurnDirectoryStage(name) {
997
+ async function isAbandonedTurnDirectoryStage(directory, name, liveness) {
821
998
  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];
822
- return writer !== void 0 && !isProcessAlive(Number(writer));
999
+ if (writer === void 0) return false;
1000
+ if (Number(writer) !== process.pid) return !await isRecordedProcessAlive({ pid: Number(writer) }, liveness);
1001
+ return isStagedOwnerGone(join(directory, name, "0", "owner.json"), liveness);
823
1002
  }
824
- async function collectTurns(profile, turn) {
1003
+ async function isStagedOwnerGone(recordPath, liveness) {
1004
+ const owner = await readJson(recordPath).catch(() => null);
1005
+ return typeof owner === "object" && owner !== null && owner.pid === process.pid && !await isRecordedProcessAlive(owner, liveness);
1006
+ }
1007
+ async function collectTurns(profile, turn, liveness) {
825
1008
  for (const [directory, abandoned] of [
826
1009
  [turn.directory, isAbandonedTurnEntry],
827
1010
  [dirname(turn.directory), isAbandonedTurnDirectoryStage]
828
1011
  ]) {
829
- for (const name of (await readdir(directory)).filter(abandoned)) {
830
- await rm(join(directory, name), { recursive: true, force: true }).catch(() => void 0);
1012
+ for (const name of await readdir(directory)) {
1013
+ if (await abandoned(directory, name, liveness)) await rm(join(directory, name), { recursive: true, force: true }).catch(() => void 0);
831
1014
  }
832
1015
  }
833
1016
  const older = (await readdir(turn.directory)).filter((name) => TURN.test(name)).map(Number).filter((slot) => slot < turn.slot).sort((left, right) => left - right);
@@ -845,8 +1028,9 @@ async function collectTurns(profile, turn) {
845
1028
  await rm(trash, { recursive: true, force: true }).catch(() => void 0);
846
1029
  }
847
1030
  }
1031
+ await sweepLiveness(liveness);
848
1032
  }
849
- async function publishProfileLock(profile, lockPath, token, requireProfile) {
1033
+ async function publishProfileLock(profile, lockPath, token, requireProfile, liveness) {
850
1034
  try {
851
1035
  await mkdir(lockPath, { mode: PRIVATE_DIR_MODE });
852
1036
  } catch (error) {
@@ -859,7 +1043,7 @@ async function publishProfileLock(profile, lockPath, token, requireProfile) {
859
1043
  await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);
860
1044
  const ownerPath = join(lockPath, "owner.json");
861
1045
  const staged = join(dirname(lockPath), `.lock-owner-${token}.tmp`);
862
- const owner = `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token }, null, 2)}
1046
+ const owner = `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, ...processOwner(liveness) }, null, 2)}
863
1047
  `;
864
1048
  try {
865
1049
  await writeFile(staged, owner, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
@@ -904,7 +1088,7 @@ async function signalTestLockContention(profile) {
904
1088
  await writeFile(signalPath, `${profile}
905
1089
  `, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
906
1090
  }
907
- async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
1091
+ async function recoverOwnerlessLock(profile, lockPath, liveness, staleAfterMs) {
908
1092
  let aged;
909
1093
  try {
910
1094
  const { mtimeMs } = await stat(lockPath);
@@ -921,7 +1105,7 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
921
1105
  if (!onlyOrphanedClaims && (!aged || recoveryFenced(checkedAt, staleAfterMs))) return false;
922
1106
  for (const name of entries.filter((entry) => RECOVERY_CLAIM.test(entry))) {
923
1107
  const claimPath = join(lockPath, name);
924
- if (await hasLiveRecoveryOwner(claimPath)) {
1108
+ if (await hasLiveRecoveryOwner(claimPath, liveness)) {
925
1109
  if (!await exists(join(lockPath, "owner.json"))) await rename(claimPath, join(lockPath, "owner.json"));
926
1110
  return false;
927
1111
  }
@@ -937,7 +1121,7 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
937
1121
  await removeAgedOwnerFiles(lockPath, staleAfterMs);
938
1122
  return true;
939
1123
  }
940
- async function hasLiveRecoveryOwner(claimPath) {
1124
+ async function hasLiveRecoveryOwner(claimPath, liveness) {
941
1125
  let contents;
942
1126
  try {
943
1127
  contents = await readFile(claimPath, "utf8");
@@ -951,8 +1135,8 @@ async function hasLiveRecoveryOwner(claimPath) {
951
1135
  } catch {
952
1136
  return false;
953
1137
  }
954
- const pid = typeof owner === "object" && owner !== null && "pid" in owner ? owner.pid : null;
955
- return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && isProcessAlive(pid);
1138
+ const record = typeof owner === "object" && owner !== null && "pid" in owner ? owner : null;
1139
+ return typeof record?.pid === "number" && Number.isInteger(record.pid) && record.pid > 0 && await isRecordedProcessAlive(record, liveness);
956
1140
  }
957
1141
  function recoveryFenced(since, staleAfterMs) {
958
1142
  return performance.now() - since >= staleAfterMs / 2;
@@ -969,11 +1153,11 @@ async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
969
1153
  if (aged) await rm(path, { force: true }).catch(() => void 0);
970
1154
  }
971
1155
  }
972
- async function recoverStaleLock(profile, lockPath, staleAfterMs) {
1156
+ async function recoverStaleLock(profile, lockPath, liveness, staleAfterMs) {
973
1157
  const ownerPath = join(lockPath, "owner.json");
974
1158
  const observed = await readFile(ownerPath, "utf8").catch(() => null);
975
- if (observed === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
976
- if (!await isAbandonedOwner(observed, ownerPath, staleAfterMs)) return false;
1159
+ if (observed === null) return recoverOwnerlessLock(profile, lockPath, liveness, staleAfterMs);
1160
+ if (!await isAbandonedOwner(observed, ownerPath, liveness, staleAfterMs)) return false;
977
1161
  await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
978
1162
  const claimPath = join(lockPath, `.recover-${randomUUID()}.json`);
979
1163
  let moved = false;
@@ -1009,7 +1193,7 @@ async function recoverStaleLock(profile, lockPath, staleAfterMs) {
1009
1193
  await removeAgedOwnerFiles(lockPath, staleAfterMs);
1010
1194
  return true;
1011
1195
  }
1012
- async function isAbandonedOwner(observed, ownerPath, staleAfterMs) {
1196
+ async function isAbandonedOwner(observed, ownerPath, liveness, staleAfterMs) {
1013
1197
  let owner;
1014
1198
  try {
1015
1199
  owner = JSON.parse(observed);
@@ -1017,7 +1201,7 @@ async function isAbandonedOwner(observed, ownerPath, staleAfterMs) {
1017
1201
  owner = null;
1018
1202
  }
1019
1203
  if (typeof owner === "object" && owner !== null && "pid" in owner) {
1020
- return isStaleOwner(owner, staleAfterMs);
1204
+ return isStaleOwner(owner, liveness, staleAfterMs);
1021
1205
  }
1022
1206
  const modifiedAt = await stat(ownerPath).then(({ mtimeMs }) => mtimeMs, () => Number.NaN);
1023
1207
  return Date.now() - modifiedAt >= staleAfterMs;
@@ -1039,14 +1223,14 @@ async function waitForTestBarrier(environmentName, profile) {
1039
1223
  }
1040
1224
  }
1041
1225
  }
1042
- function isStaleOwner(owner, staleAfterMs) {
1226
+ async function isStaleOwner(owner, liveness, staleAfterMs) {
1043
1227
  const now = Date.now();
1044
1228
  const createdAt = typeof owner?.createdAt === "string" ? Date.parse(owner.createdAt) : Number.NaN;
1045
1229
  if (!Number.isFinite(createdAt) || now - createdAt < staleAfterMs) return false;
1046
1230
  if (typeof owner?.pid !== "number" || !Number.isInteger(owner.pid) || owner.pid <= 0) {
1047
1231
  return false;
1048
1232
  }
1049
- return !isProcessAlive(owner.pid);
1233
+ return !await isRecordedProcessAlive(owner, liveness);
1050
1234
  }
1051
1235
  async function exists(path) {
1052
1236
  try {
@@ -1057,13 +1241,22 @@ async function exists(path) {
1057
1241
  throw error;
1058
1242
  }
1059
1243
  }
1060
- function isProcessAlive(pid) {
1244
+ async function isRecordedProcessAlive(owner, acquisition) {
1245
+ const pid = owner.pid;
1246
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return true;
1061
1247
  try {
1062
1248
  process.kill(pid, 0);
1063
- return true;
1064
1249
  } catch (error) {
1065
1250
  return !isErrno(error, "ESRCH");
1066
1251
  }
1252
+ 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;
1253
+ const origin = processOrigin();
1254
+ if (!origin || owner.bootId !== origin.bootId || typeof owner.pidNamespace !== "string" || !/^pid:\[[0-9]+\]$/.test(owner.pidNamespace)) return true;
1255
+ if (owner.pidNamespace === origin.pidNamespace) {
1256
+ return typeof owner.processStartTime !== "string" || !/^[0-9]+$/.test(owner.processStartTime) || owner.processStartTime === origin.processStartTime;
1257
+ }
1258
+ const liveness = parseLiveness(owner.liveness);
1259
+ return liveness === null || await probeLiveness(liveness, acquisition) === "unknown";
1067
1260
  }
1068
1261
  function validateProfileName(profile) {
1069
1262
  if (!profile || profile === "." || profile === ".." || profile.includes("/") || profile.includes("\\") || profile.includes("\0")) {