@gajae-code/coding-agent 0.12.2 → 0.12.5

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 (40) hide show
  1. package/CHANGELOG.md +15 -2
  2. package/dist/types/async/job-manager.d.ts +9 -1
  3. package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +5 -1
  4. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -2
  5. package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -2
  6. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +5 -3
  7. package/dist/types/session/internal/managed-session-storage.d.ts +3 -22
  8. package/dist/types/session/session-manager.d.ts +1 -1
  9. package/dist/types/task/executor.d.ts +1 -1
  10. package/dist/types/task/index.d.ts +2 -1
  11. package/dist/types/task/receipt.d.ts +1 -0
  12. package/dist/types/task/types.d.ts +16 -0
  13. package/dist/types/task/worktree.d.ts +13 -6
  14. package/dist/types/tools/subagent.d.ts +2 -0
  15. package/dist/types/utils/git.d.ts +1 -0
  16. package/package.json +8 -8
  17. package/src/async/job-manager.ts +13 -5
  18. package/src/config/provider-ranking.ts +1 -0
  19. package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -5
  20. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +2 -1
  21. package/src/gjc-runtime/ultragoal-evidence.ts +160 -31
  22. package/src/gjc-runtime/ultragoal-runtime.ts +231 -19
  23. package/src/internal-urls/docs-index.generated.ts +2 -2
  24. package/src/sdk/broker/lifecycle.ts +13 -18
  25. package/src/sdk/bus/chat-daemon-control.ts +6 -4
  26. package/src/sdk/bus/telegram-daemon-contract.ts +5 -3
  27. package/src/sdk/session.ts +1 -0
  28. package/src/session/internal/managed-session-scope.ts +107 -158
  29. package/src/session/internal/managed-session-storage.ts +57 -445
  30. package/src/session/session-manager.ts +12 -7
  31. package/src/session/session-storage.ts +44 -29
  32. package/src/task/executor.ts +5 -2
  33. package/src/task/index.ts +232 -64
  34. package/src/task/receipt.ts +22 -2
  35. package/src/task/render.ts +12 -1
  36. package/src/task/types.ts +18 -0
  37. package/src/task/worktree.ts +122 -43
  38. package/src/tools/subagent-render.ts +5 -2
  39. package/src/tools/subagent.ts +8 -0
  40. 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
  }
@@ -137,24 +134,15 @@ export interface ManagedStorageLock {
137
134
  assertOwned(): void;
138
135
  release(): Promise<void>;
139
136
  }
140
- export interface ManagedStorageLockSync {
141
- path: string;
142
- attemptId: string;
143
- assertOwned(): void;
144
- release(): void;
145
- }
146
137
 
147
138
  export interface ManagedFileSnapshot {
148
139
  bytes: Buffer;
149
140
  identity: { dev: bigint; ino: bigint; size: number; mtimeNs: bigint; ctimeNs: bigint; sha256: string };
150
141
  }
151
142
 
