@gajae-code/coding-agent 0.12.4 → 0.12.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.
Files changed (139) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +4 -1
  3. package/dist/types/async/job-manager.d.ts +9 -1
  4. package/dist/types/eval/backend.d.ts +2 -2
  5. package/dist/types/exec/bash-executor.d.ts +10 -8
  6. package/dist/types/extensibility/extensions/runner.d.ts +9 -7
  7. package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
  8. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
  9. package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
  10. package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
  11. package/dist/types/gjc-runtime/team-store.d.ts +0 -4
  12. package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
  13. package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
  14. package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
  15. package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
  16. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
  17. package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +83 -0
  18. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +57 -4
  19. package/dist/types/internal-urls/registry-helpers.d.ts +18 -0
  20. package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
  21. package/dist/types/modes/components/plugin-settings.d.ts +2 -0
  22. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  23. package/dist/types/sdk/broker/broker.d.ts +18 -0
  24. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
  25. package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
  26. package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -2
  27. package/dist/types/sdk/bus/index.d.ts +4 -0
  28. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -3
  29. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
  30. package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
  31. package/dist/types/sdk/host/host.d.ts +23 -2
  32. package/dist/types/sdk/host/reverse-leases.d.ts +7 -4
  33. package/dist/types/session/agent-session.d.ts +5 -4
  34. package/dist/types/session/artifacts.d.ts +8 -5
  35. package/dist/types/session/attempt-record-store.d.ts +19 -0
  36. package/dist/types/session/client-bridge.d.ts +14 -0
  37. package/dist/types/session/internal/managed-session-storage.d.ts +7 -23
  38. package/dist/types/session/messages.d.ts +4 -1
  39. package/dist/types/session/session-manager.d.ts +7 -5
  40. package/dist/types/session/session-storage.d.ts +8 -0
  41. package/dist/types/session/streaming-output.d.ts +32 -0
  42. package/dist/types/task/executor.d.ts +1 -1
  43. package/dist/types/task/index.d.ts +2 -1
  44. package/dist/types/task/receipt.d.ts +3 -0
  45. package/dist/types/task/types.d.ts +194 -0
  46. package/dist/types/task/worktree.d.ts +13 -6
  47. package/dist/types/tools/bash.d.ts +1 -1
  48. package/dist/types/tools/fetch.d.ts +3 -0
  49. package/dist/types/tools/output-meta.d.ts +37 -5
  50. package/dist/types/tools/subagent.d.ts +2 -0
  51. package/dist/types/tools/tool-result.d.ts +1 -0
  52. package/dist/types/utils/artifact-id.d.ts +2 -0
  53. package/dist/types/utils/git.d.ts +1 -0
  54. package/package.json +8 -8
  55. package/scripts/capture-sticky-viewport-showcase.ts +131 -5
  56. package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
  57. package/scripts/verify-sticky-viewport-showcase.ts +620 -93
  58. package/src/async/job-manager.ts +13 -5
  59. package/src/config/model-profiles.ts +8 -0
  60. package/src/config/settings-schema.ts +17 -3
  61. package/src/cursor.ts +20 -13
  62. package/src/defaults/gjc/skills/ultragoal/SKILL.md +5 -2
  63. package/src/eval/backend.ts +2 -2
  64. package/src/exec/bash-executor.ts +425 -151
  65. package/src/extensibility/extensions/runner.ts +80 -6
  66. package/src/extensibility/extensions/wrapper.ts +22 -15
  67. package/src/extensibility/runtime-skill-discovery.ts +4 -6
  68. package/src/gjc-runtime/launch-tmux.ts +309 -293
  69. package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
  70. package/src/gjc-runtime/psmux-detect.ts +52 -6
  71. package/src/gjc-runtime/team-launch.ts +50 -2
  72. package/src/gjc-runtime/team-runtime.ts +716 -251
  73. package/src/gjc-runtime/team-store.ts +25 -7
  74. package/src/gjc-runtime/team-workers.ts +161 -1
  75. package/src/gjc-runtime/tmux-common.ts +16 -4
  76. package/src/gjc-runtime/tmux-gc.ts +37 -5
  77. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
  78. package/src/gjc-runtime/tmux-provider-context.ts +451 -0
  79. package/src/gjc-runtime/tmux-sessions.ts +409 -156
  80. package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
  81. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
  82. package/src/gjc-runtime/ultragoal-review-source.ts +378 -0
  83. package/src/gjc-runtime/ultragoal-runtime.ts +640 -159
  84. package/src/gjc-runtime/workflow-manifest.generated.json +40 -1
  85. package/src/gjc-runtime/workflow-manifest.ts +12 -0
  86. package/src/internal-urls/agent-protocol.ts +98 -38
  87. package/src/internal-urls/artifact-protocol.ts +28 -17
  88. package/src/internal-urls/docs-index.generated.ts +6 -6
  89. package/src/internal-urls/local-protocol.ts +39 -18
  90. package/src/internal-urls/registry-helpers.ts +62 -0
  91. package/src/modes/acp/acp-agent.ts +51 -13
  92. package/src/modes/acp/acp-client-bridge.ts +154 -11
  93. package/src/modes/components/gjc-bundle-settings.ts +2 -0
  94. package/src/modes/components/plugin-settings.ts +5 -0
  95. package/src/modes/components/settings-selector.ts +4 -0
  96. package/src/modes/controllers/selector-controller.ts +1 -0
  97. package/src/prompts/tools/bash.md +2 -2
  98. package/src/prompts/tools/monitor.md +4 -4
  99. package/src/sdk/acp/adapter.ts +200 -9
  100. package/src/sdk/broker/broker.ts +93 -10
  101. package/src/sdk/broker/lifecycle-ledger.ts +227 -11
  102. package/src/sdk/broker/lifecycle.ts +692 -98
  103. package/src/sdk/bus/chat-daemon-control.ts +33 -5
  104. package/src/sdk/bus/index.ts +347 -14
  105. package/src/sdk/bus/telegram-daemon-contract.ts +7 -3
  106. package/src/sdk/bus/telegram-daemon.ts +283 -44
  107. package/src/sdk/bus/topic-registry.ts +133 -10
  108. package/src/sdk/host/host.ts +187 -9
  109. package/src/sdk/host/reverse-leases.ts +249 -42
  110. package/src/sdk/session.ts +8 -6
  111. package/src/session/agent-session.ts +930 -354
  112. package/src/session/artifacts.ts +211 -74
  113. package/src/session/attempt-record-store.ts +106 -0
  114. package/src/session/client-bridge.ts +15 -0
  115. package/src/session/internal/managed-session-scope.ts +332 -198
  116. package/src/session/internal/managed-session-storage.ts +162 -450
  117. package/src/session/messages.ts +32 -7
  118. package/src/session/session-manager.ts +31 -28
  119. package/src/session/session-storage.ts +190 -53
  120. package/src/session/streaming-output.ts +198 -40
  121. package/src/task/executor.ts +5 -2
  122. package/src/task/index.ts +277 -64
  123. package/src/task/provider-retry-status.ts +4 -1
  124. package/src/task/receipt.ts +26 -2
  125. package/src/task/render.ts +42 -1
  126. package/src/task/types.ts +52 -0
  127. package/src/task/worktree.ts +122 -43
  128. package/src/tools/bash.ts +818 -239
  129. package/src/tools/eval.ts +99 -4
  130. package/src/tools/fetch.ts +77 -15
  131. package/src/tools/find.ts +7 -3
  132. package/src/tools/monitor.ts +7 -1
  133. package/src/tools/output-meta.ts +706 -116
  134. package/src/tools/search.ts +9 -4
  135. package/src/tools/subagent-render.ts +5 -2
  136. package/src/tools/subagent.ts +8 -0
  137. package/src/tools/tool-result.ts +1 -0
  138. package/src/utils/artifact-id.ts +6 -0
  139. package/src/utils/git.ts +2 -0
