agl 21.0.0 → 22.0.0

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 (67) hide show
  1. package/changelog.md +27 -0
  2. package/dist_serve/bundle.js +1 -1
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/classes.aglhome.d.ts +25 -0
  5. package/dist_ts/classes.aglhome.js +65 -0
  6. package/dist_ts/classes.authmodels.js +7 -12
  7. package/dist_ts/classes.authstore.js +2 -29
  8. package/dist_ts/classes.cli.js +93 -35
  9. package/dist_ts/classes.config.d.ts +2 -11
  10. package/dist_ts/classes.config.js +14 -80
  11. package/dist_ts/classes.controller.d.ts +4 -1
  12. package/dist_ts/classes.controller.js +64 -26
  13. package/dist_ts/classes.embeddeddb.js +6 -5
  14. package/dist_ts/classes.gitreversion.js +60 -10
  15. package/dist_ts/classes.upgradecoordinator.d.ts +6 -1
  16. package/dist_ts/classes.upgradecoordinator.js +588 -177
  17. package/dist_ts/classes.upgradetransaction.js +46 -12
  18. package/dist_ts/classes.uploadmanager.d.ts +12 -0
  19. package/dist_ts/classes.uploadmanager.js +204 -2
  20. package/dist_ts/constants.upgradeenvironment.d.ts +2 -0
  21. package/dist_ts/constants.upgradeenvironment.js +3 -0
  22. package/dist_ts/functions.controllerdataroot.d.ts +4 -5
  23. package/dist_ts/functions.controllerdataroot.js +4 -29
  24. package/dist_ts/functions.embeddeddb.d.ts +1 -1
  25. package/dist_ts/functions.embeddeddb.js +8 -3
  26. package/dist_ts/functions.runtimeenvironment.js +2 -1
  27. package/dist_ts/index.d.ts +1 -0
  28. package/dist_ts/index.js +2 -1
  29. package/dist_ts/interfaces.config.d.ts +5 -7
  30. package/dist_ts/interfaces.gitreversion.d.ts +1 -1
  31. package/dist_ts_migration/classes.documentmigrationrunner.js +3 -1
  32. package/dist_ts_migration/index.d.ts +2 -0
  33. package/dist_ts_migration/index.js +3 -1
  34. package/dist_ts_migration/v23_aglhome.d.ts +91 -0
  35. package/dist_ts_migration/v23_aglhome.js +1775 -0
  36. package/dist_ts_migration/v23_runtimeconfig.d.ts +11 -0
  37. package/dist_ts_migration/v23_runtimeconfig.js +87 -0
  38. package/dist_ts_migration/v2_controllerdataroot.d.ts +5 -0
  39. package/dist_ts_migration/v2_controllerdataroot.js +176 -15
  40. package/package.json +3 -3
  41. package/readme.md +116 -35
  42. package/readme.plan.md +24 -9
  43. package/ts/00_commitinfo_data.ts +1 -1
  44. package/ts/classes.aglhome.ts +116 -0
  45. package/ts/classes.authmodels.ts +5 -13
  46. package/ts/classes.authstore.ts +1 -39
  47. package/ts/classes.cli.ts +92 -34
  48. package/ts/classes.config.ts +20 -117
  49. package/ts/classes.controller.ts +92 -26
  50. package/ts/classes.embeddeddb.ts +5 -4
  51. package/ts/classes.gitreversion.ts +79 -13
  52. package/ts/classes.upgradecoordinator.ts +668 -191
  53. package/ts/classes.upgradetransaction.ts +46 -11
  54. package/ts/classes.uploadmanager.ts +231 -1
  55. package/ts/constants.upgradeenvironment.ts +2 -0
  56. package/ts/functions.controllerdataroot.ts +11 -47
  57. package/ts/functions.embeddeddb.ts +13 -2
  58. package/ts/functions.runtimeenvironment.ts +1 -0
  59. package/ts/index.ts +1 -0
  60. package/ts/interfaces.config.ts +5 -7
  61. package/ts/interfaces.gitreversion.ts +1 -0
  62. package/ts_migration/classes.documentmigrationrunner.ts +2 -0
  63. package/ts_migration/index.ts +2 -0
  64. package/ts_migration/v23_aglhome.ts +2101 -0
  65. package/ts_migration/v23_runtimeconfig.ts +112 -0
  66. package/ts_migration/v2_controllerdataroot.ts +174 -16
  67. package/ts_web/00_commitinfo_data.ts +1 -1
