@ricsam/r5d-worker 0.0.92 → 0.0.94

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.
package/dist/cjs/main.cjs CHANGED
@@ -4067,7 +4067,7 @@ async function startWorker(options) {
4067
4067
  }
4068
4068
  return heads;
4069
4069
  };
4070
- const pushChangedProjectHeads = async (activeMountIds, publishedHead, publicationHeads, suppressedMountIds = /* @__PURE__ */ new Set()) => {
4070
+ const pushChangedProjectHeads = async (activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, suppressedMountIds = /* @__PURE__ */ new Set()) => {
4071
4071
  const active = new Set(activeMountIds);
4072
4072
  for (const project of projectConfigById.values()) {
4073
4073
  if (!readyProjectIds.has(project.projectId) || !(0, import_repository_transition_policy.repositoryMirrorWritesAllowed)(project)) continue;
@@ -4082,6 +4082,12 @@ async function startWorker(options) {
4082
4082
  }
4083
4083
  }
4084
4084
  if (changed.size === 0) continue;
4085
+ const branchLeases = /* @__PURE__ */ new Map();
4086
+ for (const branchName of changed.keys()) {
4087
+ const mountId = projectMountId(project.projectId, branchName);
4088
+ if (!expectedMirrorHeads.has(mountId)) throw new Error(`Missing observed hidden-mirror lease for active project mount ${mountId}`);
4089
+ branchLeases.set(branchName, expectedMirrorHeads.get(mountId) ?? null);
4090
+ }
4085
4091
  const connection = projectConnection(project);
4086
4092
  const results = await (0, import_project_worktrees.pushProjectMirrorHeads)({
4087
4093
  projectRoot: configuredProjectRoot(projectsRoot, project),
@@ -4090,7 +4096,9 @@ async function startWorker(options) {
4090
4096
  credentialHelper: connection.credentialHelper,
4091
4097
  credentialUsername: connection.mirrorCredentialUsername,
4092
4098
  onlyBranches: new Set(changed.keys()),
4093
- publicationHeads: changed
4099
+ publicationHeads: changed,
4100
+ expectedMirrorHeads: branchLeases,
4101
+ branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId]))
4094
4102
  });
4095
4103
  (0, import_project_worktrees.assertProjectMirrorHeadsPushed)(results);
4096
4104
  for (const result of results) {
@@ -4154,7 +4162,8 @@ async function startWorker(options) {
4154
4162
  mirrorUrl: connection.mirrorUrl,
4155
4163
  credentialHelper: connection.credentialHelper,
4156
4164
  credentialUsername: connection.mirrorCredentialUsername,
4157
- allowNonFastForward: input.allowNonFastForward
4165
+ allowNonFastForward: input.allowNonFastForward,
4166
+ branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId]))
4158
4167
  })
4159
4168
  });
4160
4169
  } catch (error) {
@@ -4167,7 +4176,7 @@ async function startWorker(options) {
4167
4176
  }
4168
4177
  return bundles;
4169
4178
  };
