@ricsam/r5d-worker 0.0.119 → 0.0.121

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
@@ -940,24 +940,43 @@ function tryGit(args, options = {}) {
940
940
  return false;
941
941
  }
942
942
  }
943
+ class GitTransportTimeoutError extends Error {
944
+ }
943
945
  async function runGitAsync(args, options = {}) {
944
946
  const command = workerGitCommand.commandArgs(args);
947
+ const bounded = options.timeoutMs !== void 0;
945
948
  const subprocess = Bun.spawn(command, {
946
949
  cwd: options.cwd,
947
950
  stdin: "ignore",
948
951
  stdout: "pipe",
949
952
  stderr: "pipe",
950
- env: (0, import_git_process_environment.workerGitProcessEnvironment)()
953
+ env: (0, import_git_process_environment.workerGitProcessEnvironment)(),
954
+ ...bounded ? { detached: true } : {}
951
955
  });
952
- const [stdout, stderr, exitCode] = await Promise.all([
956
+ const completion = Promise.all([
953
957
  new Response(subprocess.stdout).text(),
954
958
  new Response(subprocess.stderr).text(),
955
959
  subprocess.exited
956
- ]);
957
- if (exitCode !== 0) {
958
- throw new Error(`git ${args.join(" ")} failed: ${stderr.trim() || stdout.trim() || `exit ${exitCode}`}`);
960
+ ]).then(([stdout, stderr, exitCode]) => ({ stdout, stderr, exitCode }));
961
+ let timer;
962
+ const timedOut = bounded ? new Promise((_resolve, reject) => {
963
+ timer = setTimeout(() => reject(new GitTransportTimeoutError(`git ${args.join(" ")} timed out after ${options.timeoutMs}ms`)), options.timeoutMs);
964
+ }) : null;
965
+ try {
966
+ const { stdout, stderr, exitCode } = timedOut ? await Promise.race([completion, timedOut]) : await completion;
967
+ if (exitCode !== 0) {
968
+ throw new Error(`git ${args.join(" ")} failed: ${stderr.trim() || stdout.trim() || `exit ${exitCode}`}`);
969
+ }
970
+ return stdout.trim();
971
+ } catch (error) {
972
+ if (error instanceof GitTransportTimeoutError) {
973
+ void (0, import_process_tree.terminateProcessTree)(subprocess, { graceMs: 0 }).catch(() => void 0);
974
+ void completion.catch(() => void 0);
975
+ }
976
+ throw error;
977
+ } finally {
978
+ if (timer) clearTimeout(timer);
959
979
  }
960
- return stdout.trim();
961
980
  }
962
981
  async function tryGitAsync(args, options = {}) {
963
982
  try {
@@ -1818,14 +1837,13 @@ const workerGitSecurityTestHarness = {
1818
1837
  executeEditFileOperation,
1819
1838
  terminateCredentialBearingChildren,
1820
1839
  terminateCredentialBearingChildrenWithRetention,
1821
- async commitCredentialGeneration(prepared, credential, children, beforeMutation, previousGenerationFenced = false) {
1822
- return await commitCredentialGeneration(
1823
- prepared,
1824
- credential,
1825
- async () => await terminateCredentialBearingChildren(children),
1826
- beforeMutation,
1827
- previousGenerationFenced
1828
- );
1840
+ async commitCredentialGeneration(prepared, credential, options) {
1841
+ return await commitCredentialGeneration(prepared, credential, {
1842
+ secretMaterialChanged: options.secretMaterialChanged,
1843
+ terminatePreviousGeneration: async () => await terminateCredentialBearingChildren(options.children),
1844
+ beforeMutation: options.beforeMutation,
1845
+ previousGenerationFenced: options.previousGenerationFenced ?? false
1846
+ });
1829
1847
  },
1830
1848
  credentialGenerationReceiptPath,
1831
1849
  credentialAuthorityLockPath,
@@ -1863,14 +1881,14 @@ function installGitHubCredentialGeneration(credential) {
1863
1881
  `);
1864
1882
  }
1865
1883
  }
1866
- async function commitCredentialGeneration(prepared, credential, terminatePreviousGeneration, beforeMutation, previousGenerationFenced = false) {
1884
+ async function commitCredentialGeneration(prepared, credential, options) {
1867
1885
  const changed = prepared.changed;
1868
1886
  try {
1869
- if (changed && !previousGenerationFenced) {
1887
+ if (changed && options.secretMaterialChanged && !options.previousGenerationFenced) {
1870
1888
  githubCredential = null;
1871
- await terminatePreviousGeneration();
1889
+ await options.terminatePreviousGeneration();
1872
1890
  }
1873
- prepared.commit(beforeMutation);
1891
+ prepared.commit(options.beforeMutation);
1874
1892
  } catch (error) {
1875
1893
  prepared.discard();
1876
1894
  throw error instanceof import_registry_auth.RegistryAuthConfigurationError ? error : new import_registry_auth.RegistryAuthConfigurationError(error);
@@ -4319,33 +4337,56 @@ async function startWorker(options, projectRuntime = {
4319
4337
  return reconciledProjectIds;
4320
4338
  };
4321
4339
  const ORIGIN_DIVERGENCE_REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
4340
+ const ORIGIN_DIVERGENCE_FETCH_TIMEOUT_MS = 3e4;
4341
+ const ORIGIN_DIVERGENCE_REFRESH_BUDGET_MS = 9e4;
4322
4342
  const lastOriginDivergenceFetchMs = /* @__PURE__ */ new Map();
4323
- const collectAheadOfOriginBranches = async (refetchOriginProjectIds) => {
4343
+ const collectAheadOfOriginBranches = async (refetchOriginProjectIds, onProgress) => {
4324
4344
  const aheadBranches = [];
4325
4345
  for (const projectId of [...lastOriginDivergenceFetchMs.keys()]) {
4326
4346
  if (!projectConfigById.has(projectId)) lastOriginDivergenceFetchMs.delete(projectId);
4327
4347
  }
4328
- for (const project of projectConfigById.values()) {
4348
+ const passStartedAtMs = Date.now();
4349
+ let budgetSpentReported = false;
4350
+ const projects = [...projectConfigById.values()];
4351
+ for (const [projectIndex, project] of projects.entries()) {
4329
4352
  if (project.executionDisabled || !readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
4353
+ onProgress?.({
4354
+ detail: `Checking origin divergence for ${project.projectPath}`,
4355
+ completedItems: projectIndex,
4356
+ totalItems: projects.length
4357
+ });
4330
4358
  const connection = projectConnection(project);
4331
4359
  const primaryPath = configuredProjectBranchPath(projectsRoot, project, primaryProjectBranch(project));
4332
4360
  const lastFetchedAtMs = lastOriginDivergenceFetchMs.get(project.projectId);
4333
4361
  if (refetchOriginProjectIds.has(project.projectId) || lastFetchedAtMs === void 0 || Date.now() - lastFetchedAtMs >= ORIGIN_DIVERGENCE_REFRESH_INTERVAL_MS) {
4334
- if (await tryGitAsync(
4335
- [
4336
- ...(0, import_git_process_environment.gitTransportSecurityArgs)(connection.originUrl, connection.credentialHelper, connection.originCredentialUsername),
4337
- "fetch",
4338
- "--no-recurse-submodules",
4339
- "--prune",
4340
- "origin",
4341
- "+refs/heads/*:refs/remotes/origin/*"
4342
- ],
4343
- { cwd: primaryPath }
4344
- )) {
4345
- lastOriginDivergenceFetchMs.set(project.projectId, Date.now());
4362
+ if (Date.now() - passStartedAtMs >= ORIGIN_DIVERGENCE_REFRESH_BUDGET_MS) {
4363
+ if (!budgetSpentReported) {
4364
+ budgetSpentReported = true;
4365
+ process.stderr.write(
4366
+ `[r5d-worker] origin divergence refresh budget of ${ORIGIN_DIVERGENCE_REFRESH_BUDGET_MS}ms is spent; remaining origins keep their previous comparison until the next refresh
4367
+ `
4368
+ );
4369
+ }
4346
4370
  } else {
4347
- process.stderr.write(`[r5d-worker] could not refresh origin divergence for ${project.projectPath}
4348
- `);
4371
+ try {
4372
+ await runGitAsync(
4373
+ [
4374
+ ...(0, import_git_process_environment.gitTransportSecurityArgs)(connection.originUrl, connection.credentialHelper, connection.originCredentialUsername),
4375
+ "fetch",
4376
+ "--no-recurse-submodules",
4377
+ "--prune",
4378
+ "origin",
4379
+ "+refs/heads/*:refs/remotes/origin/*"
4380
+ ],
4381
+ { cwd: primaryPath, timeoutMs: ORIGIN_DIVERGENCE_FETCH_TIMEOUT_MS }
4382
+ );
4383
+ lastOriginDivergenceFetchMs.set(project.projectId, Date.now());
4384
+ } catch (error) {
4385
+ process.stderr.write(
4386
+ `[r5d-worker] could not refresh origin divergence for ${project.projectPath}: ${error instanceof Error ? error.message : String(error)}
4387
+ `
4388
+ );
4389
+ }
4349
4390
  }
4350
4391
  }
4351
4392
  for (const branch of project.branches) {
@@ -5378,13 +5419,11 @@ async function startWorker(options, projectRuntime = {
5378
5419
  } catch (error) {
5379
5420
  throw error instanceof import_registry_auth.RegistryAuthConfigurationError ? error : new import_registry_auth.RegistryAuthConfigurationError(error);
5380
5421
  }
5381
- await commitCredentialGeneration(
5382
- preparedAuthGeneration,
5383
- message.githubCredential,
5384
- terminateActiveCredentialBearingChildren,
5385
- void 0,
5386
- credentialPublicationPreauthorized
5387
- );
5422
+ await commitCredentialGeneration(preparedAuthGeneration, message.githubCredential, {
5423
+ secretMaterialChanged: credentialTransitionPhase !== "current",
5424
+ terminatePreviousGeneration: terminateActiveCredentialBearingChildren,
5425
+ previousGenerationFenced: credentialPublicationPreauthorized
5426
+ });
5388
5427
  if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
5389
5428
  throw new Error("Workspace configuration was superseded by a newer server generation");
5390
5429
  }
@@ -5519,7 +5558,15 @@ async function startWorker(options, projectRuntime = {
5519
5558
  }
5520
5559
  };
5521
5560
  const hasDurableBranchDeletion = projectWorkspaceState.tombstones.some((tombstone) => tombstone.kind === "branch");
5522
- const result = await performWorkspaceSync({
5561
+ sendLifecycleProgress({
5562
+ phase: "configuring",
5563
+ operationId: configurationOperationId,
5564
+ detail: `Synchronizing the workspace after configuring ${message.projects.length} project(s)`,
5565
+ completedItems: message.projects.length,
5566
+ totalItems: message.projects.length
5567
+ });
5568
+ const connectSyncStartedAtMs = Date.now();
5569
+ const syncResult = await performWorkspaceSync({
5523
5570
  attemptId: crypto.randomUUID(),
5524
5571
  trigger: { type: "connect" },
5525
5572
  confirmedLargeDiff: hasDurableBranchDeletion,
@@ -5527,6 +5574,11 @@ async function startWorker(options, projectRuntime = {
5527
5574
  resetToCanonical: workspaceConfigurationResetToCanonicalIsAllowed(message.resetToCanonical, incidentDeferral.incidentId),
5528
5575
  assertStillAdmitted: assertConfigurationSyncStillAdmitted
5529
5576
  });
5577
+ const connectSyncMs = Date.now() - connectSyncStartedAtMs;
5578
+ const result = syncResult.telemetry ? syncResult : {
5579
+ ...syncResult,
5580
+ telemetry: { totalMs: connectSyncMs, queueMs: 0, prepareMs: lastWorkspaceSyncMirrorObservationMs, synchronizeMs: connectSyncMs }
5581
+ };
5530
5582
  assertConfigurationSyncStillAdmitted();
5531
5583
  if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
5532
5584
  throw new Error("Workspace configuration was superseded by a newer server generation");
@@ -5559,7 +5611,14 @@ async function startWorker(options, projectRuntime = {
5559
5611
  const pending = [...pendingCheckouts.values()].sort(
5560
5612
  (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
5561
5613
  );
5562
- return { result, pending, aheadOfOriginBranches: await collectAheadOfOriginBranches(reconciledProjectIds) };
5614
+ return {
5615
+ result,
5616
+ pending,
5617
+ aheadOfOriginBranches: await collectAheadOfOriginBranches(
5618
+ reconciledProjectIds,
5619
+ (progress) => sendLifecycleProgress({ phase: "configuring", operationId: configurationOperationId, ...progress })
5620
+ )
5621
+ };
5563
5622
  });
5564
5623
  } catch (error) {
5565
5624
  if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
@@ -5913,7 +5972,15 @@ async function startWorker(options, projectRuntime = {
5913
5972
  }
5914
5973
  if (message.type === "workspace_config") {
5915
5974
  const receiptGeneration = ++workspaceConfigurationReceiptGeneration;
5916
- const refreshesDeferredConfiguration = deferredWorkspaceConfiguration !== null && activeWorkspaceIncidentId === null && message.deferWorkspaceSyncForIncidentId === void 0;
5975
+ const refreshesDeferredConfiguration = (0, import_workspace_incident_state.workspaceConfigurationRefreshesDeferredConfiguration)({
5976
+ deferredIncidentId: deferredWorkspaceConfiguration?.incidentId ?? null,
5977
+ activeIncidentId: activeWorkspaceIncidentId,
5978
+ requestedDeferredIncidentId: message.deferWorkspaceSyncForIncidentId
5979
+ });
5980
+ if (refreshesDeferredConfiguration && deferredWorkspaceConfigurationRefreshTimer) {
5981
+ clearTimeout(deferredWorkspaceConfigurationRefreshTimer);
5982
+ deferredWorkspaceConfigurationRefreshTimer = void 0;
5983
+ }
5917
5984
  const digest = (0, import_recovery_store.workerOperationDigest)(message);
5918
5985
  const cached = projectRuntime.configuration;
5919
5986
  const canReuseConfiguration = cached?.digest === digest && cached.workspaceSync?.remoteUrl === message.workspaceRemoteUrl && Boolean(cached.workspaceSync.credentialHelper) && cached.workspaceSync.credentialUsername === workerCredentialUsername && cached.workspaceSync.gitIdentity?.name === message.gitIdentity.name && cached.workspaceSync.gitIdentity?.email === message.gitIdentity.email && !unsafeWorkspaceSyncBouncePending && credentialGenerationFingerprint({ ...message, workerBaseUrl: baseUrl, workerAuthHeader: bearerAuthHeader }) === configuredCredentialGenerationFingerprint;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.119",
3
+ "version": "0.0.121",
4
4
  "type": "commonjs"
5
5
  }
@@ -2367,7 +2367,9 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
2367
2367
  }
2368
2368
  restoreDeferredWorkspaceMountsFromHead(
2369
2369
  workspacePath,
2370
- input.mounts.filter(({ id }) => deferredMountIds.has(id))
2370
+ input.mounts.filter(
2371
+ (mount) => deferredMountIds.has(mount.id) || mount.preserveLocalOnHydrationBasisChange === true && cycleSkippedMountIds.has(mount.id)
2372
+ )
2371
2373
  );
2372
2374
  await stageAndCommitWorkspace();
2373
2375
  await fsyncWorkspaceCheckoutTreeYielding(workspacePath);
@@ -18,7 +18,8 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var workspace_incident_state_exports = {};
20
20
  __export(workspace_incident_state_exports, {
21
- applyWorkspaceIncidentUpdate: () => applyWorkspaceIncidentUpdate
21
+ applyWorkspaceIncidentUpdate: () => applyWorkspaceIncidentUpdate,
22
+ workspaceConfigurationRefreshesDeferredConfiguration: () => workspaceConfigurationRefreshesDeferredConfiguration
22
23
  });
23
24
  module.exports = __toCommonJS(workspace_incident_state_exports);
24
25
  function applyWorkspaceIncidentUpdate(currentIncidentId, update) {
@@ -26,7 +27,11 @@ function applyWorkspaceIncidentUpdate(currentIncidentId, update) {
26
27
  if (currentIncidentId && update.incidentId && update.incidentId !== currentIncidentId) return currentIncidentId;
27
28
  return null;
28
29
  }
30
+ function workspaceConfigurationRefreshesDeferredConfiguration(input) {
31
+ return input.deferredIncidentId !== null && input.activeIncidentId === null && input.requestedDeferredIncidentId === void 0;
32
+ }
29
33
  // Annotate the CommonJS export names for ESM import in node:
30
34
  0 && (module.exports = {
31
- applyWorkspaceIncidentUpdate
35
+ applyWorkspaceIncidentUpdate,
36
+ workspaceConfigurationRefreshesDeferredConfiguration
32
37
  });
package/dist/mjs/main.mjs CHANGED
@@ -34,7 +34,7 @@ import {
34
34
  registryAuthHasAppManagedGitHubCredential,
35
35
  RegistryAuthConfigurationError
36
36
  } from "./registry-auth.mjs";
37
- import { applyWorkspaceIncidentUpdate } from "./workspace-incident-state.mjs";
37
+ import { applyWorkspaceIncidentUpdate, workspaceConfigurationRefreshesDeferredConfiguration } from "./workspace-incident-state.mjs";
38
38
  import {
39
39
  hasActiveVisibleProjectsWorkspaceTarget,
40
40
  PENDING_CREATED_BRANCH_AUTOMATIC_SYNC_GRACE_MS,
@@ -959,24 +959,43 @@ function tryGit(args, options = {}) {
959
959
  return false;
960
960
  }
961
961
  }
962
+ class GitTransportTimeoutError extends Error {
963
+ }
962
964
  async function runGitAsync(args, options = {}) {
963
965
  const command = workerGitCommand.commandArgs(args);
966
+ const bounded = options.timeoutMs !== void 0;
964
967
  const subprocess = Bun.spawn(command, {
965
968
  cwd: options.cwd,
966
969
  stdin: "ignore",
967
970
  stdout: "pipe",
968
971
  stderr: "pipe",
969
- env: workerGitProcessEnvironment()
972
+ env: workerGitProcessEnvironment(),
973
+ ...bounded ? { detached: true } : {}
970
974
  });
971
- const [stdout, stderr, exitCode] = await Promise.all([
975
+ const completion = Promise.all([
972
976
  new Response(subprocess.stdout).text(),
973
977
  new Response(subprocess.stderr).text(),
974
978
  subprocess.exited
975
- ]);
976
- if (exitCode !== 0) {
977
- throw new Error(`git ${args.join(" ")} failed: ${stderr.trim() || stdout.trim() || `exit ${exitCode}`}`);
979
+ ]).then(([stdout, stderr, exitCode]) => ({ stdout, stderr, exitCode }));
980
+ let timer;
981
+ const timedOut = bounded ? new Promise((_resolve, reject) => {
982
+ timer = setTimeout(() => reject(new GitTransportTimeoutError(`git ${args.join(" ")} timed out after ${options.timeoutMs}ms`)), options.timeoutMs);
983
+ }) : null;
984
+ try {
985
+ const { stdout, stderr, exitCode } = timedOut ? await Promise.race([completion, timedOut]) : await completion;
986
+ if (exitCode !== 0) {
987
+ throw new Error(`git ${args.join(" ")} failed: ${stderr.trim() || stdout.trim() || `exit ${exitCode}`}`);
988
+ }
989
+ return stdout.trim();
990
+ } catch (error) {
991
+ if (error instanceof GitTransportTimeoutError) {
992
+ void terminateProcessTree(subprocess, { graceMs: 0 }).catch(() => void 0);
993
+ void completion.catch(() => void 0);
994
+ }
995
+ throw error;
996
+ } finally {
997
+ if (timer) clearTimeout(timer);
978
998
  }
979
- return stdout.trim();
980
999
  }
981
1000
  async function tryGitAsync(args, options = {}) {
982
1001
  try {
@@ -1837,14 +1856,13 @@ const workerGitSecurityTestHarness = {
1837
1856
  executeEditFileOperation,
1838
1857
  terminateCredentialBearingChildren,
1839
1858
  terminateCredentialBearingChildrenWithRetention,
1840
- async commitCredentialGeneration(prepared, credential, children, beforeMutation, previousGenerationFenced = false) {
1841
- return await commitCredentialGeneration(
1842
- prepared,
1843
- credential,
1844
- async () => await terminateCredentialBearingChildren(children),
1845
- beforeMutation,
1846
- previousGenerationFenced
1847
- );
1859
+ async commitCredentialGeneration(prepared, credential, options) {
1860
+ return await commitCredentialGeneration(prepared, credential, {
1861
+ secretMaterialChanged: options.secretMaterialChanged,
1862
+ terminatePreviousGeneration: async () => await terminateCredentialBearingChildren(options.children),
1863
+ beforeMutation: options.beforeMutation,
1864
+ previousGenerationFenced: options.previousGenerationFenced ?? false
1865
+ });
1848
1866
  },
1849
1867
  credentialGenerationReceiptPath,
1850
1868
  credentialAuthorityLockPath,
@@ -1882,14 +1900,14 @@ function installGitHubCredentialGeneration(credential) {
1882
1900
  `);
1883
1901
  }
1884
1902
  }
1885
- async function commitCredentialGeneration(prepared, credential, terminatePreviousGeneration, beforeMutation, previousGenerationFenced = false) {
1903
+ async function commitCredentialGeneration(prepared, credential, options) {
1886
1904
  const changed = prepared.changed;
1887
1905
  try {
1888
- if (changed && !previousGenerationFenced) {
1906
+ if (changed && options.secretMaterialChanged && !options.previousGenerationFenced) {
1889
1907
  githubCredential = null;
1890
- await terminatePreviousGeneration();
1908
+ await options.terminatePreviousGeneration();
1891
1909
  }
1892
- prepared.commit(beforeMutation);
1910
+ prepared.commit(options.beforeMutation);
1893
1911
  } catch (error) {
1894
1912
  prepared.discard();
1895
1913
  throw error instanceof RegistryAuthConfigurationError ? error : new RegistryAuthConfigurationError(error);
@@ -4338,33 +4356,56 @@ async function startWorker(options, projectRuntime = {
4338
4356
  return reconciledProjectIds;
4339
4357
  };
4340
4358
  const ORIGIN_DIVERGENCE_REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
4359
+ const ORIGIN_DIVERGENCE_FETCH_TIMEOUT_MS = 3e4;
4360
+ const ORIGIN_DIVERGENCE_REFRESH_BUDGET_MS = 9e4;
4341
4361
  const lastOriginDivergenceFetchMs = /* @__PURE__ */ new Map();
4342
- const collectAheadOfOriginBranches = async (refetchOriginProjectIds) => {
4362
+ const collectAheadOfOriginBranches = async (refetchOriginProjectIds, onProgress) => {
4343
4363
  const aheadBranches = [];
4344
4364
  for (const projectId of [...lastOriginDivergenceFetchMs.keys()]) {
4345
4365
  if (!projectConfigById.has(projectId)) lastOriginDivergenceFetchMs.delete(projectId);
4346
4366
  }
4347
- for (const project of projectConfigById.values()) {
4367
+ const passStartedAtMs = Date.now();
4368
+ let budgetSpentReported = false;
4369
+ const projects = [...projectConfigById.values()];
4370
+ for (const [projectIndex, project] of projects.entries()) {
4348
4371
  if (project.executionDisabled || !readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
4372
+ onProgress?.({
4373
+ detail: `Checking origin divergence for ${project.projectPath}`,
4374
+ completedItems: projectIndex,
4375
+ totalItems: projects.length
4376
+ });
4349
4377
  const connection = projectConnection(project);
4350
4378
  const primaryPath = configuredProjectBranchPath(projectsRoot, project, primaryProjectBranch(project));
4351
4379
  const lastFetchedAtMs = lastOriginDivergenceFetchMs.get(project.projectId);
4352
4380
  if (refetchOriginProjectIds.has(project.projectId) || lastFetchedAtMs === void 0 || Date.now() - lastFetchedAtMs >= ORIGIN_DIVERGENCE_REFRESH_INTERVAL_MS) {
4353
- if (await tryGitAsync(
4354
- [
4355
- ...gitTransportSecurityArgs(connection.originUrl, connection.credentialHelper, connection.originCredentialUsername),
4356
- "fetch",
4357
- "--no-recurse-submodules",
4358
- "--prune",
4359
- "origin",
4360
- "+refs/heads/*:refs/remotes/origin/*"
4361
- ],
4362
- { cwd: primaryPath }
4363
- )) {
4364
- lastOriginDivergenceFetchMs.set(project.projectId, Date.now());
4381
+ if (Date.now() - passStartedAtMs >= ORIGIN_DIVERGENCE_REFRESH_BUDGET_MS) {
4382
+ if (!budgetSpentReported) {
4383
+ budgetSpentReported = true;
4384
+ process.stderr.write(
4385
+ `[r5d-worker] origin divergence refresh budget of ${ORIGIN_DIVERGENCE_REFRESH_BUDGET_MS}ms is spent; remaining origins keep their previous comparison until the next refresh
4386
+ `
4387
+ );
4388
+ }
4365
4389
  } else {
4366
- process.stderr.write(`[r5d-worker] could not refresh origin divergence for ${project.projectPath}
4367
- `);
4390
+ try {
4391
+ await runGitAsync(
4392
+ [
4393
+ ...gitTransportSecurityArgs(connection.originUrl, connection.credentialHelper, connection.originCredentialUsername),
4394
+ "fetch",
4395
+ "--no-recurse-submodules",
4396
+ "--prune",
4397
+ "origin",
4398
+ "+refs/heads/*:refs/remotes/origin/*"
4399
+ ],
4400
+ { cwd: primaryPath, timeoutMs: ORIGIN_DIVERGENCE_FETCH_TIMEOUT_MS }
4401
+ );
4402
+ lastOriginDivergenceFetchMs.set(project.projectId, Date.now());
4403
+ } catch (error) {
4404
+ process.stderr.write(
4405
+ `[r5d-worker] could not refresh origin divergence for ${project.projectPath}: ${error instanceof Error ? error.message : String(error)}
4406
+ `
4407
+ );
4408
+ }
4368
4409
  }
4369
4410
  }
4370
4411
  for (const branch of project.branches) {
@@ -5397,13 +5438,11 @@ async function startWorker(options, projectRuntime = {
5397
5438
  } catch (error) {
5398
5439
  throw error instanceof RegistryAuthConfigurationError ? error : new RegistryAuthConfigurationError(error);
5399
5440
  }
5400
- await commitCredentialGeneration(
5401
- preparedAuthGeneration,
5402
- message.githubCredential,
5403
- terminateActiveCredentialBearingChildren,
5404
- void 0,
5405
- credentialPublicationPreauthorized
5406
- );
5441
+ await commitCredentialGeneration(preparedAuthGeneration, message.githubCredential, {
5442
+ secretMaterialChanged: credentialTransitionPhase !== "current",
5443
+ terminatePreviousGeneration: terminateActiveCredentialBearingChildren,
5444
+ previousGenerationFenced: credentialPublicationPreauthorized
5445
+ });
5407
5446
  if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
5408
5447
  throw new Error("Workspace configuration was superseded by a newer server generation");
5409
5448
  }
@@ -5538,7 +5577,15 @@ async function startWorker(options, projectRuntime = {
5538
5577
  }
5539
5578
  };
5540
5579
  const hasDurableBranchDeletion = projectWorkspaceState.tombstones.some((tombstone) => tombstone.kind === "branch");
5541
- const result = await performWorkspaceSync({
5580
+ sendLifecycleProgress({
5581
+ phase: "configuring",
5582
+ operationId: configurationOperationId,
5583
+ detail: `Synchronizing the workspace after configuring ${message.projects.length} project(s)`,
5584
+ completedItems: message.projects.length,
5585
+ totalItems: message.projects.length
5586
+ });
5587
+ const connectSyncStartedAtMs = Date.now();
5588
+ const syncResult = await performWorkspaceSync({
5542
5589
  attemptId: crypto.randomUUID(),
5543
5590
  trigger: { type: "connect" },
5544
5591
  confirmedLargeDiff: hasDurableBranchDeletion,
@@ -5546,6 +5593,11 @@ async function startWorker(options, projectRuntime = {
5546
5593
  resetToCanonical: workspaceConfigurationResetToCanonicalIsAllowed(message.resetToCanonical, incidentDeferral.incidentId),
5547
5594
  assertStillAdmitted: assertConfigurationSyncStillAdmitted
5548
5595
  });
5596
+ const connectSyncMs = Date.now() - connectSyncStartedAtMs;
5597
+ const result = syncResult.telemetry ? syncResult : {
5598
+ ...syncResult,
5599
+ telemetry: { totalMs: connectSyncMs, queueMs: 0, prepareMs: lastWorkspaceSyncMirrorObservationMs, synchronizeMs: connectSyncMs }
5600
+ };
5549
5601
  assertConfigurationSyncStillAdmitted();
5550
5602
  if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
5551
5603
  throw new Error("Workspace configuration was superseded by a newer server generation");
@@ -5578,7 +5630,14 @@ async function startWorker(options, projectRuntime = {
5578
5630
  const pending = [...pendingCheckouts.values()].sort(
5579
5631
  (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
5580
5632
  );
5581
- return { result, pending, aheadOfOriginBranches: await collectAheadOfOriginBranches(reconciledProjectIds) };
5633
+ return {
5634
+ result,
5635
+ pending,
5636
+ aheadOfOriginBranches: await collectAheadOfOriginBranches(
5637
+ reconciledProjectIds,
5638
+ (progress) => sendLifecycleProgress({ phase: "configuring", operationId: configurationOperationId, ...progress })
5639
+ )
5640
+ };
5582
5641
  });
5583
5642
  } catch (error) {
5584
5643
  if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
@@ -5932,7 +5991,15 @@ async function startWorker(options, projectRuntime = {
5932
5991
  }
5933
5992
  if (message.type === "workspace_config") {
5934
5993
  const receiptGeneration = ++workspaceConfigurationReceiptGeneration;
5935
- const refreshesDeferredConfiguration = deferredWorkspaceConfiguration !== null && activeWorkspaceIncidentId === null && message.deferWorkspaceSyncForIncidentId === void 0;
5994
+ const refreshesDeferredConfiguration = workspaceConfigurationRefreshesDeferredConfiguration({
5995
+ deferredIncidentId: deferredWorkspaceConfiguration?.incidentId ?? null,
5996
+ activeIncidentId: activeWorkspaceIncidentId,
5997
+ requestedDeferredIncidentId: message.deferWorkspaceSyncForIncidentId
5998
+ });
5999
+ if (refreshesDeferredConfiguration && deferredWorkspaceConfigurationRefreshTimer) {
6000
+ clearTimeout(deferredWorkspaceConfigurationRefreshTimer);
6001
+ deferredWorkspaceConfigurationRefreshTimer = void 0;
6002
+ }
5936
6003
  const digest = workerOperationDigest(message);
5937
6004
  const cached = projectRuntime.configuration;
5938
6005
  const canReuseConfiguration = cached?.digest === digest && cached.workspaceSync?.remoteUrl === message.workspaceRemoteUrl && Boolean(cached.workspaceSync.credentialHelper) && cached.workspaceSync.credentialUsername === workerCredentialUsername && cached.workspaceSync.gitIdentity?.name === message.gitIdentity.name && cached.workspaceSync.gitIdentity?.email === message.gitIdentity.email && !unsafeWorkspaceSyncBouncePending && credentialGenerationFingerprint({ ...message, workerBaseUrl: baseUrl, workerAuthHeader: bearerAuthHeader }) === configuredCredentialGenerationFingerprint;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.119",
3
+ "version": "0.0.121",
4
4
  "type": "module"
5
5
  }
@@ -2330,7 +2330,9 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
2330
2330
  }
2331
2331
  restoreDeferredWorkspaceMountsFromHead(
2332
2332
  workspacePath,
2333
- input.mounts.filter(({ id }) => deferredMountIds.has(id))
2333
+ input.mounts.filter(
2334
+ (mount) => deferredMountIds.has(mount.id) || mount.preserveLocalOnHydrationBasisChange === true && cycleSkippedMountIds.has(mount.id)
2335
+ )
2334
2336
  );
2335
2337
  await stageAndCommitWorkspace();
2336
2338
  await fsyncWorkspaceCheckoutTreeYielding(workspacePath);
@@ -3,6 +3,10 @@ function applyWorkspaceIncidentUpdate(currentIncidentId, update) {
3
3
  if (currentIncidentId && update.incidentId && update.incidentId !== currentIncidentId) return currentIncidentId;
4
4
  return null;
5
5
  }
6
+ function workspaceConfigurationRefreshesDeferredConfiguration(input) {
7
+ return input.deferredIncidentId !== null && input.activeIncidentId === null && input.requestedDeferredIncidentId === void 0;
8
+ }
6
9
  export {
7
- applyWorkspaceIncidentUpdate
10
+ applyWorkspaceIncidentUpdate,
11
+ workspaceConfigurationRefreshesDeferredConfiguration
8
12
  };
@@ -957,10 +957,15 @@ export declare const workerGitSecurityTestHarness: {
957
957
  executeEditFileOperation: typeof executeEditFileOperation;
958
958
  terminateCredentialBearingChildren: typeof terminateCredentialBearingChildren;
959
959
  terminateCredentialBearingChildrenWithRetention: typeof terminateCredentialBearingChildrenWithRetention;
960
- commitCredentialGeneration(prepared: PreparedPrivateAuthFileGeneration, credential: WorkerGitHubCredential | null, children: readonly {
961
- label: string;
962
- terminate: () => void | Promise<void>;
963
- }[], beforeMutation?: (filePath: string, index: number) => void, previousGenerationFenced?: boolean): Promise<boolean>;
960
+ commitCredentialGeneration(prepared: PreparedPrivateAuthFileGeneration, credential: WorkerGitHubCredential | null, options: {
961
+ children: readonly {
962
+ label: string;
963
+ terminate: () => void | Promise<void>;
964
+ }[];
965
+ secretMaterialChanged: boolean;
966
+ beforeMutation?: (filePath: string, index: number) => void;
967
+ previousGenerationFenced?: boolean;
968
+ }): Promise<boolean>;
964
969
  credentialGenerationReceiptPath: typeof credentialGenerationReceiptPath;
965
970
  credentialAuthorityLockPath: typeof credentialAuthorityLockPath;
966
971
  acquireCredentialAuthorityLock: typeof acquireCredentialAuthorityLock;
@@ -3,3 +3,17 @@ export type WorkspaceIncidentUpdate = {
3
3
  status: "remediating" | "waiting_for_worker" | "resolved" | "confirmed" | "reset" | null;
4
4
  };
5
5
  export declare function applyWorkspaceIncidentUpdate(currentIncidentId: string | null, update: WorkspaceIncidentUpdate): string | null;
6
+ /**
7
+ * Whether an incoming `workspace_config` is the full configuration a terminal
8
+ * incident update promised: the worker still holds an incident-deferred
9
+ * configuration, no incident is active any more, and the server did not ask
10
+ * for another deferral. Arrival of that configuration disarms the refresh
11
+ * watchdog; from then on the configuration's own generation checks and its
12
+ * failure close govern, so a legitimately long full configuration is never
13
+ * cut off mid-flight by the arrival timer.
14
+ */
15
+ export declare function workspaceConfigurationRefreshesDeferredConfiguration(input: {
16
+ deferredIncidentId: string | null;
17
+ activeIncidentId: string | null;
18
+ requestedDeferredIncidentId: string | undefined;
19
+ }): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.119",
3
+ "version": "0.0.121",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",