@@ -129,6 +129,7 @@ import {
129
129
  resolveInitialRuntimeConfig,
130
130
  resolveStoredRuntimeConfig,
131
131
  } from './classes.config.js';
132
+ import { resolveAGLHomePaths } from './classes.aglhome.js';
132
133
  import { ensureControllerDataRootForStartup } from './functions.controllerdataroot.js';
133
134
  import { SmartDataAuthStore } from './classes.authstore.js';
134
135
  import { PasskeyManager } from './classes.passkeymanager.js';
@@ -301,6 +302,10 @@ import {
301
302
  resolveCurrentPnpmGlobalInstallation,
302
303
  resolveInheritedUpgradeTargetInstallation,
303
304
  } from './classes.upgradetransaction.js';
305
+ import {
306
+ upgradeCoordinationRootEnvironmentVariable,
307
+ upgradeTokenEnvironmentVariable,
308
+ } from './constants.upgradeenvironment.js';
304
309
 
305
310
  const currentCliName = String(controllerPackageName) === upgradePackageTransitionSource.packageName
306
311
  ? upgradePackageTransitionSource.cliName
@@ -1443,6 +1448,7 @@ export class OpenCodeController {
1443
1448
  private flexAutoAcceptDrainTask?: Promise<void>;
1444
1449
  private runtimeConfig?: Awaited<ReturnType<typeof resolveInitialRuntimeConfig>>;
1445
1450
  private controllerId?: string;
1451
+ private openCodeRuntimeDirectory?: string;
1446
1452
  private authStore?: SmartDataAuthStore;
1447
1453
  private passkeyManager?: PasskeyManager;
1448
1454
  private openCodeSupervisor?: OpenCodeSupervisor;
@@ -1594,6 +1600,23 @@ export class OpenCodeController {
1594
1600
  });
1595
1601
  }
1596
1602
 