4170
- const applyObservedProjectHeadsAfterInboundWorkspace = async (bundles, activeMountIds, requireObservedHeads) => {
4179
+ const applyObservedProjectHeadsAfterInboundWorkspace = async (bundles, activeMountIds, requireObservedHeads, integratedWorkspaceHead) => {
4171
4180
  const active = new Set(activeMountIds);
4172
4181
  for (const bundle of bundles) {
4173
4182
  const project = projectConfigById.get(bundle.projectId);
@@ -4179,7 +4188,16 @@ async function startWorker(options) {
4179
4188
  projectRoot: bundle.projectRoot,
4180
4189
  primaryBranchName: bundle.primaryBranchName,
4181
4190
  observations,
4182
- branchMutationAllowed: (branchName, checkoutPath) => !projectBranchMountBusy(project.projectId, branchName, checkoutPath)
4191
+ branchMutationAllowed: (branchName, checkoutPath) => !projectBranchMountBusy(project.projectId, branchName, checkoutPath),
4192
+ serverSyncedWorkspaceCovered: async (observation) => {
4193
+ if (!integratedWorkspaceHead) return false;
4194
+ for (const workspaceHead of observation.serverSyncedWorkspaceHeads) {
4195
+ if (await tryGitAsync(["merge-base", "--is-ancestor", workspaceHead, integratedWorkspaceHead], { cwd: workspaceShadowRoot })) {
4196
+ return true;
4197
+ }
4198
+ }
4199
+ return false;
4200
+ }
4183
4201
  });
4184
4202
  if (requireObservedHeads) {
4185
4203
  for (const observation of observations) {
@@ -4304,6 +4322,12 @@ async function startWorker(options) {
4304
4322
  const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
4305
4323
  const mounts = outerRemediation ? (0, import_workspace_git_sync.workspaceGitMountsForRemediation)(configuredMounts) : configuredMounts;
4306
4324
  const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
4325
+ const expectedMirrorHeads = /* @__PURE__ */ new Map();
4326
+ for (const bundle of observedProjectHeads) {
4327
+ for (const observation of bundle.observations) {
4328
+ expectedMirrorHeads.set(projectMountId(bundle.projectId, observation.branchName), observation.mirrorHead);
4329
+ }
4330
+ }
4307
4331
  const inboundMoveMountIds = new Set(
4308
4332
  observedProjectHeads.flatMap(
4309
4333
  ({ projectId, observations }) => observations.filter(({ shouldMove }) => shouldMove).map(({ branchName }) => projectMountId(projectId, branchName))
@@ -4325,12 +4349,17 @@ async function startWorker(options) {
4325
4349
  requiredAncestorHeads: input.requiredAncestorHeads,
4326
4350
  assertStillAdmitted: input.assertStillAdmitted,
4327
4351
  afterWorkspacePublished: outerRemediation ? void 0 : async ({ activeMountIds, publishedHead }) => {
4328
- await pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, inboundMoveMountIds);
4352
+ await pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, inboundMoveMountIds);
4329
4353
  }
4330
4354
  });
4331
4355
  input.assertStillAdmitted?.();
4332
4356
  if (["no_change", "updated", "pushed"].includes(result.outcome) && !outerRemediation) {
4333
- await applyObservedProjectHeadsAfterInboundWorkspace(observedProjectHeads, result.activeMountIds, false);
4357
+ await applyObservedProjectHeadsAfterInboundWorkspace(
4358
+ observedProjectHeads,
4359
+ result.activeMountIds,
4360
+ false,
4361
+ result.localHead ?? result.remoteHead
4362
+ );
4334
4363
  }
4335
4364
  return mapWorkspaceGitResult(input.attemptId, input.trigger, result);
4336
4365
  };
@@ -4425,11 +4454,12 @@ async function startWorker(options) {
4425
4454
  }
4426
4455
  try {
4427
4456
  assertStillAdmitted();
4457
+ const hasDurableBranchDeletion = projectWorkspaceState.tombstones.some((tombstone) => tombstone.kind === "branch");
4428
4458
  return await performWorkspaceSync({
4429
4459
  attemptId,
4430
4460
  trigger: input.trigger,
4431
- confirmedLargeDiff: input.confirmedLargeDiff,
4432
- confirmationReason: input.confirmationReason,
4461
+ confirmedLargeDiff: input.confirmedLargeDiff === true || hasDurableBranchDeletion,
4462
+ confirmationReason: input.confirmationReason ?? (hasDurableBranchDeletion ? "durable project branch deletion" : void 0),
4433
4463
  resetToCanonical: input.resetToCanonical,
4434
4464
  requiredAncestorHeads: input.requiredAncestorHeads,
4435
4465
  assertStillAdmitted
@@ -4966,9 +4996,12 @@ async function startWorker(options) {
4966
4996
  );
4967
4997
  }
4968
4998
  };
4999
+ const hasDurableBranchDeletion = projectWorkspaceState.tombstones.some((tombstone) => tombstone.kind === "branch");
4969
5000
  const result = await performWorkspaceSync({
4970
5001
  attemptId: crypto.randomUUID(),
4971
5002
  trigger: { type: "connect" },
5003
+ confirmedLargeDiff: hasDurableBranchDeletion,
5004
+ confirmationReason: hasDurableBranchDeletion ? "durable project branch deletion" : void 0,
4972
5005
  resetToCanonical: workspaceConfigurationResetToCanonicalIsAllowed(message.resetToCanonical, incidentDeferral.incidentId),
4973
5006
  assertStillAdmitted: assertConfigurationSyncStillAdmitted
4974
5007
  });
@@ -5210,7 +5243,9 @@ async function startWorker(options) {
5210
5243
  ptyEnvFilesV1: true,
5211
5244
  workspaceRemediationAncestorGuardV1: true,
5212
5245
  workspaceIncidentConfigDeferralV1: true,
5213
- workspaceConfigResetToCanonicalV1: true
5246
+ workspaceConfigResetToCanonicalV1: true,
5247
+ projectBranchDeletionFastAckV1: true,
5248
+ projectMirrorLeaseV1: true
5214
5249
  },
5215
5250
  projectRoot: projectsRoot,
5216
5251
  artifactRoot,
@@ -5464,23 +5499,15 @@ async function startWorker(options) {
5464
5499
  return true;
5465
5500
  });
5466
5501
  if (deletionNeedsSync) {
5467
- const result = await runWorkspaceSync({
5468
- trigger: {
5502
+ scheduleAutomaticWorkspaceSync(
5503
+ {
5469
5504
  type: "manual",
5470
5505
  projectId: message.projectId,
5471
5506
  branchName: message.branchName,
5472
5507
  detail: "deleted project branch"
5473
5508
  },
5474
- confirmedLargeDiff: true,
5475
- confirmationReason: "explicit project branch deletion",
5476
- sendResult: false
5477
- });
5478
- if (result.outcome === "failed" || result.outcome === "conflict_blocked" || result.outcome === "large_diff_blocked") {
5479
- throw new Error(result.error ?? `Project branch deletion workspace sync ended with ${result.outcome}`);
5480
- }
5481
- if (pendingMirrorDeletes.has(deletionKey)) {
5482
- throw new Error("Project branch deletion did not remove the hidden mirror ref");
5483
- }
5509
+ 0
5510
+ );
5484
5511
  }
5485
5512
  sendWorkerMessage(ws, {
5486
5513
  type: "operation_result",
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.92",
3
+ "version": "0.0.94",
4
4
  "type": "commonjs"
5
5
  }
@@ -64,6 +64,7 @@ const PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION = 1;
64
64
  const MAX_PROJECT_WORKTREE_SNAPSHOT_MANIFEST_BYTES = 64 * 1024;
65
65
  const PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID = (0, import_node_crypto.randomUUID)();
66
66
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
67
+ const PROJECT_GIT_TRANSPORT_TIMEOUT_MS = 3e4;
67
68
  const PROJECT_WORKTREE_SNAPSHOT_NAME = new RegExp(
68
69
  `^${PROJECT_WORKTREE_SNAPSHOT_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([1-9]\\d*)-([A-Za-z0-9]{6})$`
69
70
  );
@@ -133,7 +134,7 @@ function git(cwd, args, action) {
133
134
  function tryGit(cwd, args) {
134
135
  return gitResult(cwd, args).exitCode === 0;
135
136
  }
136
- async function gitResultAsync(cwd, args) {
137
+ async function gitResultAsync(cwd, args, timeoutMs) {
137
138
  const subprocess = Bun.spawn(gitCommandArgs(args), {
138
139
  cwd,
139
140
  stdin: "ignore",
@@ -141,12 +142,22 @@ async function gitResultAsync(cwd, args) {
141
142
  stderr: "pipe",
142
143
  env: (0, import_git_process_environment.workerGitProcessEnvironment)()
143
144
  });
144
- const [stdout, stderr, exitCode] = await Promise.all([
145
- new Response(subprocess.stdout).text(),
146
- new Response(subprocess.stderr).text(),
147
- subprocess.exited
148
- ]);
149
- return { exitCode, stdout: stdout.trim(), stderr: stderr.trim() };
145
+ let timedOut = false;
146
+ const timeout = timeoutMs ? setTimeout(() => {
147
+ timedOut = true;
148
+ subprocess.kill("SIGKILL");
149
+ }, timeoutMs) : void 0;
150
+ try {
151
+ const [stdout, stderr, exitCode] = await Promise.all([
152
+ new Response(subprocess.stdout).text(),
153
+ new Response(subprocess.stderr).text(),
154
+ subprocess.exited
155
+ ]);
156
+ if (timedOut) throw new Error(`Git transport timed out after ${timeoutMs}ms`);
157
+ return { exitCode, stdout: stdout.trim(), stderr: stderr.trim() };
158
+ } finally {
159
+ if (timeout) clearTimeout(timeout);
160
+ }
150
161
  }
151
162
  async function gitAsync(cwd, args, action) {
152
163
  const result = await gitResultAsync(cwd, args);
@@ -861,25 +872,33 @@ async function configureRepository(input) {
861
872
  }
862
873
  }
863
874
  async function fetchProjectHeads(input) {
864
- const originFetch = await gitResultAsync(input.primaryPath, [
865
- ...(0, import_git_process_environment.gitTransportSecurityArgs)(input.originUrl, input.credentialHelper, input.originCredentialUsername),
866
- "fetch",
867
- "--no-recurse-submodules",
868
- "--prune",
869
- "origin",
870
- "+refs/heads/*:refs/remotes/origin/*"
871
- ]);
875
+ const originFetch = await gitResultAsync(
876
+ input.primaryPath,
877
+ [
878
+ ...(0, import_git_process_environment.gitTransportSecurityArgs)(input.originUrl, input.credentialHelper, input.originCredentialUsername),
879
+ "fetch",
880
+ "--no-recurse-submodules",
881
+ "--prune",
882
+ "origin",
883
+ "+refs/heads/*:refs/remotes/origin/*"
884
+ ],
885
+ PROJECT_GIT_TRANSPORT_TIMEOUT_MS
886
+ );
872
887
  if (originFetch.exitCode !== 0) {
873
888
  throw new Error(`Fetch project origin: ${originFetch.stderr || originFetch.stdout || `git exited ${originFetch.exitCode}`}`);
874
889
  }
875
- const mirrorFetch = await gitResultAsync(input.primaryPath, [
876
- ...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.mirrorCredentialUsername),
877
- "fetch",
878
- "--no-recurse-submodules",
879
- "--prune",
880
- input.mirrorUrl,
881
- "+refs/heads/*:refs/r5d/mirror/*"
882
- ]);
890
+ const mirrorFetch = await gitResultAsync(
891
+ input.primaryPath,
892
+ [
893
+ ...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.mirrorCredentialUsername),
894
+ "fetch",
895
+ "--no-recurse-submodules",
896
+ "--prune",
897
+ input.mirrorUrl,
898
+ "+refs/heads/*:refs/r5d/mirror/*"
899
+ ],
900
+ PROJECT_GIT_TRANSPORT_TIMEOUT_MS
901
+ );
883
902
  if (mirrorFetch.exitCode !== 0 && !/(couldn't find remote ref|does not have any commits|remote repository is empty|no such ref)/i.test(
884
903
  `${mirrorFetch.stderr}
885
904
  ${mirrorFetch.stdout}`
@@ -1258,13 +1277,17 @@ function removeProjectWorktrees(input) {
1258
1277
  }
1259
1278
  async function deleteProjectMirrorBranch(input) {
1260
1279
  (0, import_managed_paths.validateManagedBranchName)(input.branchName);
1261
- const deleted = await gitResultAsync(input.gitDirectory, [
1262
- ...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
1263
- "push",
1264
- "--no-recurse-submodules",
1265
- input.mirrorUrl,
1266
- `:refs/heads/${input.branchName}`
1267
- ]);
1280
+ const deleted = await gitResultAsync(
1281
+ input.gitDirectory,
1282
+ [
1283
+ ...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
1284
+ "push",
1285
+ "--no-recurse-submodules",
1286
+ input.mirrorUrl,
1287
+ `:refs/heads/${input.branchName}`
1288
+ ],
1289
+ PROJECT_GIT_TRANSPORT_TIMEOUT_MS
1290
+ );
1268
1291
  if (deleted.exitCode !== 0 && !/(remote ref does not exist|unable to delete|no such ref)/i.test(`${deleted.stderr}
1269
1292
  ${deleted.stdout}`)) {
1270
1293
  throw new Error(
@@ -1284,21 +1307,50 @@ async function pushProjectMirrorHeads(input) {
1284
1307
  if (!await tryGitAsync(checkoutPath, ["cat-file", "-e", `${head}^{commit}`])) {
1285
1308
  throw new Error(`Exact publication head ${head} is unavailable for project branch ${branchName}`);
1286
1309
  }
1310
+ if (!input.expectedMirrorHeads.has(branchName)) {
1311
+ throw new Error(`Missing hidden-mirror lease for project branch ${branchName}`);
1312
+ }
1313
+ const expectedMirrorHead = input.expectedMirrorHeads.get(branchName) ?? null;
1314
+ if (expectedMirrorHead !== null && !/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(expectedMirrorHead)) {
1315
+ throw new Error(`Invalid hidden-mirror lease for project branch ${branchName}`);
1316
+ }
1317
+ const branchId = input.branchIds.get(branchName);
1318
+ if (!branchId || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(branchId)) {
1319
+ throw new Error(`Missing valid branch incarnation for project branch ${branchName}`);
1320
+ }
1321
+ if (expectedMirrorHead) {
1322
+ const handoffPrefix = `refs/r5d/server-synced/${branchId}/`;
1323
+ const markedCommits = (await gitAsync(
1324
+ checkoutPath,
1325
+ ["for-each-ref", "--format=%(objectname)", handoffPrefix],
1326
+ `inspect synchronized server commits before publishing ${branchName}`
1327
+ )).split(/\r?\n/).filter((value) => /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(value));
1328
+ for (const markedCommit of markedCommits) {
1329
+ const markerIsInLease = await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", markedCommit, expectedMirrorHead]);
1330
+ if (markerIsInLease && !await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", markedCommit, head])) {
1331
+ throw new Error(`Refusing to overwrite synchronized server commit history for project branch ${branchName}`);
1332
+ }
1333
+ }
1334
+ }
1287
1335
  if (projectWorktreeOperationInProgress(checkoutPath)) {
1288
1336
  results.push({ branchName, head, pushed: false, reason: "git_operation_in_progress" });
1289
1337
  continue;
1290
1338
  }
1291
- await gitAsync(
1339
+ const pushed = await gitResultAsync(
1292
1340
  checkoutPath,
1293
1341
  [
1294
1342
  ...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
1295
1343
  "push",
1296
1344
  "--no-recurse-submodules",
1345
+ `--force-with-lease=refs/heads/${branchName}:${expectedMirrorHead ?? ""}`,
1297
1346
  input.mirrorUrl,
1298
- `+${head}:refs/heads/${branchName}`
1347
+ `${head}:refs/heads/${branchName}`
1299
1348
  ],
1300
- `push hidden project mirror for ${branchName}`
1349
+ PROJECT_GIT_TRANSPORT_TIMEOUT_MS
1301
1350
  );
1351
+ if (pushed.exitCode !== 0) {
1352
+ throw new Error(`Push hidden project mirror for ${branchName}: ${pushed.stderr || pushed.stdout || `git exited ${pushed.exitCode}`}`);
1353
+ }
1302
1354
  results.push({ branchName, head, pushed: true });
1303
1355
  }
1304
1356
  return results;
@@ -1311,14 +1363,19 @@ function assertProjectMirrorHeadsPushed(results) {
1311
1363
  }
1312
1364
  async function observeProjectMirrorHeads(input) {
1313
1365
  const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
1314
- const fetch = await gitResultAsync(primaryPath, [
1315
- ...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
1316
- "fetch",
1317
- "--no-recurse-submodules",
1318
- "--prune",
1319
- input.mirrorUrl,
1320
- "+refs/heads/*:refs/r5d/mirror/*"
1321
- ]);
1366
+ const fetch = await gitResultAsync(
1367
+ primaryPath,
1368
+ [
1369
+ ...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
1370
+ "fetch",
1371
+ "--no-recurse-submodules",
1372
+ "--prune",
1373
+ input.mirrorUrl,
1374
+ "+refs/heads/*:refs/r5d/mirror/*",
1375
+ "+refs/r5d/server-synced/*:refs/r5d/server-synced/*"
1376
+ ],
1377
+ PROJECT_GIT_TRANSPORT_TIMEOUT_MS
1378
+ );
1322
1379
  if (fetch.exitCode !== 0)
1323
1380
  throw new Error(`Refresh project head mirror: ${fetch.stderr || fetch.stdout || `git exited ${fetch.exitCode}`}`);
1324
1381
  const observations = [];
@@ -1327,14 +1384,40 @@ async function observeProjectMirrorHeads(input) {
1327
1384
  const previousHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve local project head ${branchName}`);
1328
1385
  const mirrorRef = `refs/r5d/mirror/${branchName}`;
1329
1386
  const mirrorHead = await tryGitAsync(primaryPath, ["show-ref", "--verify", "--quiet", mirrorRef]) ? await gitAsync(primaryPath, ["rev-parse", mirrorRef], `resolve refreshed mirror head ${branchName}`) : null;
1330
- const shouldMove = Boolean(
1331
- mirrorHead && mirrorHead !== previousHead && (input.allowNonFastForward || await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", previousHead, mirrorHead]))
1387
+ const branchId = input.branchIds?.get(branchName);
1388
+ if (branchId && !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(branchId)) throw new Error(`Invalid branch id for ${branchName}`);
1389
+ const handoffPrefix = branchId ? `refs/r5d/server-synced/${branchId}/` : null;
1390
+ const handoffRows = handoffPrefix ? await gitAsync(
1391
+ primaryPath,
1392
+ ["for-each-ref", "--format=%(objectname) %(refname)", handoffPrefix],
1393
+ `inspect synchronized server commits for ${branchName}`
1394
+ ) : "";
1395
+ const serverSyncedWorkspaceHeads = (await Promise.all(
1396
+ handoffRows.split(/\r?\n/).map(async (row) => {
1397
+ const separator = row.indexOf(" ");
1398
+ if (separator < 0 || !mirrorHead) return null;
1399
+ const markedCommit = row.slice(0, separator);
1400
+ const ref = row.slice(separator + 1);
1401
+ if (!/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(markedCommit) || !handoffPrefix || !ref.startsWith(handoffPrefix)) {
1402
+ return null;
1403
+ }
1404
+ const markerIsIntegrated = markedCommit === mirrorHead || await tryGitAsync(primaryPath, ["merge-base", "--is-ancestor", markedCommit, mirrorHead]);
1405
+ if (!markerIsIntegrated) return null;
1406
+ const workspaceHead = ref.slice(handoffPrefix.length);
1407
+ return /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(workspaceHead) ? workspaceHead : null;
1408
+ })
1409
+ )).filter((workspaceHead) => workspaceHead !== null).sort();
1410
+ const fastForward = Boolean(
1411
+ mirrorHead && mirrorHead !== previousHead && await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", previousHead, mirrorHead])
1332
1412
  );
1413
+ const serverSynced = Boolean(mirrorHead && mirrorHead !== previousHead && serverSyncedWorkspaceHeads.length > 0);
1414
+ const shouldMove = Boolean(mirrorHead && mirrorHead !== previousHead && (input.allowNonFastForward || fastForward || serverSynced));
1333
1415
  observations.push({
1334
1416
  branchName,
1335
1417
  previousHead,
1336
1418
  mirrorHead,
1337
- mode: input.allowNonFastForward ? "reseed" : "fast_forward",
1419
+ mode: input.allowNonFastForward ? "reseed" : serverSynced && !fastForward ? "server_synced" : "fast_forward",
1420
+ serverSyncedWorkspaceHeads,
1338
1421
  shouldMove
1339
1422
  });
1340
1423
  }
@@ -1346,7 +1429,8 @@ async function applyObservedProjectMirrorHeads(input) {
1346
1429
  const { branchName, mirrorHead } = observation;
1347
1430
  const checkoutPath = branchPath(input.projectRoot, branchName);
1348
1431
  const currentHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve local project head ${branchName}`);
1349
- if (!observation.shouldMove || !mirrorHead || currentHead === mirrorHead || currentHead !== observation.previousHead) {
1432
+ const serverSyncedCovered = observation.mode !== "server_synced" || (input.serverSyncedWorkspaceCovered ? await input.serverSyncedWorkspaceCovered(observation) : false);
1433
+ if (!observation.shouldMove || !serverSyncedCovered || !mirrorHead || currentHead === mirrorHead || currentHead !== observation.previousHead) {
1350
1434
  results.push({ branchName, previousHead: currentHead, mirrorHead, moved: false });
1351
1435
  continue;
1352
1436
  }
@@ -1358,14 +1442,14 @@ async function applyObservedProjectMirrorHeads(input) {
1358
1442
  results.push({ branchName, previousHead: currentHead, mirrorHead, moved: false });
1359
1443
  continue;
1360
1444
  }
1361
- if (observation.mode === "reseed") {
1445
+ if (observation.mode === "reseed" || observation.mode === "server_synced") {
1362
1446
  await preserveHead(checkoutPath);
1363
1447
  if (!mutationAllowed()) {
1364
1448
  results.push({ branchName, previousHead: currentHead, mirrorHead, moved: false });
1365
1449
  continue;
1366
1450
  }
1367
1451
  }
1368
- const action = observation.mode === "reseed" ? "reseed" : "fast-forward";
1452
+ const action = observation.mode === "reseed" ? "reseed" : observation.mode === "server_synced" ? "adopt server commit" : "fast-forward";
1369
1453
  await gitAsync(checkoutPath, ["read-tree", "--reset", mirrorHead], `${action} project index ${branchName}`);
1370
1454
  await gitAsync(
1371
1455
  checkoutPath,
package/dist/mjs/main.mjs CHANGED
@@ -4073,7 +4073,7 @@ async function startWorker(options) {
4073
4073
  }
4074
4074
  return heads;
4075
4075
  };
4076
- const pushChangedProjectHeads = async (activeMountIds, publishedHead, publicationHeads, suppressedMountIds = /* @__PURE__ */ new Set()) => {
4076
+ const pushChangedProjectHeads = async (activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, suppressedMountIds = /* @__PURE__ */ new Set()) => {
4077
4077
  const active = new Set(activeMountIds);
4078
4078
  for (const project of projectConfigById.values()) {
4079
4079
  if (!readyProjectIds.has(project.projectId) || !repositoryMirrorWritesAllowed(project)) continue;
@@ -4088,6 +4088,12 @@ async function startWorker(options) {
4088
4088
  }
4089
4089
  }
4090
4090
  if (changed.size === 0) continue;
4091
+ const branchLeases = /* @__PURE__ */ new Map();
4092
+ for (const branchName of changed.keys()) {
4093
+ const mountId = projectMountId(project.projectId, branchName);
4094
+ if (!expectedMirrorHeads.has(mountId)) throw new Error(`Missing observed hidden-mirror lease for active project mount ${mountId}`);
4095
+ branchLeases.set(branchName, expectedMirrorHeads.get(mountId) ?? null);
4096
+ }
4091
4097
  const connection = projectConnection(project);
4092
4098
  const results = await pushProjectMirrorHeads({
4093
4099
  projectRoot: configuredProjectRoot(projectsRoot, project),
@@ -4096,7 +4102,9 @@ async function startWorker(options) {
4096
4102
  credentialHelper: connection.credentialHelper,
4097
4103
  credentialUsername: connection.mirrorCredentialUsername,
4098
4104
  onlyBranches: new Set(changed.keys()),
4099
- publicationHeads: changed
4105
+ publicationHeads: changed,
4106
+ expectedMirrorHeads: branchLeases,
4107
+ branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId]))
4100
4108
  });
4101
4109
  assertProjectMirrorHeadsPushed(results);
4102
4110
  for (const result of results) {
@@ -4160,7 +4168,8 @@ async function startWorker(options) {
4160
4168
  mirrorUrl: connection.mirrorUrl,
4161
4169
  credentialHelper: connection.credentialHelper,
4162
4170
  credentialUsername: connection.mirrorCredentialUsername,
4163
- allowNonFastForward: input.allowNonFastForward
4171
+ allowNonFastForward: input.allowNonFastForward,
4172
+ branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId]))
4164
4173
  })
4165
4174
  });
4166
4175
  } catch (error) {
@@ -4173,7 +4182,7 @@ async function startWorker(options) {
4173
4182
  }
4174
4183
  return bundles;
4175
4184
  };
4176
- const applyObservedProjectHeadsAfterInboundWorkspace = async (bundles, activeMountIds, requireObservedHeads) => {
4185
+ const applyObservedProjectHeadsAfterInboundWorkspace = async (bundles, activeMountIds, requireObservedHeads, integratedWorkspaceHead) => {
4177
4186
  const active = new Set(activeMountIds);
4178
4187
  for (const bundle of bundles) {
4179
4188
  const project = projectConfigById.get(bundle.projectId);
@@ -4185,7 +4194,16 @@ async function startWorker(options) {
4185
4194
  projectRoot: bundle.projectRoot,
4186
4195
  primaryBranchName: bundle.primaryBranchName,
4187
4196
  observations,
4188
- branchMutationAllowed: (branchName, checkoutPath) => !projectBranchMountBusy(project.projectId, branchName, checkoutPath)
4197
+ branchMutationAllowed: (branchName, checkoutPath) => !projectBranchMountBusy(project.projectId, branchName, checkoutPath),
4198
+ serverSyncedWorkspaceCovered: async (observation) => {
4199
+ if (!integratedWorkspaceHead) return false;
4200
+ for (const workspaceHead of observation.serverSyncedWorkspaceHeads) {
4201
+ if (await tryGitAsync(["merge-base", "--is-ancestor", workspaceHead, integratedWorkspaceHead], { cwd: workspaceShadowRoot })) {
4202
+ return true;
4203
+ }
4204
+ }
4205
+ return false;
4206
+ }
4189
4207
  });
4190
4208
  if (requireObservedHeads) {
4191
4209
  for (const observation of observations) {
@@ -4310,6 +4328,12 @@ async function startWorker(options) {
4310
4328
  const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
4311
4329
  const mounts = outerRemediation ? workspaceGitMountsForRemediation(configuredMounts) : configuredMounts;
4312
4330
  const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
4331
+ const expectedMirrorHeads = /* @__PURE__ */ new Map();
4332
+ for (const bundle of observedProjectHeads) {
4333
+ for (const observation of bundle.observations) {
4334
+ expectedMirrorHeads.set(projectMountId(bundle.projectId, observation.branchName), observation.mirrorHead);
4335
+ }
4336
+ }
4313
4337
  const inboundMoveMountIds = new Set(
4314
4338
  observedProjectHeads.flatMap(
4315
4339
  ({ projectId, observations }) => observations.filter(({ shouldMove }) => shouldMove).map(({ branchName }) => projectMountId(projectId, branchName))
@@ -4331,12 +4355,17 @@ async function startWorker(options) {
4331
4355
  requiredAncestorHeads: input.requiredAncestorHeads,
4332
4356
  assertStillAdmitted: input.assertStillAdmitted,
4333
4357
  afterWorkspacePublished: outerRemediation ? void 0 : async ({ activeMountIds, publishedHead }) => {
4334
- await pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, inboundMoveMountIds);
4358
+ await pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, inboundMoveMountIds);
4335
4359
  }
4336
4360
  });
4337
4361
  input.assertStillAdmitted?.();
4338
4362
  if (["no_change", "updated", "pushed"].includes(result.outcome) && !outerRemediation) {
4339
- await applyObservedProjectHeadsAfterInboundWorkspace(observedProjectHeads, result.activeMountIds, false);
4363
+ await applyObservedProjectHeadsAfterInboundWorkspace(
4364
+ observedProjectHeads,
4365
+ result.activeMountIds,
4366
+ false,
4367
+ result.localHead ?? result.remoteHead
4368
+ );
4340
4369
  }
4341
4370
  return mapWorkspaceGitResult(input.attemptId, input.trigger, result);
4342
4371
  };
@@ -4431,11 +4460,12 @@ async function startWorker(options) {
4431
4460
  }
4432
4461
  try {
4433
4462
  assertStillAdmitted();
4463
+ const hasDurableBranchDeletion = projectWorkspaceState.tombstones.some((tombstone) => tombstone.kind === "branch");
4434
4464
  return await performWorkspaceSync({
4435
4465
  attemptId,
4436
4466
  trigger: input.trigger,
4437
- confirmedLargeDiff: input.confirmedLargeDiff,
4438
- confirmationReason: input.confirmationReason,
4467
+ confirmedLargeDiff: input.confirmedLargeDiff === true || hasDurableBranchDeletion,
4468
+ confirmationReason: input.confirmationReason ?? (hasDurableBranchDeletion ? "durable project branch deletion" : void 0),
4439
4469
  resetToCanonical: input.resetToCanonical,
4440
4470
  requiredAncestorHeads: input.requiredAncestorHeads,
4441
4471
  assertStillAdmitted
@@ -4972,9 +5002,12 @@ async function startWorker(options) {
4972
5002
  );
4973
5003
  }
4974
5004
  };
5005
+ const hasDurableBranchDeletion = projectWorkspaceState.tombstones.some((tombstone) => tombstone.kind === "branch");
4975
5006
  const result = await performWorkspaceSync({
4976
5007
  attemptId: crypto.randomUUID(),
4977
5008
  trigger: { type: "connect" },
5009
+ confirmedLargeDiff: hasDurableBranchDeletion,
5010
+ confirmationReason: hasDurableBranchDeletion ? "durable project branch deletion" : void 0,
4978
5011
  resetToCanonical: workspaceConfigurationResetToCanonicalIsAllowed(message.resetToCanonical, incidentDeferral.incidentId),
4979
5012
  assertStillAdmitted: assertConfigurationSyncStillAdmitted
4980
5013
  });
@@ -5216,7 +5249,9 @@ async function startWorker(options) {
5216
5249
  ptyEnvFilesV1: true,
5217
5250
  workspaceRemediationAncestorGuardV1: true,
5218
5251
  workspaceIncidentConfigDeferralV1: true,
5219
- workspaceConfigResetToCanonicalV1: true
5252
+ workspaceConfigResetToCanonicalV1: true,
5253
+ projectBranchDeletionFastAckV1: true,
5254
+ projectMirrorLeaseV1: true
5220
5255
  },
5221
5256
  projectRoot: projectsRoot,
5222
5257
  artifactRoot,
@@ -5470,23 +5505,15 @@ async function startWorker(options) {
5470
5505
  return true;
5471
5506
  });
5472
5507
  if (deletionNeedsSync) {
5473
- const result = await runWorkspaceSync({
5474
- trigger: {
5508
+ scheduleAutomaticWorkspaceSync(
5509
+ {
5475
5510
  type: "manual",
5476
5511
  projectId: message.projectId,
5477
5512
  branchName: message.branchName,
5478
5513
  detail: "deleted project branch"
5479
5514
  },
5480
- confirmedLargeDiff: true,
5481
- confirmationReason: "explicit project branch deletion",
5482
- sendResult: false
5483
- });
5484
- if (result.outcome === "failed" || result.outcome === "conflict_blocked" || result.outcome === "large_diff_blocked") {
5485
- throw new Error(result.error ?? `Project branch deletion workspace sync ended with ${result.outcome}`);
5486
- }
5487
- if (pendingMirrorDeletes.has(deletionKey)) {
5488
- throw new Error("Project branch deletion did not remove the hidden mirror ref");
5489
- }
5515
+ 0
5516
+ );
5490
5517
  }
5491
5518
  sendWorkerMessage(ws, {
5492
5519
  type: "operation_result",
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.92",
3
+ "version": "0.0.94",
4
4
  "type": "module"
5
5
  }
@@ -11,6 +11,7 @@ const PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION = 1;
11
11
  const MAX_PROJECT_WORKTREE_SNAPSHOT_MANIFEST_BYTES = 64 * 1024;
12
12
  const PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID = randomUUID();
13
13
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
14
+ const PROJECT_GIT_TRANSPORT_TIMEOUT_MS = 3e4;
14
15
  const PROJECT_WORKTREE_SNAPSHOT_NAME = new RegExp(
15
16
  `^${PROJECT_WORKTREE_SNAPSHOT_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([1-9]\\d*)-([A-Za-z0-9]{6})$`
16
17
  );
@@ -80,7 +81,7 @@ function git(cwd, args, action) {
80
81
  function tryGit(cwd, args) {
81
82
  return gitResult(cwd, args).exitCode === 0;
82
83
  }
83
- async function gitResultAsync(cwd, args) {
84
+ async function gitResultAsync(cwd, args, timeoutMs) {
84
85
  const subprocess = Bun.spawn(gitCommandArgs(args), {
85
86
  cwd,
86
87
  stdin: "ignore",
@@ -88,12 +89,22 @@ async function gitResultAsync(cwd, args) {
88
89
  stderr: "pipe",
89
90
  env: workerGitProcessEnvironment()
90
91
  });
91
- const [stdout, stderr, exitCode] = await Promise.all([
92
- new Response(subprocess.stdout).text(),
93
- new Response(subprocess.stderr).text(),
94
- subprocess.exited
95
- ]);
96
- return { exitCode, stdout: stdout.trim(), stderr: stderr.trim() };
92
+ let timedOut = false;
93
+ const timeout = timeoutMs ? setTimeout(() => {
94
+ timedOut = true;
95
+ subprocess.kill("SIGKILL");
96
+ }, timeoutMs) : void 0;
97
+ try {
98
+ const [stdout, stderr, exitCode] = await Promise.all([
99
+ new Response(subprocess.stdout).text(),
100
+ new Response(subprocess.stderr).text(),
101
+ subprocess.exited
102
+ ]);
103
+ if (timedOut) throw new Error(`Git transport timed out after ${timeoutMs}ms`);
104
+ return { exitCode, stdout: stdout.trim(), stderr: stderr.trim() };
105
+ } finally {
106
+ if (timeout) clearTimeout(timeout);
107
+ }
97
108
  }
98
109
  async function gitAsync(cwd, args, action) {
99
110
  const result = await gitResultAsync(cwd, args);
@@ -808,25 +819,33 @@ async function configureRepository(input) {
808
819
  }
809
820
  }
810
821
  async function fetchProjectHeads(input) {
811
- const originFetch = await gitResultAsync(input.primaryPath, [
812
- ...gitTransportSecurityArgs(input.originUrl, input.credentialHelper, input.originCredentialUsername),
813
- "fetch",
814
- "--no-recurse-submodules",
815
- "--prune",
816
- "origin",
817
- "+refs/heads/*:refs/remotes/origin/*"
818
- ]);
822
+ const originFetch = await gitResultAsync(
823
+ input.primaryPath,
824
+ [
825
+ ...gitTransportSecurityArgs(input.originUrl, input.credentialHelper, input.originCredentialUsername),
826
+ "fetch",
827
+ "--no-recurse-submodules",
828
+ "--prune",
829
+ "origin",
830
+ "+refs/heads/*:refs/remotes/origin/*"
831
+ ],
832
+ PROJECT_GIT_TRANSPORT_TIMEOUT_MS
833
+ );
819
834
  if (originFetch.exitCode !== 0) {
820
835
  throw new Error(`Fetch project origin: ${originFetch.stderr || originFetch.stdout || `git exited ${originFetch.exitCode}`}`);
821
836
  }
822
- const mirrorFetch = await gitResultAsync(input.primaryPath, [
823
- ...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.mirrorCredentialUsername),
824
- "fetch",
825
- "--no-recurse-submodules",
826
- "--prune",
827
- input.mirrorUrl,
828
- "+refs/heads/*:refs/r5d/mirror/*"
829
- ]);
837
+ const mirrorFetch = await gitResultAsync(
838
+ input.primaryPath,
839
+ [
840
+ ...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.mirrorCredentialUsername),
841
+ "fetch",
842
+ "--no-recurse-submodules",
843
+ "--prune",
844
+ input.mirrorUrl,
845
+ "+refs/heads/*:refs/r5d/mirror/*"
846
+ ],
847
+ PROJECT_GIT_TRANSPORT_TIMEOUT_MS
848
+ );
830
849
  if (mirrorFetch.exitCode !== 0 && !/(couldn't find remote ref|does not have any commits|remote repository is empty|no such ref)/i.test(
831
850
  `${mirrorFetch.stderr}
832
851
  ${mirrorFetch.stdout}`
@@ -1205,13 +1224,17 @@ function removeProjectWorktrees(input) {
1205
1224
  }
1206
1225
  async function deleteProjectMirrorBranch(input) {
1207
1226
  validateManagedBranchName(input.branchName);
1208
- const deleted = await gitResultAsync(input.gitDirectory, [
1209
- ...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
1210
- "push",
1211
- "--no-recurse-submodules",
1212
- input.mirrorUrl,
1213
- `:refs/heads/${input.branchName}`
1214
- ]);
1227
+ const deleted = await gitResultAsync(
1228
+ input.gitDirectory,
1229
+ [
1230
+ ...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
1231
+ "push",
1232
+ "--no-recurse-submodules",
1233
+ input.mirrorUrl,
1234
+ `:refs/heads/${input.branchName}`
1235
+ ],
1236
+ PROJECT_GIT_TRANSPORT_TIMEOUT_MS
1237
+ );
1215
1238
  if (deleted.exitCode !== 0 && !/(remote ref does not exist|unable to delete|no such ref)/i.test(`${deleted.stderr}
1216
1239
  ${deleted.stdout}`)) {
1217
1240
  throw new Error(
@@ -1231,21 +1254,50 @@ async function pushProjectMirrorHeads(input) {
1231
1254
  if (!await tryGitAsync(checkoutPath, ["cat-file", "-e", `${head}^{commit}`])) {
1232
1255
  throw new Error(`Exact publication head ${head} is unavailable for project branch ${branchName}`);
1233
1256
  }
1257
+ if (!input.expectedMirrorHeads.has(branchName)) {
1258
+ throw new Error(`Missing hidden-mirror lease for project branch ${branchName}`);
1259
+ }
1260
+ const expectedMirrorHead = input.expectedMirrorHeads.get(branchName) ?? null;
1261
+ if (expectedMirrorHead !== null && !/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(expectedMirrorHead)) {
1262
+ throw new Error(`Invalid hidden-mirror lease for project branch ${branchName}`);
1263
+ }
1264
+ const branchId = input.branchIds.get(branchName);
1265
+ if (!branchId || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(branchId)) {
1266
+ throw new Error(`Missing valid branch incarnation for project branch ${branchName}`);
1267
+ }
1268
+ if (expectedMirrorHead) {
1269
+ const handoffPrefix = `refs/r5d/server-synced/${branchId}/`;
1270
+ const markedCommits = (await gitAsync(
1271
+ checkoutPath,
1272
+ ["for-each-ref", "--format=%(objectname)", handoffPrefix],
1273
+ `inspect synchronized server commits before publishing ${branchName}`
1274
+ )).split(/\r?\n/).filter((value) => /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(value));
1275
+ for (const markedCommit of markedCommits) {
1276
+ const markerIsInLease = await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", markedCommit, expectedMirrorHead]);
1277
+ if (markerIsInLease && !await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", markedCommit, head])) {
1278
+ throw new Error(`Refusing to overwrite synchronized server commit history for project branch ${branchName}`);
1279
+ }
1280
+ }
1281
+ }
1234
1282
  if (projectWorktreeOperationInProgress(checkoutPath)) {
1235
1283
  results.push({ branchName, head, pushed: false, reason: "git_operation_in_progress" });
1236
1284
  continue;
1237
1285
  }
1238
- await gitAsync(
1286
+ const pushed = await gitResultAsync(
1239
1287
  checkoutPath,
1240
1288
  [
1241
1289
  ...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
1242
1290
  "push",
1243
1291
  "--no-recurse-submodules",
1292
+ `--force-with-lease=refs/heads/${branchName}:${expectedMirrorHead ?? ""}`,
1244
1293
  input.mirrorUrl,
1245
- `+${head}:refs/heads/${branchName}`
1294
+ `${head}:refs/heads/${branchName}`
1246
1295
  ],
1247
- `push hidden project mirror for ${branchName}`
1296
+ PROJECT_GIT_TRANSPORT_TIMEOUT_MS
1248
1297
  );
1298
+ if (pushed.exitCode !== 0) {
1299
+ throw new Error(`Push hidden project mirror for ${branchName}: ${pushed.stderr || pushed.stdout || `git exited ${pushed.exitCode}`}`);
1300
+ }
1249
1301
  results.push({ branchName, head, pushed: true });
1250
1302
  }
1251
1303
  return results;
@@ -1258,14 +1310,19 @@ function assertProjectMirrorHeadsPushed(results) {
1258
1310
  }
1259
1311
  async function observeProjectMirrorHeads(input) {
1260
1312
  const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
1261
- const fetch = await gitResultAsync(primaryPath, [
1262
- ...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
1263
- "fetch",
1264
- "--no-recurse-submodules",
1265
- "--prune",
1266
- input.mirrorUrl,
1267
- "+refs/heads/*:refs/r5d/mirror/*"
1268
- ]);
1313
+ const fetch = await gitResultAsync(
1314
+ primaryPath,
1315
+ [
1316
+ ...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
1317
+ "fetch",
1318
+ "--no-recurse-submodules",
1319
+ "--prune",
1320
+ input.mirrorUrl,
1321
+ "+refs/heads/*:refs/r5d/mirror/*",
1322
+ "+refs/r5d/server-synced/*:refs/r5d/server-synced/*"
1323
+ ],
1324
+ PROJECT_GIT_TRANSPORT_TIMEOUT_MS
1325
+ );
1269
1326
  if (fetch.exitCode !== 0)
1270
1327
  throw new Error(`Refresh project head mirror: ${fetch.stderr || fetch.stdout || `git exited ${fetch.exitCode}`}`);
1271
1328
  const observations = [];
@@ -1274,14 +1331,40 @@ async function observeProjectMirrorHeads(input) {
1274
1331
  const previousHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve local project head ${branchName}`);
1275
1332
  const mirrorRef = `refs/r5d/mirror/${branchName}`;
1276
1333
  const mirrorHead = await tryGitAsync(primaryPath, ["show-ref", "--verify", "--quiet", mirrorRef]) ? await gitAsync(primaryPath, ["rev-parse", mirrorRef], `resolve refreshed mirror head ${branchName}`) : null;
1277
- const shouldMove = Boolean(
1278
- mirrorHead && mirrorHead !== previousHead && (input.allowNonFastForward || await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", previousHead, mirrorHead]))
1334
+ const branchId = input.branchIds?.get(branchName);
1335
+ if (branchId && !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(branchId)) throw new Error(`Invalid branch id for ${branchName}`);
1336
+ const handoffPrefix = branchId ? `refs/r5d/server-synced/${branchId}/` : null;
1337
+ const handoffRows = handoffPrefix ? await gitAsync(
1338
+ primaryPath,
1339
+ ["for-each-ref", "--format=%(objectname) %(refname)", handoffPrefix],
1340
+ `inspect synchronized server commits for ${branchName}`
1341
+ ) : "";
1342
+ const serverSyncedWorkspaceHeads = (await Promise.all(
1343
+ handoffRows.split(/\r?\n/).map(async (row) => {
1344
+ const separator = row.indexOf(" ");
1345
+ if (separator < 0 || !mirrorHead) return null;
1346
+ const markedCommit = row.slice(0, separator);
1347
+ const ref = row.slice(separator + 1);
1348
+ if (!/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(markedCommit) || !handoffPrefix || !ref.startsWith(handoffPrefix)) {
1349
+ return null;
1350
+ }
1351
+ const markerIsIntegrated = markedCommit === mirrorHead || await tryGitAsync(primaryPath, ["merge-base", "--is-ancestor", markedCommit, mirrorHead]);
1352
+ if (!markerIsIntegrated) return null;
1353
+ const workspaceHead = ref.slice(handoffPrefix.length);
1354
+ return /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(workspaceHead) ? workspaceHead : null;
1355
+ })
1356
+ )).filter((workspaceHead) => workspaceHead !== null).sort();
1357
+ const fastForward = Boolean(
1358
+ mirrorHead && mirrorHead !== previousHead && await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", previousHead, mirrorHead])
1279
1359
  );
1360
+ const serverSynced = Boolean(mirrorHead && mirrorHead !== previousHead && serverSyncedWorkspaceHeads.length > 0);
1361
+ const shouldMove = Boolean(mirrorHead && mirrorHead !== previousHead && (input.allowNonFastForward || fastForward || serverSynced));
1280
1362
  observations.push({
1281
1363
  branchName,
1282
1364
  previousHead,
1283
1365
  mirrorHead,
1284
- mode: input.allowNonFastForward ? "reseed" : "fast_forward",
1366
+ mode: input.allowNonFastForward ? "reseed" : serverSynced && !fastForward ? "server_synced" : "fast_forward",
1367
+ serverSyncedWorkspaceHeads,
1285
1368
  shouldMove
1286
1369
  });
1287
1370
  }
@@ -1293,7 +1376,8 @@ async function applyObservedProjectMirrorHeads(input) {
1293
1376
  const { branchName, mirrorHead } = observation;
1294
1377
  const checkoutPath = branchPath(input.projectRoot, branchName);
1295
1378
  const currentHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve local project head ${branchName}`);
1296
- if (!observation.shouldMove || !mirrorHead || currentHead === mirrorHead || currentHead !== observation.previousHead) {
1379
+ const serverSyncedCovered = observation.mode !== "server_synced" || (input.serverSyncedWorkspaceCovered ? await input.serverSyncedWorkspaceCovered(observation) : false);
1380
+ if (!observation.shouldMove || !serverSyncedCovered || !mirrorHead || currentHead === mirrorHead || currentHead !== observation.previousHead) {
1297
1381
  results.push({ branchName, previousHead: currentHead, mirrorHead, moved: false });
1298
1382
  continue;
1299
1383
  }
@@ -1305,14 +1389,14 @@ async function applyObservedProjectMirrorHeads(input) {
1305
1389
  results.push({ branchName, previousHead: currentHead, mirrorHead, moved: false });
1306
1390
  continue;
1307
1391
  }
1308
- if (observation.mode === "reseed") {
1392
+ if (observation.mode === "reseed" || observation.mode === "server_synced") {
1309
1393
  await preserveHead(checkoutPath);
1310
1394
  if (!mutationAllowed()) {
1311
1395
  results.push({ branchName, previousHead: currentHead, mirrorHead, moved: false });
1312
1396
  continue;
1313
1397
  }
1314
1398
  }
1315
- const action = observation.mode === "reseed" ? "reseed" : "fast-forward";
1399
+ const action = observation.mode === "reseed" ? "reseed" : observation.mode === "server_synced" ? "adopt server commit" : "fast-forward";
1316
1400
  await gitAsync(checkoutPath, ["read-tree", "--reset", mirrorHead], `${action} project index ${branchName}`);
1317
1401
  await gitAsync(
1318
1402
  checkoutPath,
@@ -175,6 +175,10 @@ export declare function pushProjectMirrorHeads(input: {
175
175
  onlyBranches?: ReadonlySet<string>;
176
176
  /** Exact pre-projection commits to publish after the outer tree is reachable. */
177
177
  publicationHeads?: ReadonlyMap<string, string>;
178
+ /** Hidden-mirror tips observed before the corresponding outer projection. */
179
+ expectedMirrorHeads: ReadonlyMap<string, string | null>;
180
+ /** Durable branch incarnations used to scope server-authored handoff refs. */
181
+ branchIds: ReadonlyMap<string, string>;
178
182
  }): Promise<Array<{
179
183
  branchName: string;
180
184
  head: string;
@@ -200,7 +204,8 @@ export type ProjectMirrorHeadObservation = {
200
204
  branchName: string;
201
205
  previousHead: string;
202
206
  mirrorHead: string | null;
203
- mode: "fast_forward" | "reseed";
207
+ mode: "fast_forward" | "reseed" | "server_synced";
208
+ serverSyncedWorkspaceHeads: string[];
204
209
  shouldMove: boolean;
205
210
  };
206
211
  /**
@@ -210,6 +215,7 @@ export type ProjectMirrorHeadObservation = {
210
215
  */
211
216
  export declare function observeProjectMirrorHeads(input: Omit<ProjectMirrorHeadUpdateInput, "branchMutationAllowed"> & {
212
217
  allowNonFastForward: boolean;
218
+ branchIds?: ReadonlyMap<string, string>;
213
219
  }): Promise<ProjectMirrorHeadObservation[]>;
214
220
  /** Apply only previously observed object ids; this function never refetches. */
215
221
  export declare function applyObservedProjectMirrorHeads(input: {
@@ -218,6 +224,8 @@ export declare function applyObservedProjectMirrorHeads(input: {
218
224
  observations: readonly ProjectMirrorHeadObservation[];
219
225
  /** Rechecked immediately before refs or the index move. */
220
226
  branchMutationAllowed?: (branchName: string, checkoutPath: string) => boolean;
227
+ /** Proves the visible outer workspace has hydrated the server commit's exact source generation. */
228
+ serverSyncedWorkspaceCovered?: (observation: ProjectMirrorHeadObservation) => Promise<boolean>;
221
229
  }): Promise<Array<{
222
230
  branchName: string;
223
231
  previousHead: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.92",
3
+ "version": "0.0.94",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",