152
- export const MANAGED_SESSION_SECURITY_CODES = [
153
- "acl_denied",
154
- "acl_io_error",
155
- "acl_present",
156
- "acl_malformed",
157
- "acl_unknown",
143
+ const ACL_FAILURE_CODES = new Set(["acl_denied", "acl_io_error", "acl_present", "acl_malformed", "acl_unknown"]);
144
+ const ACL_CLEAR_EVIDENCE = new Set(["cleared", "already_absent", "unsupported", "not_run"]);
145
+ const GENERAL_FAILURE_CODES = new Set([
158
146
  "acl_unavailable",
159
147
  "acl_apply_failed",
160
148
  "acl_verify_failed",
@@ -167,52 +155,7 @@ export const MANAGED_SESSION_SECURITY_CODES = [
167
155
  "owner_mismatch",
168
156
  "mode_mismatch",
169
157
  "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
158
  ]);
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
159
 
217
160
  function hasOnlyKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
218
161
  return Object.keys(value).every(key => allowed.includes(key));
@@ -230,11 +173,10 @@ export function validateNativeSecurityResult(
230
173
  const result = value as Record<string, unknown>;
231
174
  if (result.ok === false) {
232
175
  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)) {
176
+ if (!ACL_FAILURE_CODES.has(result.code) && !GENERAL_FAILURE_CODES.has(result.code)) {
235
177
  throw new Error("Unknown owner-only security failure code");
236
178
  }
237
- if (ACL_FAILURE_CODES.has(code)) {
179
+ if (ACL_FAILURE_CODES.has(result.code)) {
238
180
  if (
239
181
  (result.operation !== "clear" && result.operation !== "query") ||
240
182
  (result.attribute !== "access" && result.attribute !== "default")
@@ -455,7 +397,7 @@ class ManagedSecurityError extends Error {
455
397
  }
456
398
 
457
399
  export function managedSecurityFailureClassification(error: unknown): string | undefined {
458
- return error instanceof ManagedSecurityError ? error.getClassification() : managedSessionSecurityCode(error);
400
+ return error instanceof ManagedSecurityError ? error.getClassification() : undefined;
459
401
  }
460
402
 
461
403
  function securityError(pathname: string, result: NativeSecurity): Error {
@@ -475,11 +417,10 @@ function windowsExistingVerifyFirst(policy: ManagedSessionSecurityPolicy): boole
475
417
 
476
418
  function assertManagedPathIdentity(pathname: string, kind: "directory" | "file", expected: fs.BigIntStats): void {
477
419
  const current = fs.lstatSync(pathname, { bigint: true });
478
- if (current.isSymbolicLink()) throw new ManagedSessionSecurityError("reparse_point");
420
+ if (current.isSymbolicLink()) throw new Error("reparse_point");
479
421
  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");
422
+ if (!expectedKind) throw new Error(kind === "directory" ? "not_directory" : "not_file");
423
+ if (current.dev !== expected.dev || current.ino !== expected.ino) throw new Error("identity_mismatch");
483
424
  }
484
425
 
485
426
  function verifyExistingManagedPathSecurity(
@@ -499,7 +440,7 @@ function verifyExistingManagedPathSecurity(
499
440
  function secureExistingManagedDirectory(pathname: string, kind: "directory" | "file"): void {
500
441
  const named = fs.lstatSync(pathname, { bigint: true });
501
442
  const safeKind = kind === "directory" ? named.isDirectory() : named.isFile();
502
- if (!safeKind || named.isSymbolicLink()) throw new ManagedSessionSecurityError("reparse_point");
443
+ if (!safeKind || named.isSymbolicLink()) throw new Error(`Unsafe managed ${kind}: ${pathname}`);
503
444
  const verified = validateNativeSecurityResult(
504
445
  verifyOwnerOnlyPathSecurityExpected(pathname, kind, named.dev, named.ino),
505
446
  "verify",
@@ -659,14 +600,15 @@ export class ManagedSessionDescendantStore {
659
600
  if (this.#authority) {
660
601
  if (this.#authorityBaseDir === this.#baseDir) {
661
602
  const verified = this.#authority.verifyOwnerOnlyDirectory();
662
- if (!verified.ok) throw managedSessionSecurityErrorForCode(verified.code);
603
+ if (!verified.ok) throw new Error(verified.code ?? "acl_verify_failed");
663
604
  } else {
664
605
  this.#assertBound();
665
606
  return;
666
607
  }
667
608
  } else {
668
609
  const named = fs.lstatSync(this.#baseDir, { bigint: true });
669
- if (!named.isDirectory() || named.isSymbolicLink()) throw new ManagedSessionSecurityError("reparse_point");
610
+ if (!named.isDirectory() || named.isSymbolicLink())
611
+ throw new Error(`Unsafe managed directory: ${this.#baseDir}`);
670
612
  if (windowsExistingVerifyFirst(this.#policy))
671
613
  verifyExistingManagedPathSecurity(this.#baseDir, "directory", named);
672
614
  else {
@@ -753,13 +695,7 @@ export class ManagedSessionDescendantStore {
753
695
  this.#assertBound();
754
696
  const resolved = this.#resolve(relativePath);
755
697
  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
- }
698
+ await replaceManagedFile(resolved, bytes, this.#subtreeRoot, this.#policy);
763
699
  this.#assertBound();
764
700
  return;
765
701
  }
@@ -772,13 +708,7 @@ export class ManagedSessionDescendantStore {
772
708
  this.#assertBound();
773
709
  const resolved = this.#resolve(relativePath);
774
710
  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
- }
711
+ replaceManagedFileSync(resolved, bytes, this.#subtreeRoot, this.#policy);
782
712
  this.#assertBound();
783
713
  return;
784
714
  }
@@ -1225,7 +1155,7 @@ export class ManagedSessionDescendantStore {
1225
1155
  }
1226
1156
 
1227
1157
  function secureFileDescriptor(pathname: string, fd: number, operation: "apply" | "verify"): void {
1228
- if (process.platform !== "linux" && process.platform !== "win32") {
1158
+ if (process.platform !== "linux") {
1229
1159
  if (operation === "apply") secure(pathname, "file");
1230
1160
  else {
1231
1161
  const verified = validateNativeSecurityResult(verifyOwnerOnlyPathSecurity(pathname, "file"), "verify", "file");
@@ -1309,9 +1239,11 @@ function isCtimeOnlyIdentityMismatch(
1309
1239
  return sameStableIdentityIgnoringCtime(observed, expected) && observed.ctimeNs !== expected.ctimeNs;
1310
1240
  }
1311
1241
 
1312
- function parseLockBytes(bytes: Uint8Array): LockRecord | undefined {
1242
+ function parseLock(pathname: string): LockRecord | undefined {
1313
1243
  try {
1314
- const value: unknown = JSON.parse(Buffer.from(bytes).toString("utf8"));
1244
+ const stat = fs.lstatSync(pathname);
1245
+ if (!stat.isFile() || stat.isSymbolicLink()) return undefined;
1246
+ const value: unknown = JSON.parse(fs.readFileSync(pathname, "utf8"));
1315
1247
  if (!value || typeof value !== "object") return undefined;
1316
1248
  const record = value as Partial<LockRecord>;
1317
1249
  return typeof record.attemptId === "string" &&
@@ -1327,16 +1259,6 @@ function parseLockBytes(bytes: Uint8Array): LockRecord | undefined {
1327
1259
  }
1328
1260
  }
1329
1261
 
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
1262
  function ownerDefinitelyGone(record: LockRecord): boolean {
1341
1263
  if (record.bootId && bootId() && record.bootId !== bootId()) return true;
1342
1264
  try {
@@ -1357,82 +1279,6 @@ function writeLockDescriptor(fd: number, record: LockRecord): void {
1357
1279
  function sameFileIdentity(left: fs.BigIntStats, right: fs.BigIntStats): boolean {
1358
1280
  return left.dev === right.dev && left.ino === right.ino;
1359
1281
  }
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
1282
 
1437
1283
  /** Create a managed directory and fail closed unless its owner-only mode/ACL verifies. */
1438
1284
  export function ensureManagedDirectory(
@@ -1640,126 +1486,8 @@ export function publishManagedFileNoReplaceSync(
1640
1486
  if (failure !== undefined) throw failure;
1641
1487
  }
1642
1488
 
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
1489
  /** Replace one managed regular file while retaining the secured staging fd through publication. */
1762
- function replaceManagedFileSync(
1490
+ export function replaceManagedFileSync(
1763
1491
  destination: string,
1764
1492
  bytes: Uint8Array,
1765
1493
  root: ManagedDirectoryRoot,
@@ -1807,7 +1535,7 @@ function replaceManagedFileSync(
1807
1535
  }
1808
1536
  }
1809
1537
 
1810
- async function replaceManagedFile(
1538
+ export async function replaceManagedFile(
1811
1539
  destination: string,
1812
1540
  bytes: Uint8Array,
1813
1541
  root: ManagedDirectoryRoot,
@@ -1842,6 +1570,7 @@ export async function acquireManagedLock(
1842
1570
  ensureManagedDirectory(locksDirectory, root, policy);
1843
1571
  const lockPath = path.join(locksDirectory, `${name}.lock`);
1844
1572
  const deadline = Date.now() + LOCK_WAIT_MS;
1573
+ let staleObservedAt: number | undefined;
1845
1574
  while (true) {
1846
1575
  const attemptId = randomUUID();
1847
1576
  const now = Date.now();
@@ -1866,11 +1595,7 @@ export async function acquireManagedLock(
1866
1595
  fs.fsyncSync(fd);
1867
1596
  fsyncDirectory(locksDirectory);
1868
1597
  } catch (error) {
1869
- try {
1870
- retireCreatedManagedLockExact(lockPath, fd);
1871
- } finally {
1872
- fs.closeSync(fd);
1873
- }
1598
+ fs.closeSync(fd);
1874
1599
  throw error;
1875
1600
  }
1876
1601
  const lockIdentity = fs.fstatSync(fd, { bigint: true });
@@ -1878,8 +1603,12 @@ export async function acquireManagedLock(
1878
1603
  let descriptorClosed = false;
1879
1604
  const closeDescriptor = (): void => {
1880
1605
  if (descriptorClosed) return;
1881
- fs.closeSync(fd);
1882
- descriptorClosed = true;
1606
+ try {
1607
+ secureFileDescriptor(lockPath, fd, "verify");
1608
+ } finally {
1609
+ fs.closeSync(fd);
1610
+ descriptorClosed = true;
1611
+ }
1883
1612
  };
1884
1613
  const assertOwned = (): void => {
1885
1614
  const current = parseLock(lockPath);
@@ -1894,7 +1623,8 @@ export async function acquireManagedLock(
1894
1623
  descriptorClosed ||
1895
1624
  !current ||
1896
1625
  !sameFileIdentity(lockIdentity, named) ||
1897
- current.attemptId !== attemptId
1626
+ current.attemptId !== attemptId ||
1627
+ current.leaseExpiresAt < Date.now()
1898
1628
  )
1899
1629
  throw new Error("migration_busy");
1900
1630
  };
@@ -1912,160 +1642,42 @@ export async function acquireManagedLock(
1912
1642
  attemptId,
1913
1643
  assertOwned,
1914
1644
  async release(): Promise<void> {
1915
- const finalize = (): void => {
1916
- clearInterval(heartbeat);
1917
- released = true;
1918
- closeDescriptor();
1919
- };
1645
+ clearInterval(heartbeat);
1920
1646
  try {
1921
1647
  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;
1648
+ const now = Date.now();
1649
+ // Do not unlink by pathname: a stale owner could otherwise remove a successor.
1650
+ // The lease is retired through the verified inode-bound descriptor instead.
1651
+ writeLockDescriptor(fd, { ...record, heartbeatAt: now, leaseExpiresAt: now });
1652
+ fsyncDirectory(locksDirectory);
1653
+ } finally {
1654
+ released = true;
1655
+ closeDescriptor();
1941
1656
  }
1942
- finalize();
1943
1657
  },
1944
1658
  };
1945
1659
  } catch (error) {
1946
1660
  if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
1947
1661
  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
- };
1662
+ if (owner && owner.leaseExpiresAt < Date.now()) {
1663
+ const ownerGone = ownerDefinitelyGone(owner);
1664
+ if (staleObservedAt === undefined) staleObservedAt = Date.now();
1665
+ if (ownerGone || Date.now() - staleObservedAt >= LOCK_STALE_RECHECK_MS) {
1666
+ const quarantine = `${lockPath}.${randomUUID()}.stale`;
2032
1667
  try {
2033
- assertOwned();
2034
- } catch (error) {
2035
- finalize();
2036
- throw error;
1668
+ fs.renameSync(lockPath, quarantine);
1669
+ const quarantined = parseLock(quarantine);
1670
+ if (!quarantined || quarantined.attemptId !== owner.attemptId) throw new Error("migration_busy");
1671
+ fs.unlinkSync(quarantine);
1672
+ fsyncDirectory(locksDirectory);
1673
+ } catch {
1674
+ /* retry owner observation */
2037
1675
  }
2038
- try {
2039
- secureFileDescriptor(lockPath, fd, "verify");
2040
- } catch (error) {
2041
- if (lockDescriptorOwnershipLost(error)) finalize();
2042
- throw error;
2043
- }
2044
- try {
2045
- retireManagedLockExact(lockPath, { lockIdentity, attemptId });
2046
- } catch (error) {
2047
- try {
2048
- assertOwned();
2049
- } catch {
2050
- finalize();
2051
- }
2052
- throw error;
2053
- }
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. */
1676
+ staleObservedAt = undefined;
2065
1677
  }
2066
- }
1678
+ } else staleObservedAt = undefined;
2067
1679
  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()));
1680
+ await new Promise<void>(resolve => setTimeout(resolve, 50));
2069
1681
  }
2070
1682
  }
2071
1683
  }