1603
+ private captureInheritedUpgradeCoordination(): void {
1604
+ try {
1605
+ if (Boolean(this.options.upgradeToken) !== Boolean(this.options.upgradeGlobalRoot)) {
1606
+ throw new Error('Inherited upgrade coordination requires both its token and global root.');
1607
+ }
1608
+ if (!this.options.upgradeToken || !this.options.upgradeGlobalRoot) return;
1609
+ if (process.env[upgradeTokenEnvironmentVariable] !== this.options.upgradeToken) {
1610
+ throw new Error('The inherited upgrade coordination token changed before controller startup.');
1611
+ }
1612
+ this.upgradeToken = this.options.upgradeToken;
1613
+ this.upgradeCoordinator = new UpgradeCoordinator(this.options.upgradeGlobalRoot);
1614
+ } finally {
1615
+ delete process.env[upgradeTokenEnvironmentVariable];
1616
+ delete process.env[upgradeCoordinationRootEnvironmentVariable];
1617
+ }
1618
+ }
1619
+
1597
1620
  public async start(): Promise<IControllerStartResult> {
1598
1621
  if (this.lifecycleState === 'ready') {
1599
1622
  return { status: this.getStatus(), publicUrl: this.requireRuntimeConfig().publicOrigin };
@@ -1705,7 +1728,9 @@ export class OpenCodeController {
1705
1728
  }
1706
1729
 
1707
1730
  private async performStart(): Promise<IControllerStartResult> {
1731
+ process.env.AGL_HOME = resolveAGLHomePaths().root;
1708
1732
  this.lifecycleState = 'starting';
1733
+ this.captureInheritedUpgradeCoordination();
1709
1734
  const identity = await readControllerProcessIdentity(process.pid);
1710
1735
  this.assertStartActive();
1711
1736
  if (identity) {
@@ -1714,9 +1739,11 @@ export class OpenCodeController {
1714
1739
  }
1715
1740
  let upgradeCliPath: string | undefined;
1716
1741
  if (this.options.upgradeToken && this.options.upgradeGlobalRoot) {
1717
- this.upgradeToken = this.options.upgradeToken;
1718
- this.upgradeCoordinator = new UpgradeCoordinator(this.options.upgradeGlobalRoot);
1719
- const transaction = await this.upgradeCoordinator.readTransaction(this.options.upgradeToken);
1742
+ const inheritedCoordinator = this.upgradeCoordinator;
1743
+ if (!inheritedCoordinator || this.upgradeToken !== this.options.upgradeToken) {
1744
+ throw new Error('The inherited upgrade coordination was not captured before startup.');
1745
+ }
1746
+ const transaction = await inheritedCoordinator.readTransaction(this.options.upgradeToken);
1720
1747
  const installation = await resolveInheritedUpgradeTargetInstallation(transaction);
1721
1748
  const cliPath = await plugins.fs.promises.realpath(
1722
1749
  plugins.url.fileURLToPath(new URL('../cli.js', import.meta.url)),
@@ -1732,11 +1759,11 @@ export class OpenCodeController {
1732
1759
  cliName: currentCliName,
1733
1760
  cliRelativePath: './cli.js',
1734
1761
  });
1735
- this.upgradeStatus = await this.upgradeCoordinator.readSanitizedUpgradeStatus(
1762
+ this.upgradeStatus = await inheritedCoordinator.readSanitizedUpgradeStatus(
1736
1763
  this.options.upgradeToken,
1737
1764
  );
1738
1765
  upgradeCliPath = cliPath;
1739
- await this.upgradeCoordinator.recordController(this.options.upgradeToken, {
1766
+ await inheritedCoordinator.recordController(this.options.upgradeToken, {
1740
1767
  pid: process.pid,
1741
1768
  processGroupId: this.processGroupId,
1742
1769
  fingerprint: this.processFingerprint,
@@ -1872,6 +1899,17 @@ export class OpenCodeController {
1872
1899
  this.assertStartActive();
1873
1900
  this.runtimeConfig = runtimeConfig;
1874
1901
  this.controllerId = authDocument.id;
1902
+ this.openCodeRuntimeDirectory = plugins.path.join(
1903
+ dataDirectory,
1904
+ 'runtime',
1905
+ 'opencode',
1906
+ plugins.crypto.createHash('sha256').update(authDocument.id, 'utf8').digest('hex'),
1907
+ );
1908
+ await plugins.fs.promises.mkdir(this.openCodeRuntimeDirectory, {
1909
+ recursive: true,
1910
+ mode: 0o700,
1911
+ });
1912
+ await plugins.fs.promises.chmod(this.openCodeRuntimeDirectory, 0o700);
1875
1913
 
1876
1914
  const passkeyManager = new PasskeyManager({ store: authStore });
1877
1915
  this.passkeyManager = passkeyManager;
@@ -1972,8 +2010,9 @@ export class OpenCodeController {
1972
2010
  this.setupCodeExpiresAt = setupAuthority?.expiresAt.getTime();
1973
2011
  this.assertStartActive();
1974
2012
 
1975
- // v2 migration: the fixed workspace becomes the first registered project.
1976
- const pendingProjectRemovals = await this.ensureInitialProject(runtimeConfig.workspaceDirectory);
2013
+ const pendingProjectRemovals = await this.registerExplicitInitialProject(
2014
+ this.options.runtimeOverrides.initialProjectDirectory,
2015
+ );
1977
2016
  const startupProjectState = await this.reconcileProjectState(false);
1978
2017
  for (const project of pendingProjectRemovals) this.retiringProjectIds.add(project.id);
1979
2018
  for (const project of startupProjectState.projects) {
@@ -2107,6 +2146,7 @@ export class OpenCodeController {
2107
2146
  database: flexDatabase,
2108
2147
  flexCredentialDirectory: plugins.path.join(
2109
2148
  dataDirectory,
2149
+ 'credentials',
2110
2150
  'flex-provider-credentials',
2111
2151
  plugins.crypto.createHash('sha256').update(controllerId, 'utf8').digest('hex'),
2112
2152
  ),
@@ -2212,7 +2252,8 @@ export class OpenCodeController {
2212
2252
  browserResourceHost = new ControllerBrowserResourceHost({
2213
2253
  runtimeDirectory: plugins.path.join(
2214
2254
  dataDirectory,
2215
- 'browser-runtime',
2255
+ 'runtime',
2256
+ 'browser',
2216
2257
  plugins.crypto.createHash('sha256').update(controllerId).digest('hex'),
2217
2258
  ),
2218
2259
  authorizeCapability: (requestArg) => (
@@ -2338,6 +2379,19 @@ export class OpenCodeController {
2338
2379
  };
2339
2380
  }
2340
2381
 
2382
+ private coordinatorForUpgradeToken(
2383
+ tokenArg: string,
2384
+ globalRootArg: string,
2385
+ ): UpgradeCoordinator {
2386
+ if (this.upgradeToken === tokenArg && this.upgradeCoordinator) {
2387
+ if (this.upgradeCoordinator.canonicalGlobalRoot !== globalRootArg) {
2388
+ throw new Error('The inherited upgrade coordination global root changed unexpectedly.');
2389
+ }
2390
+ return this.upgradeCoordinator;
2391
+ }
2392
+ return new UpgradeCoordinator(globalRootArg);
2393
+ }
2394
+
2341
2395
  private startOpenCodeEventStream(
2342
2396
  openCodeClientArg: OpenCodeClientAdapter,
2343
2397
  skipInitialStreamAdvanceArg = false,
@@ -2476,9 +2530,12 @@ export class OpenCodeController {
2476
2530
 
2477
2531
  private createOpenCodeSupervisor(): OpenCodeSupervisor {
2478
2532
  const runtimeConfig = this.requireRuntimeConfig();
2533
+ if (!this.openCodeRuntimeDirectory) {
2534
+ throw new Error('The OpenCode runtime directory is unavailable.');
2535
+ }
2479
2536
  let supervisor!: OpenCodeSupervisor;
2480
2537
  supervisor = new OpenCodeSupervisor({
2481
- directory: runtimeConfig.workspaceDirectory,
2538
+ directory: this.openCodeRuntimeDirectory,
2482
2539
  port: runtimeConfig.opencodePort,
2483
2540
  onChildExit: () => {
2484
2541
  this.openCodeSupervisorExitSignals.get(supervisor)?.resolve();
@@ -3192,6 +3249,7 @@ export class OpenCodeController {
3192
3249
  this.authStore = undefined;
3193
3250
  this.passkeyManager = undefined;
3194
3251
  this.controllerId = undefined;
3252
+ this.openCodeRuntimeDirectory = undefined;
3195
3253
  }
3196
3254
  });
3197
3255
  }
@@ -3312,7 +3370,10 @@ export class OpenCodeController {
3312
3370
  try {
3313
3371
  const installation = await resolveCurrentPnpmGlobalInstallation();
3314
3372
  const status = this.getStatus();
3315
- const coordinator = new UpgradeCoordinator(installation.globalRoot);
3373
+ const coordinator = this.coordinatorForUpgradeToken(
3374
+ requestArg.token,
3375
+ installation.globalRoot,
3376
+ );
3316
3377
  await coordinator.consumeLaunchGrant({
3317
3378
  token: requestArg.token,
3318
3379
  port: this.requireRuntimeConfig().controllerPort,
@@ -3372,7 +3433,10 @@ export class OpenCodeController {
3372
3433
  return this.runOperation(peer, async (signalArg) => (
3373
3434
  this.runUpgradePreparationAdmission(async () => {
3374
3435
  const installation = await resolveCurrentPnpmGlobalInstallation();
3375
- const coordinator = new UpgradeCoordinator(installation.globalRoot);
3436
+ const coordinator = this.coordinatorForUpgradeToken(
3437
+ requestArg.token,
3438
+ installation.globalRoot,
3439
+ );
3376
3440
  const status = this.getStatus();
3377
3441
  await coordinator.consumeControllerActionGrant({
3378
3442
  token: requestArg.token,
@@ -3406,7 +3470,10 @@ export class OpenCodeController {
3406
3470
  return this.runOperation(peer, async () => (
3407
3471
  this.runUpgradePreparationAdmission(async () => {
3408
3472
  const installation = await resolveCurrentPnpmGlobalInstallation();
3409
- const coordinator = new UpgradeCoordinator(installation.globalRoot);
3473
+ const coordinator = this.coordinatorForUpgradeToken(
3474
+ requestArg.token,
3475
+ installation.globalRoot,
3476
+ );
3410
3477
  if (await this.upgradePreparationReplayIsAccepted(
3411
3478
  coordinator,
3412
3479
  requestArg.token,
@@ -3454,7 +3521,10 @@ export class OpenCodeController {
3454
3521
  );
3455
3522
  return this.runOperation(peer, async () => {
3456
3523
  const installation = await resolveCurrentPnpmGlobalInstallation();
3457
- const coordinator = new UpgradeCoordinator(installation.globalRoot);
3524
+ const coordinator = this.coordinatorForUpgradeToken(
3525
+ requestArg.token,
3526
+ installation.globalRoot,
3527
+ );
3458
3528
  const status = this.getStatus();
3459
3529
  await coordinator.consumeControllerActionGrant({
3460
3530
  token: requestArg.token,
@@ -6187,20 +6257,17 @@ export class OpenCodeController {
6187
6257
  return projectsRoot;
6188
6258
  }
6189
6259
 
6190
- private async ensureInitialProject(
6191
- workspaceDirectoryArg: string,
6260
+ private async registerExplicitInitialProject(
6261
+ initialProjectDirectoryArg: string | undefined,
6192
6262
  ): Promise<IControllerProjectDocument[]> {
6193
6263
  const authStore = this.requireAuthStore();
6194
- const [activeProjects, pendingRemovals] = await Promise.all([
6195
- authStore.listProjects(),
6196
- authStore.listPendingProjectRemovals(),
6197
- ]);
6198
- if (activeProjects.length === 0 && pendingRemovals.length === 0) {
6199
- const workspaceDirectory = await resolveCanonicalDirectory(workspaceDirectoryArg);
6264
+ const pendingRemovals = await authStore.listPendingProjectRemovals();
6265
+ if (initialProjectDirectoryArg !== undefined) {
6266
+ const initialProjectDirectory = await resolveCanonicalDirectory(initialProjectDirectoryArg);
6200
6267
  await authStore.createProject(
6201
- plugins.path.basename(workspaceDirectory.directory) || 'workspace',
6202
- workspaceDirectory.directory,
6203
- workspaceDirectory,
6268
+ this.deriveProjectName(initialProjectDirectory.directory),
6269
+ initialProjectDirectory.directory,
6270
+ initialProjectDirectory,
6204
6271
  );
6205
6272
  }
6206
6273
  return pendingRemovals;
@@ -14170,13 +14237,12 @@ export class OpenCodeController {
14170
14237
  if (!client) return;
14171
14238
  const deadline = AbortSignal.timeout(abortConversationsOnStopTimeoutMs);
14172
14239
  const directories = new Set<string>();
14173
- if (this.runtimeConfig) directories.add(this.runtimeConfig.workspaceDirectory);
14174
14240
  try {
14175
14241
  for (const project of (await this.authStore?.listProjects()) ?? []) {
14176
14242
  directories.add(project.directory);
14177
14243
  }
14178
14244
  } catch {
14179
- // Project registry unavailable — workspace sessions still get aborted.
14245
+ // Project registry unavailable; shutdown continues with owned resources.
14180
14246
  }
14181
14247
  for (const directory of directories) {
14182
14248
  try {
@@ -46,10 +46,8 @@ export class EmbeddedControllerDatabase {
46
46
  constructor(private readonly options: IEmbeddedControllerDatabaseOptions) {}
47
47
 
48
48
  public get socketPath(): string {
49
- // Unix socket paths are capped near 108 bytes (SUN_LEN), so the socket
50
- // lives in tmpdir under a short name derived from the data directory —
51
- // deterministic, so a second controller process attaches to the same
52
- // engine, and independent of how deep the data directory is nested.
49
+ // The hashed name is deterministic so another AGL process can attach to
50
+ // the same engine without owning its storage files.
53
51
  return embeddedDatabaseSocketPath(this.options.dataDirectory);
54
52
  }
55
53
 
@@ -74,6 +72,9 @@ export class EmbeddedControllerDatabase {
74
72
  if (this.localDb || this.migrationRunner?.hasOwnedResources) {
75
73
  throw new Error('Embedded database startup cleanup must complete before retrying.');
76
74
  }
75
+ const socketDirectory = plugins.path.dirname(this.socketPath);
76
+ await plugins.fs.promises.mkdir(socketDirectory, { recursive: true, mode: 0o700 });
77
+ await plugins.fs.promises.chmod(socketDirectory, 0o700);
77
78
  if (this.options.legacyDataDirectory) {
78
79
  const migrationRunner = new EmbeddedDatabaseMigrationRunner({
79
80
  sourceDirectory: this.options.legacyDataDirectory,
@@ -1,4 +1,5 @@
1
1
  import * as plugins from './plugins.js';
2
+ import { resolveAGLHomePaths } from './classes.aglhome.js';
2
3
  import {
3
4
  assertCanonicalDirectory,
4
5
  assertCanonicalFilesystemObject,
@@ -665,6 +666,13 @@ class GitReversionInjectedFaultError extends Error {
665
666
  }
666
667
  }
667
668
 
669
+ class GitReversionConcurrentRecordReadError extends Error {
670
+ constructor(messageArg: string) {
671
+ super(messageArg);
672
+ this.name = 'GitReversionConcurrentRecordReadError';
673
+ }
674
+ }
675
+
668
676
  class GitReversionSourceKeepOwnershipError extends GitReversionFencedError {
669
677
  constructor(messageArg: string, optionsArg?: ErrorOptions) {
670
678
  super(messageArg, optionsArg);
@@ -739,8 +747,8 @@ export class ControllerGitReversion {
739
747
  this.validateLimits();
740
748
  this.maximumPrivateTreeEntries = privateTreeTraversalLimit(this.limits);
741
749
  this.rootDirectory = optionsArg.temporaryRoot ?? plugins.path.join(
742
- plugins.os.tmpdir(),
743
- `harness-controller-reversion-${this.ownerKey}`,
750
+ resolveAGLHomePaths().gitReversion,
751
+ sha256(optionsArg.ownerId),
744
752
  );
745
753
  if (!plugins.path.isAbsolute(this.rootDirectory)) {
746
754
  throw new Error('The Git reversion temporary root must be absolute.');
@@ -3182,6 +3190,8 @@ export class ControllerGitReversion {
3182
3190
  descriptionArg: string,
3183
3191
  signalArg?: AbortSignal,
3184
3192
  requirePrivateModeArg = false,
3193
+ detectConcurrentReplacementArg = false,
3194
+ afterOpenArg?: () => Promise<void>,
3185
3195
  ): Promise<Buffer> {
3186
3196
  let handle: plugins.fs.promises.FileHandle;
3187
3197
  try {
@@ -3196,7 +3206,13 @@ export class ControllerGitReversion {
3196
3206
  throw new Error(`${descriptionArg} could not be opened safely.`, { cause: errorArg });
3197
3207
  }
3198
3208
  try {
3209
+ await afterOpenArg?.();
3199
3210
  const before = await handle.stat();
3211
+ if (detectConcurrentReplacementArg && before.nlink === 0) {
3212
+ throw new GitReversionConcurrentRecordReadError(
3213
+ `${descriptionArg} was replaced while it was opened.`,
3214
+ );
3215
+ }
3200
3216
  if (
3201
3217
  !before.isFile()
3202
3218
  || (requirePrivateModeArg && before.nlink !== 1)
@@ -3222,12 +3238,46 @@ export class ControllerGitReversion {
3222
3238
  if (total > maximumBytesArg) throw new Error(`${descriptionArg} exceeds its byte limit.`);
3223
3239
  }
3224
3240
  const after = await handle.stat();
3241
+ if (detectConcurrentReplacementArg && after.nlink === 0) {
3242
+ throw new GitReversionConcurrentRecordReadError(
3243
+ `${descriptionArg} was replaced while it was read.`,
3244
+ );
3245
+ }
3225
3246
  if (
3226
3247
  before.dev !== after.dev
3227
3248
  || before.ino !== after.ino
3228
3249
  || before.size !== after.size
3229
3250
  || total !== before.size
3251
+ || !after.isFile()
3252
+ || (requirePrivateModeArg && after.nlink !== 1)
3253
+ || (requirePrivateModeArg && (after.mode & 0o077) !== 0)
3254
+ || (typeof process.getuid === 'function' && after.uid !== process.getuid())
3230
3255
  ) throw new Error(`${descriptionArg} changed while it was read.`);
3256
+ if (detectConcurrentReplacementArg) {
3257
+ let pathAfter: plugins.fs.Stats;
3258
+ try {
3259
+ pathAfter = await plugins.fs.promises.lstat(pathArg);
3260
+ } catch (errorArg) {
3261
+ if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') {
3262
+ throw new GitReversionConcurrentRecordReadError(
3263
+ `${descriptionArg} was removed while it was read.`,
3264
+ );
3265
+ }
3266
+ throw errorArg;
3267
+ }
3268
+ if (
3269
+ !pathAfter.isFile()
3270
+ || pathAfter.isSymbolicLink()
3271
+ || pathAfter.nlink !== 1
3272
+ || (pathAfter.mode & 0o077) !== 0
3273
+ || (typeof process.getuid === 'function' && pathAfter.uid !== process.getuid())
3274
+ ) throw new Error(`${descriptionArg} pathname is not an exact owned regular file.`);
3275
+ if (pathAfter.dev !== after.dev || pathAfter.ino !== after.ino) {
3276
+ throw new GitReversionConcurrentRecordReadError(
3277
+ `${descriptionArg} was replaced while its pathname was revalidated.`,
3278
+ );
3279
+ }
3280
+ }
3231
3281
  return Buffer.concat(chunks, total);
3232
3282
  } finally {
3233
3283
  await handle.close();
@@ -12121,18 +12171,34 @@ export class ControllerGitReversion {
12121
12171
 
12122
12172
  private async readWorktreeRecordByRef<T>(refArg: string): Promise<{ oid: string; value: T } | undefined> {
12123
12173
  const path = this.worktreeRecordPathForRef(refArg);
12124
- let bytes: Buffer;
12125
- try {
12126
- bytes = await this.readBoundedRegularFile(
12127
- path,
12128
- this.limits.maximumMetadataRecordBytes,
12129
- 'Private worktree record',
12130
- undefined,
12131
- true,
12174
+ let bytes: Buffer | undefined;
12175
+ let concurrentReadError: GitReversionConcurrentRecordReadError | undefined;
12176
+ for (let attempt = 0; attempt < 4; attempt += 1) {
12177
+ try {
12178
+ bytes = await this.readBoundedRegularFile(
12179
+ path,
12180
+ this.limits.maximumMetadataRecordBytes,
12181
+ 'Private worktree record',
12182
+ undefined,
12183
+ true,
12184
+ true,
12185
+ () => this.fault({ point: 'worktree.record-read.after-open', ref: refArg }),
12186
+ );
12187
+ break;
12188
+ } catch (errorArg) {
12189
+ if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
12190
+ if (errorArg instanceof GitReversionConcurrentRecordReadError) {
12191
+ concurrentReadError = errorArg;
12192
+ continue;
12193
+ }
12194
+ throw errorArg;
12195
+ }
12196
+ }
12197
+ if (!bytes) {
12198
+ throw new GitReversionFencedError(
12199
+ 'The private worktree record changed repeatedly while it was read.',
12200
+ { cause: concurrentReadError },
12132
12201
  );
12133
- } catch (errorArg) {
12134
- if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
12135
- throw errorArg;
12136
12202
  }
12137
12203
  let value: unknown;
12138
12204
  try {