@ricsam/r5d-worker 0.0.93 → 0.0.95

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/mjs/main.mjs CHANGED
@@ -58,6 +58,7 @@ import { assertDisjointManagedRoots, managedProjectRoot, validateManagedBranchNa
58
58
  import {
59
59
  applyObservedProjectMirrorHeads,
60
60
  assertProjectMirrorHeadsPushed,
61
+ computeLocalProjectMirrorRefsToken,
61
62
  createOrRetryLinkedProjectBranch,
62
63
  deleteLinkedProjectBranch,
63
64
  deleteProjectMirrorBranch,
@@ -70,6 +71,7 @@ import {
70
71
  pushProjectMirrorHeads,
71
72
  removeProjectWorktrees
72
73
  } from "./project-worktrees.mjs";
74
+ import { projectMirrorFetchDecision } from "./project-mirror-fetch-policy.mjs";
73
75
  import {
74
76
  PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND,
75
77
  recoverProjectSnapshotsInChild,
@@ -119,6 +121,7 @@ const DEFAULT_READ_MAX_BYTES = 5e4;
119
121
  const MAX_LINE_LENGTH = 2e3;
120
122
  const WORKSPACE_GIT_QUIET_MS = 5e3;
121
123
  const WORKSPACE_GIT_PERIODIC_MS = 6e4;
124
+ const PROJECT_MIRROR_OBSERVE_CONCURRENCY = 6;
122
125
  const WORKSPACE_INCIDENT_CONFIG_REFRESH_TIMEOUT_MS = 6e4;
123
126
  const PTY_INPUT_BUSY_GRACE_MS = 3e3;
124
127
  const PTY_FOREGROUND_POLL_MS = 1e3;
@@ -1009,6 +1012,21 @@ function resolveWorkerFilePath(branchPath, inputPath, builtInPaths) {
1009
1012
  function canonicalProjectRemoteUrl(baseUrl, projectId) {
1010
1013
  return new URL(`/git/${projectId}.git`, baseUrl).toString();
1011
1014
  }
1015
+ async function mapWithConcurrencyLimit(items, limit, task) {
1016
+ const results = new Array(items.length);
1017
+ let nextIndex = 0;
1018
+ await Promise.all(
1019
+ Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
1020
+ while (true) {
1021
+ const index = nextIndex;
1022
+ nextIndex += 1;
1023
+ if (index >= items.length) return;
1024
+ results[index] = await task(items[index], index);
1025
+ }
1026
+ })
1027
+ );
1028
+ return results;
1029
+ }
1012
1030
  function shellQuote(value) {
1013
1031
  return `'${value.replace(/'/g, "'\\''")}'`;
1014
1032
  }
@@ -3468,6 +3486,10 @@ async function startWorker(options) {
3468
3486
  const readyProjectIds = /* @__PURE__ */ new Set();
3469
3487
  const reconciledProjectConfigFingerprints = /* @__PURE__ */ new Map();
3470
3488
  const lastObservedProjectHeads = /* @__PURE__ */ new Map();
3489
+ const advertisedProjectMirrorRefsTokens = /* @__PURE__ */ new Map();
3490
+ const fetchedProjectMirrorRefsTokens = /* @__PURE__ */ new Map();
3491
+ const lastProjectMirrorFetchAtMs = /* @__PURE__ */ new Map();
3492
+ const projectMirrorFetchSkippingEnabled = process.env.R5D_PROJECT_MIRROR_FETCH_SKIP !== "0";
3471
3493
  const pendingMirrorDeletes = /* @__PURE__ */ new Map();
3472
3494
  let workspaceRemoteUrl = null;
3473
3495
  let workspaceCredentialHelper = null;
@@ -3485,6 +3507,7 @@ async function startWorker(options) {
3485
3507
  let automaticSyncInFlight = false;
3486
3508
  let terminalReplayTimer;
3487
3509
  let workspaceSyncRequestsInFlight = 0;
3510
+ let lastWorkspaceSyncMirrorObservationMs = 0;
3488
3511
  let sessionArtifactSyncRequestsInFlight = 0;
3489
3512
  let cliUpdateInProgress = false;
3490
3513
  let reloadAfterClose = false;
@@ -4073,7 +4096,7 @@ async function startWorker(options) {
4073
4096
  }
4074
4097
  return heads;
4075
4098
  };
4076
- const pushChangedProjectHeads = async (activeMountIds, publishedHead, publicationHeads, suppressedMountIds = /* @__PURE__ */ new Set()) => {
4099
+ const pushChangedProjectHeads = async (activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, suppressedMountIds = /* @__PURE__ */ new Set()) => {
4077
4100
  const active = new Set(activeMountIds);
4078
4101
  for (const project of projectConfigById.values()) {
4079
4102
  if (!readyProjectIds.has(project.projectId) || !repositoryMirrorWritesAllowed(project)) continue;
@@ -4088,6 +4111,12 @@ async function startWorker(options) {
4088
4111
  }
4089
4112
  }
4090
4113
  if (changed.size === 0) continue;
4114
+ const branchLeases = /* @__PURE__ */ new Map();
4115
+ for (const branchName of changed.keys()) {
4116
+ const mountId = projectMountId(project.projectId, branchName);
4117
+ if (!expectedMirrorHeads.has(mountId)) throw new Error(`Missing observed hidden-mirror lease for active project mount ${mountId}`);
4118
+ branchLeases.set(branchName, expectedMirrorHeads.get(mountId) ?? null);
4119
+ }
4091
4120
  const connection = projectConnection(project);
4092
4121
  const results = await pushProjectMirrorHeads({
4093
4122
  projectRoot: configuredProjectRoot(projectsRoot, project),
@@ -4096,12 +4125,25 @@ async function startWorker(options) {
4096
4125
  credentialHelper: connection.credentialHelper,
4097
4126
  credentialUsername: connection.mirrorCredentialUsername,
4098
4127
  onlyBranches: new Set(changed.keys()),
4099
- publicationHeads: changed
4128
+ publicationHeads: changed,
4129
+ expectedMirrorHeads: branchLeases,
4130
+ branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId]))
4100
4131
  });
4101
4132
  assertProjectMirrorHeadsPushed(results);
4102
4133
  for (const result of results) {
4103
4134
  lastObservedProjectHeads.set(projectBranchKey(project.projectId, result.branchName), result.head);
4104
4135
  }
4136
+ try {
4137
+ fetchedProjectMirrorRefsTokens.set(
4138
+ project.projectId,
4139
+ await computeLocalProjectMirrorRefsToken({
4140
+ projectRoot: configuredProjectRoot(projectsRoot, project),
4141
+ primaryBranchName: primaryProjectBranch(project)
4142
+ })
4143
+ );
4144
+ } catch {
4145
+ fetchedProjectMirrorRefsTokens.delete(project.projectId);
4146
+ }
4105
4147
  }
4106
4148
  const publishedTombstoneIds = [
4107
4149
  ...new Set(
@@ -4127,6 +4169,7 @@ async function startWorker(options) {
4127
4169
  credentialHelper: deletion.credentialHelper,
4128
4170
  credentialUsername: deletion.credentialUsername
4129
4171
  });
4172
+ fetchedProjectMirrorRefsTokens.delete(deletion.projectId);
4130
4173
  }
4131
4174
  if (deletion.tombstoneId && !deletion.projectDeleted) {
4132
4175
  projectWorkspaceState = projectWorkspaceStateStore.recordMirrorRefDeletion({
@@ -4137,43 +4180,103 @@ async function startWorker(options) {
4137
4180
  pendingMirrorDeletes.delete(key);
4138
4181
  if (deletion.projectDeleted && !projectConfigById.has(deletion.projectId) && ![...pendingMirrorDeletes.values()].some((pending) => pending.projectId === deletion.projectId)) {
4139
4182
  removeProjectCredentialStore(deletion.projectId);
4183
+ advertisedProjectMirrorRefsTokens.delete(deletion.projectId);
4184
+ fetchedProjectMirrorRefsTokens.delete(deletion.projectId);
4185
+ lastProjectMirrorFetchAtMs.delete(deletion.projectId);
4140
4186
  }
4141
4187
  lastObservedProjectHeads.delete(key);
4142
4188
  }
4143
4189
  };
4190
+ const observeSingleProjectMirrorBundle = async (project, options2) => {
4191
+ const connection = projectConnection(project);
4192
+ const projectRoot = configuredProjectRoot(projectsRoot, project);
4193
+ const primaryBranchName = primaryProjectBranch(project);
4194
+ if (options2.refreshRemote) {
4195
+ const advertisedBeforeFetch = advertisedProjectMirrorRefsTokens.get(project.projectId);
4196
+ const observations = await observeProjectMirrorHeads({
4197
+ projectRoot,
4198
+ primaryBranchName,
4199
+ branchNames: project.branches.map(({ branchName }) => branchName),
4200
+ mirrorUrl: connection.mirrorUrl,
4201
+ credentialHelper: connection.credentialHelper,
4202
+ credentialUsername: connection.mirrorCredentialUsername,
4203
+ allowNonFastForward: options2.allowNonFastForward,
4204
+ branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId]))
4205
+ });
4206
+ if (advertisedBeforeFetch === void 0) fetchedProjectMirrorRefsTokens.delete(project.projectId);
4207
+ else fetchedProjectMirrorRefsTokens.set(project.projectId, advertisedBeforeFetch);
4208
+ lastProjectMirrorFetchAtMs.set(project.projectId, Date.now());
4209
+ return { projectId: project.projectId, projectRoot, primaryBranchName, observations };
4210
+ }
4211
+ return {
4212
+ projectId: project.projectId,
4213
+ projectRoot,
4214
+ primaryBranchName,
4215
+ observations: await observeProjectMirrorHeads({
4216
+ projectRoot,
4217
+ primaryBranchName,
4218
+ branchNames: project.branches.map(({ branchName }) => branchName),
4219
+ mirrorUrl: connection.mirrorUrl,
4220
+ credentialHelper: connection.credentialHelper,
4221
+ credentialUsername: connection.mirrorCredentialUsername,
4222
+ allowNonFastForward: options2.allowNonFastForward,
4223
+ branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId])),
4224
+ refreshRemote: false
4225
+ })
4226
+ };
4227
+ };
4144
4228
  const observeProjectHeadsBeforeOuterWorkspace = async (input) => {
4145
- const bundles = [];
4146
- for (const project of projectConfigById.values()) {
4147
- if (project.executionDisabled || !readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
4148
- try {
4149
- const connection = projectConnection(project);
4150
- const projectRoot = configuredProjectRoot(projectsRoot, project);
4151
- const primaryBranchName = primaryProjectBranch(project);
4152
- bundles.push({
4153
- projectId: project.projectId,
4154
- projectRoot,
4155
- primaryBranchName,
4156
- observations: await observeProjectMirrorHeads({
4157
- projectRoot,
4158
- primaryBranchName,
4159
- branchNames: project.branches.map(({ branchName }) => branchName),
4160
- mirrorUrl: connection.mirrorUrl,
4161
- credentialHelper: connection.credentialHelper,
4162
- credentialUsername: connection.mirrorCredentialUsername,
4163
- allowNonFastForward: input.allowNonFastForward
4164
- })
4165
- });
4166
- } catch (error) {
4167
- if (input.failClosed) throw error;
4229
+ const observationStartedAt = Date.now();
4230
+ const eligibleProjects = [...projectConfigById.values()].filter(
4231
+ (project) => !project.executionDisabled && readyProjectIds.has(project.projectId) && project.branches.length > 0
4232
+ );
4233
+ try {
4234
+ if (input.failClosed) {
4235
+ const bundles2 = [];
4236
+ for (const project of eligibleProjects) {
4237
+ bundles2.push(await observeSingleProjectMirrorBundle(project, { allowNonFastForward: input.allowNonFastForward, refreshRemote: true }));
4238
+ }
4239
+ return bundles2;
4240
+ }
4241
+ let fetchedCount = 0;
4242
+ const bundles = await mapWithConcurrencyLimit(
4243
+ eligibleProjects,
4244
+ PROJECT_MIRROR_OBSERVE_CONCURRENCY,
4245
+ async (project) => {
4246
+ try {
4247
+ const decision = projectMirrorFetchDecision({
4248
+ advertisedToken: advertisedProjectMirrorRefsTokens.get(project.projectId),
4249
+ fetchedToken: fetchedProjectMirrorRefsTokens.get(project.projectId),
4250
+ lastFetchAtMs: lastProjectMirrorFetchAtMs.get(project.projectId),
4251
+ nowMs: Date.now(),
4252
+ skippingEnabled: projectMirrorFetchSkippingEnabled
4253
+ });
4254
+ if (decision.fetch) fetchedCount += 1;
4255
+ return await observeSingleProjectMirrorBundle(project, {
4256
+ allowNonFastForward: input.allowNonFastForward,
4257
+ refreshRemote: decision.fetch
4258
+ });
4259
+ } catch (error) {
4260
+ process.stderr.write(
4261
+ `[r5d-worker] failed to observe project mirror heads before workspace update: ${error instanceof Error ? error.message : String(error)}
4262
+ `
4263
+ );
4264
+ return null;
4265
+ }
4266
+ }
4267
+ );
4268
+ if (fetchedCount > 0) {
4168
4269
  process.stderr.write(
4169
- `[r5d-worker] failed to observe project mirror heads before workspace update: ${error instanceof Error ? error.message : String(error)}
4270
+ `[r5d-worker] project mirror observation fetched ${fetchedCount} of ${eligibleProjects.length} mirror(s)
4170
4271
  `
4171
4272
  );
4172
4273
  }
4274
+ return bundles.filter((bundle) => bundle !== null);
4275
+ } finally {
4276
+ lastWorkspaceSyncMirrorObservationMs = Date.now() - observationStartedAt;
4173
4277
  }
4174
- return bundles;
4175
4278
  };
4176
- const applyObservedProjectHeadsAfterInboundWorkspace = async (bundles, activeMountIds, requireObservedHeads) => {
4279
+ const applyObservedProjectHeadsAfterInboundWorkspace = async (bundles, activeMountIds, requireObservedHeads, integratedWorkspaceHead) => {
4177
4280
  const active = new Set(activeMountIds);
4178
4281
  for (const bundle of bundles) {
4179
4282
  const project = projectConfigById.get(bundle.projectId);
@@ -4185,7 +4288,16 @@ async function startWorker(options) {
4185
4288
  projectRoot: bundle.projectRoot,
4186
4289
  primaryBranchName: bundle.primaryBranchName,
4187
4290
  observations,
4188
- branchMutationAllowed: (branchName, checkoutPath) => !projectBranchMountBusy(project.projectId, branchName, checkoutPath)
4291
+ branchMutationAllowed: (branchName, checkoutPath) => !projectBranchMountBusy(project.projectId, branchName, checkoutPath),
4292
+ serverSyncedWorkspaceCovered: async (observation) => {
4293
+ if (!integratedWorkspaceHead) return false;
4294
+ for (const workspaceHead of observation.serverSyncedWorkspaceHeads) {
4295
+ if (await tryGitAsync(["merge-base", "--is-ancestor", workspaceHead, integratedWorkspaceHead], { cwd: workspaceShadowRoot })) {
4296
+ return true;
4297
+ }
4298
+ }
4299
+ return false;
4300
+ }
4189
4301
  });
4190
4302
  if (requireObservedHeads) {
4191
4303
  for (const observation of observations) {
@@ -4310,6 +4422,12 @@ async function startWorker(options) {
4310
4422
  const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
4311
4423
  const mounts = outerRemediation ? workspaceGitMountsForRemediation(configuredMounts) : configuredMounts;
4312
4424
  const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
4425
+ const expectedMirrorHeads = /* @__PURE__ */ new Map();
4426
+ for (const bundle of observedProjectHeads) {
4427
+ for (const observation of bundle.observations) {
4428
+ expectedMirrorHeads.set(projectMountId(bundle.projectId, observation.branchName), observation.mirrorHead);
4429
+ }
4430
+ }
4313
4431
  const inboundMoveMountIds = new Set(
4314
4432
  observedProjectHeads.flatMap(
4315
4433
  ({ projectId, observations }) => observations.filter(({ shouldMove }) => shouldMove).map(({ branchName }) => projectMountId(projectId, branchName))
@@ -4331,12 +4449,17 @@ async function startWorker(options) {
4331
4449
  requiredAncestorHeads: input.requiredAncestorHeads,
4332
4450
  assertStillAdmitted: input.assertStillAdmitted,
4333
4451
  afterWorkspacePublished: outerRemediation ? void 0 : async ({ activeMountIds, publishedHead }) => {
4334
- await pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, inboundMoveMountIds);
4452
+ await pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, inboundMoveMountIds);
4335
4453
  }
4336
4454
  });
4337
4455
  input.assertStillAdmitted?.();
4338
4456
  if (["no_change", "updated", "pushed"].includes(result.outcome) && !outerRemediation) {
4339
- await applyObservedProjectHeadsAfterInboundWorkspace(observedProjectHeads, result.activeMountIds, false);
4457
+ await applyObservedProjectHeadsAfterInboundWorkspace(
4458
+ observedProjectHeads,
4459
+ result.activeMountIds,
4460
+ false,
4461
+ result.localHead ?? result.remoteHead
4462
+ );
4340
4463
  }
4341
4464
  return mapWorkspaceGitResult(input.attemptId, input.trigger, result);
4342
4465
  };
@@ -4420,6 +4543,7 @@ async function startWorker(options) {
4420
4543
  result = await workspaceSyncSingleFlight.runExclusive(async () => {
4421
4544
  queueEnteredAt = Date.now();
4422
4545
  syncStartedAt = Date.now();
4546
+ lastWorkspaceSyncMirrorObservationMs = 0;
4423
4547
  if (!deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
4424
4548
  return await failedWorkspaceSyncResult(
4425
4549
  attemptId,
@@ -4504,7 +4628,9 @@ async function startWorker(options) {
4504
4628
  telemetry: {
4505
4629
  totalMs: finishedAt - requestedAt,
4506
4630
  queueMs: queueEnteredAt - requestedAt,
4507
- prepareMs: 0,
4631
+ // Duration of the pre-outer mirror-head observation phase; near zero
4632
+ // on a cycle whose token-gated observation skipped every fetch.
4633
+ prepareMs: lastWorkspaceSyncMirrorObservationMs,
4508
4634
  synchronizeMs: finishedAt - syncStartedAt
4509
4635
  }
4510
4636
  };
@@ -5221,7 +5347,9 @@ async function startWorker(options) {
5221
5347
  workspaceRemediationAncestorGuardV1: true,
5222
5348
  workspaceIncidentConfigDeferralV1: true,
5223
5349
  workspaceConfigResetToCanonicalV1: true,
5224
- projectBranchDeletionFastAckV1: true
5350
+ projectBranchDeletionFastAckV1: true,
5351
+ projectMirrorLeaseV1: true,
5352
+ projectMirrorRefsTokensV1: true
5225
5353
  },
5226
5354
  projectRoot: projectsRoot,
5227
5355
  artifactRoot,
@@ -5259,6 +5387,17 @@ async function startWorker(options) {
5259
5387
  if (message.type === "connected") {
5260
5388
  return;
5261
5389
  }
5390
+ if (message.type === "project_mirror_refs_tokens") {
5391
+ advertisedProjectMirrorRefsTokens.clear();
5392
+ if (message.tokens && typeof message.tokens === "object") {
5393
+ for (const [projectId, token2] of Object.entries(message.tokens)) {
5394
+ if (typeof token2 === "string" && /^[0-9a-f]{64}$/.test(token2)) {
5395
+ advertisedProjectMirrorRefsTokens.set(projectId, token2);
5396
+ }
5397
+ }
5398
+ }
5399
+ return;
5400
+ }
5262
5401
  if (message.type === "workspace_config") {
5263
5402
  const receiptGeneration = ++workspaceConfigurationReceiptGeneration;
5264
5403
  const refreshesDeferredConfiguration = deferredWorkspaceConfiguration !== null && activeWorkspaceIncidentId === null && message.deferWorkspaceSyncForIncidentId === void 0;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.93",
3
+ "version": "0.0.95",
4
4
  "type": "module"
5
5
  }
@@ -0,0 +1,16 @@
1
+ const PROJECT_MIRROR_FETCH_BACKSTOP_MS = 10 * 6e4;
2
+ function projectMirrorFetchDecision(input) {
3
+ if (!input.skippingEnabled) return { fetch: true, reason: "skipping_disabled" };
4
+ if (input.advertisedToken === void 0) return { fetch: true, reason: "no_advertised_token" };
5
+ if (input.fetchedToken === void 0) return { fetch: true, reason: "never_fetched" };
6
+ if (input.advertisedToken !== input.fetchedToken) return { fetch: true, reason: "token_changed" };
7
+ const backstopMs = input.backstopMs ?? PROJECT_MIRROR_FETCH_BACKSTOP_MS;
8
+ if (input.lastFetchAtMs === void 0 || input.nowMs - input.lastFetchAtMs >= backstopMs) {
9
+ return { fetch: true, reason: "backstop_elapsed" };
10
+ }
11
+ return { fetch: false, reason: "token_match" };
12
+ }
13
+ export {
14
+ PROJECT_MIRROR_FETCH_BACKSTOP_MS,
15
+ projectMirrorFetchDecision
16
+ };
@@ -0,0 +1,24 @@
1
+ import { createHash } from "node:crypto";
2
+ const PROJECT_MIRROR_REF_PREFIX = "refs/r5d/mirror/";
3
+ const PROJECT_SERVER_SYNCED_REF_PREFIX = "refs/r5d/server-synced/";
4
+ function hashProjectMirrorRefs(refs) {
5
+ const lines = refs.map(({ objectName, refname }) => `${objectName} ${refname}`).sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
6
+ return createHash("sha256").update(lines.join("\n")).digest("hex");
7
+ }
8
+ function canonicalizeLocalProjectMirrorRefs(refs) {
9
+ const canonical = [];
10
+ for (const { refname, objectName } of refs) {
11
+ if (refname.startsWith(PROJECT_MIRROR_REF_PREFIX)) {
12
+ canonical.push({ refname: `refs/heads/${refname.slice(PROJECT_MIRROR_REF_PREFIX.length)}`, objectName });
13
+ } else if (refname.startsWith(PROJECT_SERVER_SYNCED_REF_PREFIX)) {
14
+ canonical.push({ refname, objectName });
15
+ }
16
+ }
17
+ return canonical;
18
+ }
19
+ export {
20
+ PROJECT_MIRROR_REF_PREFIX,
21
+ PROJECT_SERVER_SYNCED_REF_PREFIX,
22
+ canonicalizeLocalProjectMirrorRefs,
23
+ hashProjectMirrorRefs
24
+ };