@@ -6,10 +6,8 @@ import {
6
6
  applyOwnerOnlyFdSecurity,
7
7
  applyOwnerOnlyPathSecurity,
8
8
  exactRemoveDirectoryTree,
9
- exactReplacePath,
10
9
  exactUnlink,
11
10
  type NativeDirectoryTreeSnapshot,
12
- type NativeExactUnlinkResult,
13
11
  type NativeOwnerOnlySecurityResult,
14
12
  openRecoveryFsRoot,
15
13
  type RecoveryFsRoot,
@@ -36,6 +34,7 @@ const LOCK_LEASE_MS = 60_000;
36
34
  const LOCK_HEARTBEAT_MS = 10_000;
37
35
  const LOCK_WAIT_MS = 5_000;
38
36
 
37
+ const LOCK_STALE_RECHECK_MS = 100;
39
38
  export class ManagedPublishError extends Error {
40
39
  readonly classification:
41
40
  | "destination_conflict"
@@ -45,14 +44,12 @@ export class ManagedPublishError extends Error {
45
44
  | "identity_mismatch"
46
45
  | "io_error"
47
46
  | "durability_failed";
48
- readonly code: ManagedPublishError["classification"];
49
47
  readonly diagnostic: string;
50
48
 
51
49
  constructor(classification: ManagedPublishError["classification"], outcome: NativePublishOutcome) {
52
50
  super(classification);
53
51
  this.name = "ManagedPublishError";
54
52
  this.classification = classification;
55
- this.code = classification;
56
53
  this.diagnostic = formatNativePublishDiagnostic(outcome);
57
54
  }
58
55
  }
@@ -92,6 +89,7 @@ function sameDirectoryTreeSnapshotAfterRename(
92
89
  entry.kind === other.kind &&
93
90
  entry.dev === other.dev &&
94
91
  entry.ino === other.ino &&
92
+ entry.nlink === other.nlink &&
95
93
  entry.size === other.size &&
96
94
  entry.mtimeNs === other.mtimeNs &&
97
95
  (entry.relativePath === "" || entry.ctimeNs === other.ctimeNs) &&
@@ -137,24 +135,15 @@ export interface ManagedStorageLock {
137
135
  assertOwned(): void;
138
136
  release(): Promise<void>;
139
137
  }
140
- export interface ManagedStorageLockSync {
141
- path: string;
142
- attemptId: string;
143
- assertOwned(): void;
144
- release(): void;
145
- }
146
138
 
147
139
  export interface ManagedFileSnapshot {
148
140
  bytes: Buffer;
149
141
  identity: { dev: bigint; ino: bigint; size: number; mtimeNs: bigint; ctimeNs: bigint; sha256: string };
150
142
  }
151
143
 
152
- export const MANAGED_SESSION_SECURITY_CODES = [
153
- "acl_denied",
154
- "acl_io_error",
155
- "acl_present",
156
- "acl_malformed",
157
- "acl_unknown",
144
+ const ACL_FAILURE_CODES = new Set(["acl_denied", "acl_io_error", "acl_present", "acl_malformed", "acl_unknown"]);
145
+ const ACL_CLEAR_EVIDENCE = new Set(["cleared", "already_absent", "unsupported", "not_run"]);
146
+ const GENERAL_FAILURE_CODES = new Set([
158
147
  "acl_unavailable",
159
148
  "acl_apply_failed",
160
149
  "acl_verify_failed",
@@ -167,52 +156,7 @@ export const MANAGED_SESSION_SECURITY_CODES = [
167
156
  "owner_mismatch",
168
157
  "mode_mismatch",
169
158
  "io_error",
170
- ] as const;
171
- export type ManagedSessionSecurityCode = (typeof MANAGED_SESSION_SECURITY_CODES)[number];
172
- const ACL_FAILURE_CODES = new Set<ManagedSessionSecurityCode>([
173
- "acl_denied",
174
- "acl_io_error",
175
- "acl_present",
176
- "acl_malformed",
177
- "acl_unknown",
178
159
  ]);
179
- const ACL_CLEAR_EVIDENCE = new Set(["cleared", "already_absent", "unsupported", "not_run"]);
180
- const GENERAL_FAILURE_CODES = new Set<ManagedSessionSecurityCode>(MANAGED_SESSION_SECURITY_CODES);
181
- const LOCK_OWNERSHIP_LOSS_CODES = new Set<ManagedSessionSecurityCode>([
182
- "not_found",
183
- "not_directory",
184
- "reparse_point",
185
- "identity_mismatch",
186
- ]);
187
-
188
- function lockDescriptorOwnershipLost(error: unknown): boolean {
189
- const code = managedSecurityFailureClassification(error);
190
- return code !== undefined && LOCK_OWNERSHIP_LOSS_CODES.has(code as ManagedSessionSecurityCode);
191
- }
192
-
193
- export class ManagedSessionSecurityError extends Error {
194
- readonly code: ManagedSessionSecurityCode;
195
-
196
- constructor(code: ManagedSessionSecurityCode) {
197
- super(code);
198
- this.name = "ManagedSessionSecurityError";
199
- this.code = code;
200
- }
201
- }
202
-
203
- export function managedSessionSecurityCode(error: unknown): ManagedSessionSecurityCode | undefined {
204
- if (!(error instanceof Error) || typeof (error as unknown as { code?: unknown }).code !== "string") return undefined;
205
- const code = (error as unknown as { code: string }).code;
206
- return GENERAL_FAILURE_CODES.has(code as ManagedSessionSecurityCode)
207
- ? (code as ManagedSessionSecurityCode)
208
- : undefined;
209
- }
210
-
211
- function managedSessionSecurityErrorForCode(code: string | undefined): ManagedSessionSecurityError {
212
- return new ManagedSessionSecurityError(
213
- GENERAL_FAILURE_CODES.has(code as ManagedSessionSecurityCode) ? (code as ManagedSessionSecurityCode) : "io_error",
214
- );
215
- }
216
160
 
217
161
  function hasOnlyKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
218
162
  return Object.keys(value).every(key => allowed.includes(key));
@@ -230,11 +174,10 @@ export function validateNativeSecurityResult(
230
174
  const result = value as Record<string, unknown>;
231
175
  if (result.ok === false) {
232
176
  if (typeof result.code !== "string") throw new Error("Malformed owner-only security failure");
233
- const code = result.code as ManagedSessionSecurityCode;
234
- if (!ACL_FAILURE_CODES.has(code) && !GENERAL_FAILURE_CODES.has(code)) {
177
+ if (!ACL_FAILURE_CODES.has(result.code) && !GENERAL_FAILURE_CODES.has(result.code)) {
235
178
  throw new Error("Unknown owner-only security failure code");
236
179
  }
237
- if (ACL_FAILURE_CODES.has(code)) {
180
+ if (ACL_FAILURE_CODES.has(result.code)) {
238
181
  if (
239
182
  (result.operation !== "clear" && result.operation !== "query") ||
240
183
  (result.attribute !== "access" && result.attribute !== "default")
@@ -455,7 +398,7 @@ class ManagedSecurityError extends Error {
455
398
  }
456
399
 
457
400
  export function managedSecurityFailureClassification(error: unknown): string | undefined {
458
- return error instanceof ManagedSecurityError ? error.getClassification() : managedSessionSecurityCode(error);
401
+ return error instanceof ManagedSecurityError ? error.getClassification() : undefined;
459
402
  }
460
403
 
461
404
  function securityError(pathname: string, result: NativeSecurity): Error {
@@ -475,11 +418,10 @@ function windowsExistingVerifyFirst(policy: ManagedSessionSecurityPolicy): boole
475
418
 
476
419
  function assertManagedPathIdentity(pathname: string, kind: "directory" | "file", expected: fs.BigIntStats): void {
477
420
  const current = fs.lstatSync(pathname, { bigint: true });
478
- if (current.isSymbolicLink()) throw new ManagedSessionSecurityError("reparse_point");
421
+ if (current.isSymbolicLink()) throw new Error("reparse_point");
479
422
  const expectedKind = kind === "directory" ? current.isDirectory() : current.isFile();
480
- if (!expectedKind) throw new ManagedSessionSecurityError("not_directory");
481
- if (current.dev !== expected.dev || current.ino !== expected.ino)
482
- throw new ManagedSessionSecurityError("identity_mismatch");
423
+ if (!expectedKind) throw new Error(kind === "directory" ? "not_directory" : "not_file");
424
+ if (current.dev !== expected.dev || current.ino !== expected.ino) throw new Error("identity_mismatch");
483
425
  }
484
426
 
485
427
  function verifyExistingManagedPathSecurity(
@@ -499,7 +441,7 @@ function verifyExistingManagedPathSecurity(
499
441
  function secureExistingManagedDirectory(pathname: string, kind: "directory" | "file"): void {
500
442
  const named = fs.lstatSync(pathname, { bigint: true });
501
443
  const safeKind = kind === "directory" ? named.isDirectory() : named.isFile();
502
- if (!safeKind || named.isSymbolicLink()) throw new ManagedSessionSecurityError("reparse_point");
444
+ if (!safeKind || named.isSymbolicLink()) throw new Error(`Unsafe managed ${kind}: ${pathname}`);
503
445
  const verified = validateNativeSecurityResult(
504
446
  verifyOwnerOnlyPathSecurityExpected(pathname, kind, named.dev, named.ino),
505
447
  "verify",
@@ -537,6 +479,7 @@ export class ManagedSessionDescendantStore {
537
479
  /** Logical profile root inherited by nested managed session destinations. */
538
480
  readonly #profileAgentDir: string;
539
481
  readonly #subtreeRoot: ManagedDirectoryRoot;
482
+ #portableMutationFence?: ManagedStorageLock;
540
483
 
541
484
  constructor(
542
485
  root: ManagedDirectoryRoot,
@@ -605,6 +548,40 @@ export class ManagedSessionDescendantStore {
605
548
  return this.#policy;
606
549
  }
607
550
 
551
+ get hasRetainedAuthority(): boolean {
552
+ return this.#authority !== undefined;
553
+ }
554
+
555
+ async acquireMutationFence(name: string): Promise<ManagedStorageLock | undefined> {
556
+ if (this.#authority) return undefined;
557
+ if (!/^[a-zA-Z0-9_.-]+$/.test(name)) throw new Error("invalid_mutation_fence");
558
+ this.#assertBound();
559
+ if (this.#portableMutationFence) throw new Error("migration_busy");
560
+ const lock = await acquireManagedLock(this.#baseDir, `.gjc-${name}`, this.#subtreeRoot, this.#policy);
561
+ try {
562
+ this.#assertBound();
563
+ lock.assertOwned();
564
+ } catch (error) {
565
+ await lock.release();
566
+ throw error;
567
+ }
568
+ this.#portableMutationFence = lock;
569
+ return {
570
+ path: lock.path,
571
+ attemptId: lock.attemptId,
572
+ assertOwned: () => {
573
+ this.#assertBound();
574
+ lock.assertOwned();
575
+ },
576
+ release: async () => {
577
+ try {
578
+ await lock.release();
579
+ } finally {
580
+ if (this.#portableMutationFence === lock) this.#portableMutationFence = undefined;
581
+ }
582
+ },
583
+ };
584
+ }
608
585
  get profileAgentDir(): string {
609
586
  return this.#profileAgentDir;
610
587
  }
@@ -659,14 +636,15 @@ export class ManagedSessionDescendantStore {
659
636
  if (this.#authority) {
660
637
  if (this.#authorityBaseDir === this.#baseDir) {
661
638
  const verified = this.#authority.verifyOwnerOnlyDirectory();
662
- if (!verified.ok) throw managedSessionSecurityErrorForCode(verified.code);
639
+ if (!verified.ok) throw new Error(verified.code ?? "acl_verify_failed");
663
640
  } else {
664
641
  this.#assertBound();
665
642
  return;
666
643
  }
667
644
  } else {
668
645
  const named = fs.lstatSync(this.#baseDir, { bigint: true });
669
- if (!named.isDirectory() || named.isSymbolicLink()) throw new ManagedSessionSecurityError("reparse_point");
646
+ if (!named.isDirectory() || named.isSymbolicLink())
647
+ throw new Error(`Unsafe managed directory: ${this.#baseDir}`);
670
648
  if (windowsExistingVerifyFirst(this.#policy))
671
649
  verifyExistingManagedPathSecurity(this.#baseDir, "directory", named);
672
650
  else {
@@ -681,7 +659,18 @@ export class ManagedSessionDescendantStore {
681
659
  this.#assertBound();
682
660
  }
683
661
  #assertBound(): void {
684
- if (!this.#authority) return;
662
+ if (!this.#authority) {
663
+ const named = fs.lstatSync(this.#baseDir, { bigint: true });
664
+ if (
665
+ !named.isDirectory() ||
666
+ named.isSymbolicLink() ||
667
+ named.dev !== this.#subtreeRoot.dev ||
668
+ named.ino !== this.#subtreeRoot.ino
669
+ ) {
670
+ throw new Error("Managed descendant root binding changed");
671
+ }
672
+ return;
673
+ }
685
674
  const named = fs.lstatSync(this.#baseDir, { bigint: true });
686
675
  const retained =
687
676
  this.#authorityBaseDir === this.#baseDir
@@ -727,24 +716,41 @@ export class ManagedSessionDescendantStore {
727
716
  }
728
717
 
729
718
  async publishNoReplace(relativePath: string, bytes: Uint8Array): Promise<void> {
719
+ this.#assertBound();
720
+ this.#portableMutationFence?.assertOwned();
730
721
  const resolved = this.#resolve(relativePath);
731
722
  if (this.#authority) {
732
- this.#assertBound();
733
723
  this.#publishRetainedNoReplace(this.#relative(resolved), bytes);
734
724
  this.#assertBound();
735
725
  return;
736
726
  }
737
- await publishManagedFileNoReplace(resolved, bytes, undefined, this.#root, this.#policy);
727
+ await publishManagedFileNoReplace(
728
+ resolved,
729
+ bytes,
730
+ this.#portableMutationFence ? () => this.#portableMutationFence?.assertOwned() : undefined,
731
+ this.#subtreeRoot,
732
+ this.#policy,
733
+ );
734
+ this.#assertBound();
735
+ this.#portableMutationFence?.assertOwned();
738
736
  }
739
737
 
740
738
  publishNoReplaceSync(relativePath: string, bytes: Uint8Array): void {
739
+ this.#assertBound();
740
+ this.#portableMutationFence?.assertOwned();
741
741
  const resolved = this.#resolve(relativePath);
742
742
  if (!this.#authority) {
743
- publishManagedFileNoReplaceSync(resolved, bytes, this.#root, this.#policy);
743
+ publishManagedFileNoReplaceSync(
744
+ resolved,
745
+ bytes,
746
+ this.#subtreeRoot,
747
+ this.#policy,
748
+ this.#portableMutationFence ? () => this.#portableMutationFence?.assertOwned() : undefined,
749
+ );
744
750
  this.#assertBound();
751
+ this.#portableMutationFence?.assertOwned();
745
752
  return;
746
753
  }
747
- this.#assertBound();
748
754
  this.#publishRetainedNoReplace(this.#relative(resolved), bytes);
749
755
  this.#assertBound();
750
756
  }
@@ -753,13 +759,7 @@ export class ManagedSessionDescendantStore {
753
759
  this.#assertBound();
754
760
  const resolved = this.#resolve(relativePath);
755
761
  if (!this.#authority) {
756
- if (process.platform === "win32") {
757
- const existing = this.readExpected(relativePath);
758
- if (existing) replaceManagedFileExactSync(resolved, bytes, existing, this.#subtreeRoot);
759
- else await publishManagedFileNoReplace(resolved, bytes, undefined, this.#subtreeRoot, this.#policy);
760
- } else {
761
- await replaceManagedFile(resolved, bytes, this.#subtreeRoot, this.#policy);
762
- }
762
+ await replaceManagedFile(resolved, bytes, this.#subtreeRoot, this.#policy);
763
763
  this.#assertBound();
764
764
  return;
765
765
  }
@@ -768,17 +768,39 @@ export class ManagedSessionDescendantStore {
768
768
  this.#assertBound();
769
769
  }
770
770
 
771
+ replaceExpected(relativePath: string, bytes: Uint8Array, expected: ManagedFileSnapshot): void {
772
+ this.#assertBound();
773
+ const resolved = this.#resolve(relativePath);
774
+ if (!this.#authority) {
775
+ const fence = this.#portableMutationFence;
776
+ if (!fence) throw new Error("managed_replace_exact_unavailable");
777
+ fence.assertOwned();
778
+ const current = captureManagedFileNoFollow(resolved);
779
+ if (!sameIdentity(current.identity, expected.identity) || current.identity.sha256 !== expected.identity.sha256)
780
+ throw new Error("managed_replace_identity_mismatch");
781
+ replaceManagedFileSync(resolved, bytes, this.#subtreeRoot, this.#policy, () => fence.assertOwned());
782
+ fence.assertOwned();
783
+ return;
784
+ }
785
+ const replaced = (this.#authority as RecoveryFsRoot & RetainedManagedReplacer).replaceManaged(
786
+ this.#relative(resolved),
787
+ bytes,
788
+ expected.identity.dev.toString(),
789
+ expected.identity.ino.toString(),
790
+ String(expected.identity.size),
791
+ expected.identity.mtimeNs.toString(),
792
+ expected.identity.ctimeNs.toString(),
793
+ expected.identity.sha256,
794
+ );
795
+ if (!replaced.ok) throw new Error(replaced.code ?? "managed_replace_failed");
796
+ this.#assertBound();
797
+ }
798
+
771
799
  replaceSync(relativePath: string, bytes: Uint8Array): void {
772
800
  this.#assertBound();
773
801
  const resolved = this.#resolve(relativePath);
774
802
  if (!this.#authority) {
775
- if (process.platform === "win32") {
776
- const existing = this.readExpected(relativePath);
777
- if (existing) replaceManagedFileExactSync(resolved, bytes, existing, this.#subtreeRoot);
778
- else publishManagedFileNoReplaceSync(resolved, bytes, this.#subtreeRoot, this.#policy);
779
- } else {
780
- replaceManagedFileSync(resolved, bytes, this.#subtreeRoot, this.#policy);
781
- }
803
+ replaceManagedFileSync(resolved, bytes, this.#subtreeRoot, this.#policy);
782
804
  this.#assertBound();
783
805
  return;
784
806
  }
@@ -1225,7 +1247,7 @@ export class ManagedSessionDescendantStore {
1225
1247
  }
1226
1248
 
1227
1249
  function secureFileDescriptor(pathname: string, fd: number, operation: "apply" | "verify"): void {
1228
- if (process.platform !== "linux" && process.platform !== "win32") {
1250
+ if (process.platform !== "linux") {
1229
1251
  if (operation === "apply") secure(pathname, "file");
1230
1252
  else {
1231
1253
  const verified = validateNativeSecurityResult(verifyOwnerOnlyPathSecurity(pathname, "file"), "verify", "file");
@@ -1309,9 +1331,11 @@ function isCtimeOnlyIdentityMismatch(
1309
1331
  return sameStableIdentityIgnoringCtime(observed, expected) && observed.ctimeNs !== expected.ctimeNs;
1310
1332
  }
1311
1333
 
1312
- function parseLockBytes(bytes: Uint8Array): LockRecord | undefined {
1334
+ function parseLock(pathname: string): LockRecord | undefined {
1313
1335
  try {
1314
- const value: unknown = JSON.parse(Buffer.from(bytes).toString("utf8"));
1336
+ const stat = fs.lstatSync(pathname);
1337
+ if (!stat.isFile() || stat.isSymbolicLink()) return undefined;
1338
+ const value: unknown = JSON.parse(fs.readFileSync(pathname, "utf8"));
1315
1339
  if (!value || typeof value !== "object") return undefined;
1316
1340
  const record = value as Partial<LockRecord>;
1317
1341
  return typeof record.attemptId === "string" &&
@@ -1327,16 +1351,6 @@ function parseLockBytes(bytes: Uint8Array): LockRecord | undefined {
1327
1351
  }
1328
1352
  }
1329
1353
 
1330
- function parseLock(pathname: string): LockRecord | undefined {
1331
- try {
1332
- const stat = fs.lstatSync(pathname);
1333
- if (!stat.isFile() || stat.isSymbolicLink()) return undefined;
1334
- return parseLockBytes(fs.readFileSync(pathname));
1335
- } catch {
1336
- return undefined;
1337
- }
1338
- }
1339
-
1340
1354
  function ownerDefinitelyGone(record: LockRecord): boolean {
1341
1355
  if (record.bootId && bootId() && record.bootId !== bootId()) return true;
1342
1356
  try {
@@ -1357,82 +1371,6 @@ function writeLockDescriptor(fd: number, record: LockRecord): void {
1357
1371
  function sameFileIdentity(left: fs.BigIntStats, right: fs.BigIntStats): boolean {
1358
1372
  return left.dev === right.dev && left.ino === right.ino;
1359
1373
  }
1360
- function exactLockRetirementAccepted(lockPath: string, removed: NativeExactUnlinkResult): boolean {
1361
- if (removed.ok) return true;
1362
- if (removed.code !== "cleanup_pending") return false;
1363
- const lockDirectory = path.dirname(path.resolve(lockPath));
1364
- const authority = [
1365
- removed.detachedPath,
1366
- removed.retainedSuccessorPath,
1367
- removed.retainedPlaceholderPath,
1368
- removed.retainedUnknownPath,
1369
- ].find(pathname => {
1370
- if (typeof pathname !== "string" || pathname.length === 0 || !path.isAbsolute(pathname)) return false;
1371
- const resolved = path.resolve(pathname);
1372
- if (resolved === path.resolve(lockPath) || path.dirname(resolved) !== lockDirectory) return false;
1373
- if (!path.basename(resolved).startsWith(".gjc-")) return false;
1374
- try {
1375
- return !fs.lstatSync(resolved).isSymbolicLink();
1376
- } catch {
1377
- return false;
1378
- }
1379
- });
1380
- try {
1381
- fs.lstatSync(lockPath);
1382
- return false;
1383
- } catch (error) {
1384
- return !!authority && (error as NodeJS.ErrnoException).code === "ENOENT";
1385
- }
1386
- }
1387
-
1388
- function unlinkManagedLockSnapshotExact(lockPath: string, snapshot: ManagedFileSnapshot): void {
1389
- const removed = exactUnlink(lockPath, {
1390
- dev: snapshot.identity.dev,
1391
- ino: snapshot.identity.ino,
1392
- size: BigInt(snapshot.identity.size),
1393
- mtimeNs: snapshot.identity.mtimeNs,
1394
- sha256: snapshot.identity.sha256,
1395
- quarantineName: `.gjc-lock-retire-${process.pid}-${randomUUID()}`,
1396
- });
1397
- if (!exactLockRetirementAccepted(lockPath, removed)) throw new Error("migration_busy");
1398
- fsyncDirectory(path.dirname(lockPath));
1399
- }
1400
-
1401
- function retireCreatedManagedLockExact(lockPath: string, fd: number): void {
1402
- const opened = fs.fstatSync(fd, { bigint: true });
1403
- const snapshot = captureManagedFileNoFollow(lockPath);
1404
- if (snapshot.identity.dev !== opened.dev || snapshot.identity.ino !== opened.ino) throw new Error("migration_busy");
1405
- unlinkManagedLockSnapshotExact(lockPath, snapshot);
1406
- }
1407
-
1408
- /**
1409
- * Retire a lock only after exact identity and attempt fencing. A typed
1410
- * `cleanup_pending` result is terminal lock-authority retirement, not a claim
1411
- * that retained bytes were deleted: accept it only when native exact-unlink
1412
- * supplies concrete internal authority in this lock directory and the canonical
1413
- * lock name is absent, so a successor may acquire while quarantine evidence stays
1414
- * retained for recovery.
1415
- */
1416
- function retireManagedLockExact(
1417
- lockPath: string,
1418
- expected: { lockIdentity?: fs.BigIntStats; attemptId: string },
1419
- ): void {
1420
- let snapshot: ManagedFileSnapshot;
1421
- try {
1422
- snapshot = captureManagedFileNoFollow(lockPath);
1423
- } catch {
1424
- throw new Error("migration_busy");
1425
- }
1426
- const record = parseLockBytes(snapshot.bytes);
1427
- if (
1428
- !record ||
1429
- record.attemptId !== expected.attemptId ||
1430
- (expected.lockIdentity &&
1431
- (snapshot.identity.dev !== expected.lockIdentity.dev || snapshot.identity.ino !== expected.lockIdentity.ino))
1432
- )
1433
- throw new Error("migration_busy");
1434
- unlinkManagedLockSnapshotExact(lockPath, snapshot);
1435
- }
1436
1374
 
1437
1375
  /** Create a managed directory and fail closed unless its owner-only mode/ACL verifies. */
1438
1376
  export function ensureManagedDirectory(
@@ -1587,6 +1525,7 @@ export function publishManagedFileNoReplaceSync(
1587
1525
  bytes: Uint8Array,
1588
1526
  root?: ManagedDirectoryRoot,
1589
1527
  policy: ManagedSessionSecurityPolicy = "default",
1528
+ assertOwned?: () => void,
1590
1529
  ): void {
1591
1530
  const parent = path.dirname(destination);
1592
1531
  ensureManagedDirectory(parent, root, policy);
@@ -1597,6 +1536,7 @@ export function publishManagedFileNoReplaceSync(
1597
1536
  let outcome: NativePublishOutcome | undefined;
1598
1537
 
1599
1538
  try {
1539
+ assertOwned?.();
1600
1540
  fd = fs.openSync(
1601
1541
  staging,
1602
1542
  fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW,
@@ -1610,7 +1550,9 @@ export function publishManagedFileNoReplaceSync(
1610
1550
  const staged = fs.fstatSync(fd, { bigint: true });
1611
1551
  stagingIdentity = { dev: staged.dev, ino: staged.ino };
1612
1552
 
1553
+ assertOwned?.();
1613
1554
  outcome = classifyNativePublishOutcome(renameNoReplacePath(staging, destination));
1555
+ assertOwned?.();
1614
1556
  if (!outcome.ok) throw publishFailure(outcome);
1615
1557
 
1616
1558
  const named = fs.lstatSync(destination, { bigint: true });
@@ -1640,130 +1582,13 @@ export function publishManagedFileNoReplaceSync(
1640
1582
  if (failure !== undefined) throw failure;
1641
1583
  }
1642
1584
 
1643
- /**
1644
- * Atomically replace a managed file only when the destination is still the
1645
- * exact captured object. Unlike `replaceManagedFileSync`, this never performs
1646
- * an unconditional pathname rename.
1647
- */
1648
- export function replaceManagedFileExactSync(
1649
- destination: string,
1650
- bytes: Uint8Array,
1651
- expected: Pick<ManagedFileSnapshot, "identity">,
1652
- root: ManagedDirectoryRoot,
1653
- ): void {
1654
- assertManagedDirectoryRoot(root);
1655
- managedRelativePath(root, destination);
1656
- if (process.platform === "win32") {
1657
- const parent = path.dirname(destination);
1658
- const lock = acquireManagedLockSync(
1659
- parent,
1660
- `.${path.basename(destination)}.replace`,
1661
- root,
1662
- "windows-existing-verify-first",
1663
- );
1664
- const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.replacement`);
1665
- let fd: number | undefined;
1666
- let stagedIdentity: { dev: bigint; ino: bigint } | undefined;
1667
- let publicationUncertain = false;
1668
- try {
1669
- lock.assertOwned();
1670
- const current = captureManagedFileNoFollow(destination);
1671
- if (!sameIdentity(current.identity, expected.identity) || current.identity.sha256 !== expected.identity.sha256)
1672
- throw new Error("managed_replace_identity_mismatch");
1673
- fd = fs.openSync(
1674
- staging,
1675
- fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW,
1676
- 0o600,
1677
- );
1678
- secureFileDescriptor(staging, fd, "apply");
1679
- let offset = 0;
1680
- while (offset < bytes.byteLength) offset += fs.writeSync(fd, bytes, offset, bytes.byteLength - offset);
1681
- fs.fsyncSync(fd);
1682
- secureFileDescriptor(staging, fd, "verify");
1683
- const staged = fs.fstatSync(fd, { bigint: true });
1684
- stagedIdentity = { dev: staged.dev, ino: staged.ino };
1685
- fs.closeSync(fd);
1686
- fd = undefined;
1687
- lock.assertOwned();
1688
- const outcome = exactReplacePath(staging, destination, {
1689
- dev: expected.identity.dev,
1690
- ino: expected.identity.ino,
1691
- size: BigInt(expected.identity.size),
1692
- mtimeNs: expected.identity.mtimeNs,
1693
- sha256: expected.identity.sha256,
1694
- });
1695
- if (!outcome.ok) {
1696
- publicationUncertain = true;
1697
- throw new Error(
1698
- outcome.detachedPath === staging
1699
- ? "managed_replace_publication_pending"
1700
- : (outcome.code ?? "managed_replace_identity_mismatch"),
1701
- );
1702
- }
1703
- lock.assertOwned();
1704
- const published = fs.lstatSync(destination, { bigint: true });
1705
- if (
1706
- !published.isFile() ||
1707
- published.isSymbolicLink() ||
1708
- published.dev !== staged.dev ||
1709
- published.ino !== staged.ino
1710
- ) {
1711
- publicationUncertain = true;
1712
- throw new Error("destination_identity_changed");
1713
- }
1714
- secureExistingManagedDirectory(destination, "file");
1715
- } finally {
1716
- if (fd !== undefined) fs.closeSync(fd);
1717
- if (stagedIdentity && !publicationUncertain) {
1718
- try {
1719
- const named = fs.lstatSync(staging, { bigint: true });
1720
- if (named.dev === stagedIdentity.dev && named.ino === stagedIdentity.ino) fs.unlinkSync(staging);
1721
- } catch (error) {
1722
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") publicationUncertain = true;
1723
- }
1724
- }
1725
- lock.release();
1726
- }
1727
- return;
1728
- }
1729
- const parent = path.dirname(destination);
1730
- const parentStat = fs.lstatSync(parent, { bigint: true });
1731
- if (!parentStat.isDirectory() || parentStat.isSymbolicLink()) throw new Error("managed_replace_parent_unavailable");
1732
- const authority = openRecoveryFsRoot(parent);
1733
- try {
1734
- const retained = authority.identity();
1735
- if (
1736
- !retained.ok ||
1737
- !retained.identity ||
1738
- retained.identity.dev !== parentStat.dev.toString() ||
1739
- retained.identity.ino !== parentStat.ino.toString()
1740
- )
1741
- throw new Error("managed_replace_parent_identity_changed");
1742
- const verified = authority.verifyOwnerOnlyDirectory();
1743
- if (!verified.ok) throw new Error(verified.code ?? "managed_replace_parent_unavailable");
1744
- const replaced = authority.replaceManaged(
1745
- path.basename(destination),
1746
- bytes,
1747
- expected.identity.dev.toString(),
1748
- expected.identity.ino.toString(),
1749
- expected.identity.size.toString(),
1750
- expected.identity.mtimeNs.toString(),
1751
- expected.identity.ctimeNs.toString(),
1752
- expected.identity.sha256,
1753
- );
1754
- if (!replaced.ok) throw new Error(replaced.code ?? "managed_replace_identity_mismatch");
1755
- const synced = authority.fsync();
1756
- if (!synced.ok) throw new Error(synced.code ?? "managed_replace_fsync_failed");
1757
- } finally {
1758
- authority.close();
1759
- }
1760
- }
1761
1585
  /** Replace one managed regular file while retaining the secured staging fd through publication. */
1762
- function replaceManagedFileSync(
1586
+ export function replaceManagedFileSync(
1763
1587
  destination: string,
1764
1588
  bytes: Uint8Array,
1765
1589
  root: ManagedDirectoryRoot,
1766
1590
  policy: ManagedSessionSecurityPolicy = "default",
1591
+ assertFence?: () => void,
1767
1592
  ): void {
1768
1593
  const parent = path.dirname(destination);
1769
1594
  ensureManagedDirectory(parent, root, policy);
@@ -1771,6 +1596,7 @@ function replaceManagedFileSync(
1771
1596
  let fd: number | undefined;
1772
1597
  let stagedIdentity: { dev: bigint; ino: bigint } | undefined;
1773
1598
  try {
1599
+ assertFence?.();
1774
1600
  fd = fs.openSync(
1775
1601
  staging,
1776
1602
  fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW,
@@ -1784,7 +1610,9 @@ function replaceManagedFileSync(
1784
1610
  const staged = fs.fstatSync(fd, { bigint: true });
1785
1611
  stagedIdentity = { dev: staged.dev, ino: staged.ino };
1786
1612
  assertManagedDirectoryRoot(root);
1613
+ assertFence?.();
1787
1614
  fs.renameSync(staging, destination);
1615
+ assertFence?.();
1788
1616
  assertManagedDirectoryRoot(root);
1789
1617
  const named = fs.lstatSync(destination, { bigint: true });
1790
1618
  if (!named.isFile() || named.isSymbolicLink() || named.dev !== staged.dev || named.ino !== staged.ino) {
@@ -1807,7 +1635,7 @@ function replaceManagedFileSync(
1807
1635
  }
1808
1636
  }
1809
1637
 
1810
- async function replaceManagedFile(
1638
+ export async function replaceManagedFile(
1811
1639
  destination: string,
1812
1640
  bytes: Uint8Array,
1813
1641
  root: ManagedDirectoryRoot,
@@ -1842,6 +1670,7 @@ export async function acquireManagedLock(
1842
1670
  ensureManagedDirectory(locksDirectory, root, policy);
1843
1671
  const lockPath = path.join(locksDirectory, `${name}.lock`);
1844
1672
  const deadline = Date.now() + LOCK_WAIT_MS;
1673
+ let staleObservedAt: number | undefined;
1845
1674
  while (true) {
1846
1675
  const attemptId = randomUUID();
1847
1676
  const now = Date.now();
@@ -1866,11 +1695,7 @@ export async function acquireManagedLock(
1866
1695
  fs.fsyncSync(fd);
1867
1696
  fsyncDirectory(locksDirectory);
1868
1697
  } catch (error) {
1869
- try {
1870
- retireCreatedManagedLockExact(lockPath, fd);
1871
- } finally {
1872
- fs.closeSync(fd);
1873
- }
1698
+ fs.closeSync(fd);
1874
1699
  throw error;
1875
1700
  }
1876
1701
  const lockIdentity = fs.fstatSync(fd, { bigint: true });
@@ -1878,8 +1703,12 @@ export async function acquireManagedLock(
1878
1703
  let descriptorClosed = false;
1879
1704
  const closeDescriptor = (): void => {
1880
1705
  if (descriptorClosed) return;
1881
- fs.closeSync(fd);
1882
- descriptorClosed = true;
1706
+ try {
1707
+ secureFileDescriptor(lockPath, fd, "verify");
1708
+ } finally {
1709
+ fs.closeSync(fd);
1710
+ descriptorClosed = true;
1711
+ }
1883
1712
  };
1884
1713
  const assertOwned = (): void => {
1885
1714
  const current = parseLock(lockPath);
@@ -1894,7 +1723,8 @@ export async function acquireManagedLock(
1894
1723
  descriptorClosed ||
1895
1724
  !current ||
1896
1725
  !sameFileIdentity(lockIdentity, named) ||
1897
- current.attemptId !== attemptId
1726
+ current.attemptId !== attemptId ||
1727
+ current.leaseExpiresAt < Date.now()
1898
1728
  )
1899
1729
  throw new Error("migration_busy");
1900
1730
  };
@@ -1912,160 +1742,42 @@ export async function acquireManagedLock(
1912
1742
  attemptId,
1913
1743
  assertOwned,
1914
1744
  async release(): Promise<void> {
1915
- const finalize = (): void => {
1916
- clearInterval(heartbeat);
1917
- released = true;
1918
- closeDescriptor();
1919
- };
1745
+ clearInterval(heartbeat);
1920
1746
  try {
1921
1747
  assertOwned();
1922
- } catch (error) {
1923
- finalize();
1924
- throw error;
1925
- }
1926
- try {
1927
- secureFileDescriptor(lockPath, fd, "verify");
1928
- } catch (error) {
1929
- if (lockDescriptorOwnershipLost(error)) finalize();
1930
- throw error;
1931
- }
1932
- try {
1933
- retireManagedLockExact(lockPath, { lockIdentity, attemptId });
1934
- } catch (error) {
1935
- try {
1936
- assertOwned();
1937
- } catch {
1938
- finalize();
1939
- }
1940
- throw error;
1748
+ const now = Date.now();
1749
+ // Do not unlink by pathname: a stale owner could otherwise remove a successor.
1750
+ // The lease is retired through the verified inode-bound descriptor instead.
1751
+ writeLockDescriptor(fd, { ...record, heartbeatAt: now, leaseExpiresAt: now });
1752
+ fsyncDirectory(locksDirectory);
1753
+ } finally {
1754
+ released = true;
1755
+ closeDescriptor();
1941
1756
  }
1942
- finalize();
1943
1757
  },
1944
1758
  };
1945
1759
  } catch (error) {
1946
1760
  if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
1947
1761
  const owner = parseLock(lockPath);
1948
- if (owner && owner.leaseExpiresAt < Date.now() && ownerDefinitelyGone(owner)) {
1949
- try {
1950
- retireManagedLockExact(lockPath, { attemptId: owner.attemptId });
1951
- } catch {
1952
- /* retry owner observation */
1953
- }
1954
- }
1955
- if (Date.now() >= deadline) throw new Error("migration_busy");
1956
- await new Promise<void>(resolve => setTimeout(resolve, 50));
1957
- }
1958
- }
1959
- }
1960
- /** Acquire a synchronous lease lock for a short cross-process critical section. */
1961
- export function acquireManagedLockSync(
1962
- locksDirectory: string,
1963
- name: string,
1964
- root?: ManagedDirectoryRoot,
1965
- policy: ManagedSessionSecurityPolicy = "default",
1966
- ): ManagedStorageLockSync {
1967
- ensureManagedDirectory(locksDirectory, root, policy);
1968
- const lockPath = path.join(locksDirectory, `${name}.lock`);
1969
- const deadline = Date.now() + LOCK_WAIT_MS;
1970
- while (true) {
1971
- const attemptId = randomUUID();
1972
- const now = Date.now();
1973
- const record: LockRecord = {
1974
- attemptId,
1975
- pid: process.pid,
1976
- bootId: bootId(),
1977
- processStartId: PROCESS_START_ID,
1978
- createdAt: now,
1979
- heartbeatAt: now,
1980
- leaseExpiresAt: now + LOCK_LEASE_MS,
1981
- };
1982
- try {
1983
- const fd = fs.openSync(
1984
- lockPath,
1985
- fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW,
1986
- 0o600,
1987
- );
1988
- try {
1989
- secureFileDescriptor(lockPath, fd, "apply");
1990
- writeLockDescriptor(fd, record);
1991
- fsyncDirectory(locksDirectory);
1992
- } catch (error) {
1993
- try {
1994
- retireCreatedManagedLockExact(lockPath, fd);
1995
- } finally {
1996
- fs.closeSync(fd);
1997
- }
1998
- throw error;
1999
- }
2000
- const lockIdentity = fs.fstatSync(fd, { bigint: true });
2001
- let released = false;
2002
- let descriptorClosed = false;
2003
- const assertOwned = (): void => {
2004
- const current = parseLock(lockPath);
2005
- let named: fs.BigIntStats;
2006
- try {
2007
- named = fs.lstatSync(lockPath, { bigint: true });
2008
- } catch {
2009
- throw new Error("migration_busy");
2010
- }
2011
- if (
2012
- released ||
2013
- descriptorClosed ||
2014
- !current ||
2015
- !sameFileIdentity(lockIdentity, named) ||
2016
- current.attemptId !== attemptId
2017
- )
2018
- throw new Error("migration_busy");
2019
- };
2020
- return {
2021
- path: lockPath,
2022
- attemptId,
2023
- assertOwned,
2024
- release(): void {
2025
- const finalize = (): void => {
2026
- released = true;
2027
- if (!descriptorClosed) {
2028
- fs.closeSync(fd);
2029
- descriptorClosed = true;
2030
- }
2031
- };
2032
- try {
2033
- assertOwned();
2034
- } catch (error) {
2035
- finalize();
2036
- throw error;
2037
- }
2038
- try {
2039
- secureFileDescriptor(lockPath, fd, "verify");
2040
- } catch (error) {
2041
- if (lockDescriptorOwnershipLost(error)) finalize();
2042
- throw error;
2043
- }
1762
+ if (owner && owner.leaseExpiresAt < Date.now()) {
1763
+ const ownerGone = ownerDefinitelyGone(owner);
1764
+ if (staleObservedAt === undefined) staleObservedAt = Date.now();
1765
+ if (ownerGone || Date.now() - staleObservedAt >= LOCK_STALE_RECHECK_MS) {
1766
+ const quarantine = `${lockPath}.${randomUUID()}.stale`;
2044
1767
  try {
2045
- retireManagedLockExact(lockPath, { lockIdentity, attemptId });
2046
- } catch (error) {
2047
- try {
2048
- assertOwned();
2049
- } catch {
2050
- finalize();
2051
- }
2052
- throw error;
1768
+ fs.renameSync(lockPath, quarantine);
1769
+ const quarantined = parseLock(quarantine);
1770
+ if (!quarantined || quarantined.attemptId !== owner.attemptId) throw new Error("migration_busy");
1771
+ fs.unlinkSync(quarantine);
1772
+ fsyncDirectory(locksDirectory);
1773
+ } catch {
1774
+ /* retry owner observation */
2053
1775
  }
2054
- finalize();
2055
- },
2056
- };
2057
- } catch (error) {
2058
- if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
2059
- const owner = parseLock(lockPath);
2060
- if (owner && owner.leaseExpiresAt < Date.now() && ownerDefinitelyGone(owner)) {
2061
- try {
2062
- retireManagedLockExact(lockPath, { attemptId: owner.attemptId });
2063
- } catch {
2064
- /* Retry owner observation. */
1776
+ staleObservedAt = undefined;
2065
1777
  }
2066
- }
1778
+ } else staleObservedAt = undefined;
2067
1779
  if (Date.now() >= deadline) throw new Error("migration_busy");
2068
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.min(50, deadline - Date.now()));
1780
+ await new Promise<void>(resolve => setTimeout(resolve, 50));
2069
1781
  }
2070
1782
  }
2071
1783
  }