@ricsam/r5d-worker 0.0.94 → 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/cjs/main.cjs CHANGED
@@ -81,6 +81,7 @@ var import_repository_transition_policy = require("./repository-transition-polic
81
81
  var import_supervisor = require("./supervisor.cjs");
82
82
  var import_managed_paths = require("./managed-paths.cjs");
83
83
  var import_project_worktrees = require("./project-worktrees.cjs");
84
+ var import_project_mirror_fetch_policy = require("./project-mirror-fetch-policy.cjs");
84
85
  var import_project_snapshot_recovery_runner = require("./project-snapshot-recovery-runner.cjs");
85
86
  var import_project_workspace_state = require("./project-workspace-state.cjs");
86
87
  var import_workspace_git_sync = require("./workspace-git-sync.cjs");
@@ -113,6 +114,7 @@ const DEFAULT_READ_MAX_BYTES = 5e4;
113
114
  const MAX_LINE_LENGTH = 2e3;
114
115
  const WORKSPACE_GIT_QUIET_MS = 5e3;
115
116
  const WORKSPACE_GIT_PERIODIC_MS = 6e4;
117
+ const PROJECT_MIRROR_OBSERVE_CONCURRENCY = 6;
116
118
  const WORKSPACE_INCIDENT_CONFIG_REFRESH_TIMEOUT_MS = 6e4;
117
119
  const PTY_INPUT_BUSY_GRACE_MS = 3e3;
118
120
  const PTY_FOREGROUND_POLL_MS = 1e3;
@@ -1003,6 +1005,21 @@ function resolveWorkerFilePath(branchPath, inputPath, builtInPaths) {
1003
1005
  function canonicalProjectRemoteUrl(baseUrl, projectId) {
1004
1006
  return new URL(`/git/${projectId}.git`, baseUrl).toString();
1005
1007
  }
1008
+ async function mapWithConcurrencyLimit(items, limit, task) {
1009
+ const results = new Array(items.length);
1010
+ let nextIndex = 0;
1011
+ await Promise.all(
1012
+ Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
1013
+ while (true) {
1014
+ const index = nextIndex;
1015
+ nextIndex += 1;
1016
+ if (index >= items.length) return;
1017
+ results[index] = await task(items[index], index);
1018
+ }
1019
+ })
1020
+ );
1021
+ return results;
1022
+ }
1006
1023
  function shellQuote(value) {
1007
1024
  return `'${value.replace(/'/g, "'\\''")}'`;
1008
1025
  }
@@ -3462,6 +3479,10 @@ async function startWorker(options) {
3462
3479
  const readyProjectIds = /* @__PURE__ */ new Set();
3463
3480
  const reconciledProjectConfigFingerprints = /* @__PURE__ */ new Map();
3464
3481
  const lastObservedProjectHeads = /* @__PURE__ */ new Map();
3482
+ const advertisedProjectMirrorRefsTokens = /* @__PURE__ */ new Map();
3483
+ const fetchedProjectMirrorRefsTokens = /* @__PURE__ */ new Map();
3484
+ const lastProjectMirrorFetchAtMs = /* @__PURE__ */ new Map();
3485
+ const projectMirrorFetchSkippingEnabled = process.env.R5D_PROJECT_MIRROR_FETCH_SKIP !== "0";
3465
3486
  const pendingMirrorDeletes = /* @__PURE__ */ new Map();
3466
3487
  let workspaceRemoteUrl = null;
3467
3488
  let workspaceCredentialHelper = null;
@@ -3479,6 +3500,7 @@ async function startWorker(options) {
3479
3500
  let automaticSyncInFlight = false;
3480
3501
  let terminalReplayTimer;
3481
3502
  let workspaceSyncRequestsInFlight = 0;
3503
+ let lastWorkspaceSyncMirrorObservationMs = 0;
3482
3504
  let sessionArtifactSyncRequestsInFlight = 0;
3483
3505
  let cliUpdateInProgress = false;
3484
3506
  let reloadAfterClose = false;
@@ -4104,6 +4126,17 @@ async function startWorker(options) {
4104
4126
  for (const result of results) {
4105
4127
  lastObservedProjectHeads.set(projectBranchKey(project.projectId, result.branchName), result.head);
4106
4128
  }
4129
+ try {
4130
+ fetchedProjectMirrorRefsTokens.set(
4131
+ project.projectId,
4132
+ await (0, import_project_worktrees.computeLocalProjectMirrorRefsToken)({
4133
+ projectRoot: configuredProjectRoot(projectsRoot, project),
4134
+ primaryBranchName: primaryProjectBranch(project)
4135
+ })
4136
+ );
4137
+ } catch {
4138
+ fetchedProjectMirrorRefsTokens.delete(project.projectId);
4139
+ }
4107
4140
  }
4108
4141
  const publishedTombstoneIds = [
4109
4142
  ...new Set(
@@ -4129,6 +4162,7 @@ async function startWorker(options) {
4129
4162
  credentialHelper: deletion.credentialHelper,
4130
4163
  credentialUsername: deletion.credentialUsername
4131
4164
  });
4165
+ fetchedProjectMirrorRefsTokens.delete(deletion.projectId);
4132
4166
  }
4133
4167
  if (deletion.tombstoneId && !deletion.projectDeleted) {
4134
4168
  projectWorkspaceState = projectWorkspaceStateStore.recordMirrorRefDeletion({
@@ -4139,42 +4173,101 @@ async function startWorker(options) {
4139
4173
  pendingMirrorDeletes.delete(key);
4140
4174
  if (deletion.projectDeleted && !projectConfigById.has(deletion.projectId) && ![...pendingMirrorDeletes.values()].some((pending) => pending.projectId === deletion.projectId)) {
4141
4175
  removeProjectCredentialStore(deletion.projectId);
4176
+ advertisedProjectMirrorRefsTokens.delete(deletion.projectId);
4177
+ fetchedProjectMirrorRefsTokens.delete(deletion.projectId);
4178
+ lastProjectMirrorFetchAtMs.delete(deletion.projectId);
4142
4179
  }
4143
4180
  lastObservedProjectHeads.delete(key);
4144
4181
  }
4145
4182
  };
4183
+ const observeSingleProjectMirrorBundle = async (project, options2) => {
4184
+ const connection = projectConnection(project);
4185
+ const projectRoot = configuredProjectRoot(projectsRoot, project);
4186
+ const primaryBranchName = primaryProjectBranch(project);
4187
+ if (options2.refreshRemote) {
4188
+ const advertisedBeforeFetch = advertisedProjectMirrorRefsTokens.get(project.projectId);
4189
+ const observations = await (0, import_project_worktrees.observeProjectMirrorHeads)({
4190
+ projectRoot,
4191
+ primaryBranchName,
4192
+ branchNames: project.branches.map(({ branchName }) => branchName),
4193
+ mirrorUrl: connection.mirrorUrl,
4194
+ credentialHelper: connection.credentialHelper,
4195
+ credentialUsername: connection.mirrorCredentialUsername,
4196
+ allowNonFastForward: options2.allowNonFastForward,
4197
+ branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId]))
4198
+ });
4199
+ if (advertisedBeforeFetch === void 0) fetchedProjectMirrorRefsTokens.delete(project.projectId);
4200
+ else fetchedProjectMirrorRefsTokens.set(project.projectId, advertisedBeforeFetch);
4201
+ lastProjectMirrorFetchAtMs.set(project.projectId, Date.now());
4202
+ return { projectId: project.projectId, projectRoot, primaryBranchName, observations };
4203
+ }
4204
+ return {
4205
+ projectId: project.projectId,
4206
+ projectRoot,
4207
+ primaryBranchName,
4208
+ observations: await (0, import_project_worktrees.observeProjectMirrorHeads)({
4209
+ projectRoot,
4210
+ primaryBranchName,
4211
+ branchNames: project.branches.map(({ branchName }) => branchName),
4212
+ mirrorUrl: connection.mirrorUrl,
4213
+ credentialHelper: connection.credentialHelper,
4214
+ credentialUsername: connection.mirrorCredentialUsername,
4215
+ allowNonFastForward: options2.allowNonFastForward,
4216
+ branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId])),
4217
+ refreshRemote: false
4218
+ })
4219
+ };
4220
+ };
4146
4221
  const observeProjectHeadsBeforeOuterWorkspace = async (input) => {
4147
- const bundles = [];
4148
- for (const project of projectConfigById.values()) {
4149
- if (project.executionDisabled || !readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
4150
- try {
4151
- const connection = projectConnection(project);
4152
- const projectRoot = configuredProjectRoot(projectsRoot, project);
4153
- const primaryBranchName = primaryProjectBranch(project);
4154
- bundles.push({
4155
- projectId: project.projectId,
4156
- projectRoot,
4157
- primaryBranchName,
4158
- observations: await (0, import_project_worktrees.observeProjectMirrorHeads)({
4159
- projectRoot,
4160
- primaryBranchName,
4161
- branchNames: project.branches.map(({ branchName }) => branchName),
4162
- mirrorUrl: connection.mirrorUrl,
4163
- credentialHelper: connection.credentialHelper,
4164
- credentialUsername: connection.mirrorCredentialUsername,
4165
- allowNonFastForward: input.allowNonFastForward,
4166
- branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId]))
4167
- })
4168
- });
4169
- } catch (error) {
4170
- if (input.failClosed) throw error;
4222
+ const observationStartedAt = Date.now();
4223
+ const eligibleProjects = [...projectConfigById.values()].filter(
4224
+ (project) => !project.executionDisabled && readyProjectIds.has(project.projectId) && project.branches.length > 0
4225
+ );
4226
+ try {
4227
+ if (input.failClosed) {
4228
+ const bundles2 = [];
4229
+ for (const project of eligibleProjects) {
4230
+ bundles2.push(await observeSingleProjectMirrorBundle(project, { allowNonFastForward: input.allowNonFastForward, refreshRemote: true }));
4231
+ }
4232
+ return bundles2;
4233
+ }
4234
+ let fetchedCount = 0;
4235
+ const bundles = await mapWithConcurrencyLimit(
4236
+ eligibleProjects,
4237
+ PROJECT_MIRROR_OBSERVE_CONCURRENCY,
4238
+ async (project) => {
4239
+ try {
4240
+ const decision = (0, import_project_mirror_fetch_policy.projectMirrorFetchDecision)({
4241
+ advertisedToken: advertisedProjectMirrorRefsTokens.get(project.projectId),
4242
+ fetchedToken: fetchedProjectMirrorRefsTokens.get(project.projectId),
4243
+ lastFetchAtMs: lastProjectMirrorFetchAtMs.get(project.projectId),
4244
+ nowMs: Date.now(),
4245
+ skippingEnabled: projectMirrorFetchSkippingEnabled
4246
+ });
4247
+ if (decision.fetch) fetchedCount += 1;
4248
+ return await observeSingleProjectMirrorBundle(project, {
4249
+ allowNonFastForward: input.allowNonFastForward,
4250
+ refreshRemote: decision.fetch
4251
+ });
4252
+ } catch (error) {
4253
+ process.stderr.write(
4254
+ `[r5d-worker] failed to observe project mirror heads before workspace update: ${error instanceof Error ? error.message : String(error)}
4255
+ `
4256
+ );
4257
+ return null;
4258
+ }
4259
+ }
4260
+ );
4261
+ if (fetchedCount > 0) {
4171
4262
  process.stderr.write(
4172
- `[r5d-worker] failed to observe project mirror heads before workspace update: ${error instanceof Error ? error.message : String(error)}
4263
+ `[r5d-worker] project mirror observation fetched ${fetchedCount} of ${eligibleProjects.length} mirror(s)
4173
4264
  `
4174
4265
  );
4175
4266
  }
4267
+ return bundles.filter((bundle) => bundle !== null);
4268
+ } finally {
4269
+ lastWorkspaceSyncMirrorObservationMs = Date.now() - observationStartedAt;
4176
4270
  }
4177
- return bundles;
4178
4271
  };
4179
4272
  const applyObservedProjectHeadsAfterInboundWorkspace = async (bundles, activeMountIds, requireObservedHeads, integratedWorkspaceHead) => {
4180
4273
  const active = new Set(activeMountIds);
@@ -4443,6 +4536,7 @@ async function startWorker(options) {
4443
4536
  result = await workspaceSyncSingleFlight.runExclusive(async () => {
4444
4537
  queueEnteredAt = Date.now();
4445
4538
  syncStartedAt = Date.now();
4539
+ lastWorkspaceSyncMirrorObservationMs = 0;
4446
4540
  if (!deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
4447
4541
  return await failedWorkspaceSyncResult(
4448
4542
  attemptId,
@@ -4527,7 +4621,9 @@ async function startWorker(options) {
4527
4621
  telemetry: {
4528
4622
  totalMs: finishedAt - requestedAt,
4529
4623
  queueMs: queueEnteredAt - requestedAt,
4530
- prepareMs: 0,
4624
+ // Duration of the pre-outer mirror-head observation phase; near zero
4625
+ // on a cycle whose token-gated observation skipped every fetch.
4626
+ prepareMs: lastWorkspaceSyncMirrorObservationMs,
4531
4627
  synchronizeMs: finishedAt - syncStartedAt
4532
4628
  }
4533
4629
  };
@@ -5245,7 +5341,8 @@ async function startWorker(options) {
5245
5341
  workspaceIncidentConfigDeferralV1: true,
5246
5342
  workspaceConfigResetToCanonicalV1: true,
5247
5343
  projectBranchDeletionFastAckV1: true,
5248
- projectMirrorLeaseV1: true
5344
+ projectMirrorLeaseV1: true,
5345
+ projectMirrorRefsTokensV1: true
5249
5346
  },
5250
5347
  projectRoot: projectsRoot,
5251
5348
  artifactRoot,
@@ -5283,6 +5380,17 @@ async function startWorker(options) {
5283
5380
  if (message.type === "connected") {
5284
5381
  return;
5285
5382
  }
5383
+ if (message.type === "project_mirror_refs_tokens") {
5384
+ advertisedProjectMirrorRefsTokens.clear();
5385
+ if (message.tokens && typeof message.tokens === "object") {
5386
+ for (const [projectId, token2] of Object.entries(message.tokens)) {
5387
+ if (typeof token2 === "string" && /^[0-9a-f]{64}$/.test(token2)) {
5388
+ advertisedProjectMirrorRefsTokens.set(projectId, token2);
5389
+ }
5390
+ }
5391
+ }
5392
+ return;
5393
+ }
5286
5394
  if (message.type === "workspace_config") {
5287
5395
  const receiptGeneration = ++workspaceConfigurationReceiptGeneration;
5288
5396
  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.94",
3
+ "version": "0.0.95",
4
4
  "type": "commonjs"
5
5
  }
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var project_mirror_fetch_policy_exports = {};
20
+ __export(project_mirror_fetch_policy_exports, {
21
+ PROJECT_MIRROR_FETCH_BACKSTOP_MS: () => PROJECT_MIRROR_FETCH_BACKSTOP_MS,
22
+ projectMirrorFetchDecision: () => projectMirrorFetchDecision
23
+ });
24
+ module.exports = __toCommonJS(project_mirror_fetch_policy_exports);
25
+ const PROJECT_MIRROR_FETCH_BACKSTOP_MS = 10 * 6e4;
26
+ function projectMirrorFetchDecision(input) {
27
+ if (!input.skippingEnabled) return { fetch: true, reason: "skipping_disabled" };
28
+ if (input.advertisedToken === void 0) return { fetch: true, reason: "no_advertised_token" };
29
+ if (input.fetchedToken === void 0) return { fetch: true, reason: "never_fetched" };
30
+ if (input.advertisedToken !== input.fetchedToken) return { fetch: true, reason: "token_changed" };
31
+ const backstopMs = input.backstopMs ?? PROJECT_MIRROR_FETCH_BACKSTOP_MS;
32
+ if (input.lastFetchAtMs === void 0 || input.nowMs - input.lastFetchAtMs >= backstopMs) {
33
+ return { fetch: true, reason: "backstop_elapsed" };
34
+ }
35
+ return { fetch: false, reason: "token_match" };
36
+ }
37
+ // Annotate the CommonJS export names for ESM import in node:
38
+ 0 && (module.exports = {
39
+ PROJECT_MIRROR_FETCH_BACKSTOP_MS,
40
+ projectMirrorFetchDecision
41
+ });
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var project_mirror_refs_token_exports = {};
20
+ __export(project_mirror_refs_token_exports, {
21
+ PROJECT_MIRROR_REF_PREFIX: () => PROJECT_MIRROR_REF_PREFIX,
22
+ PROJECT_SERVER_SYNCED_REF_PREFIX: () => PROJECT_SERVER_SYNCED_REF_PREFIX,
23
+ canonicalizeLocalProjectMirrorRefs: () => canonicalizeLocalProjectMirrorRefs,
24
+ hashProjectMirrorRefs: () => hashProjectMirrorRefs
25
+ });
26
+ module.exports = __toCommonJS(project_mirror_refs_token_exports);
27
+ var import_node_crypto = require("node:crypto");
28
+ const PROJECT_MIRROR_REF_PREFIX = "refs/r5d/mirror/";
29
+ const PROJECT_SERVER_SYNCED_REF_PREFIX = "refs/r5d/server-synced/";
30
+ function hashProjectMirrorRefs(refs) {
31
+ const lines = refs.map(({ objectName, refname }) => `${objectName} ${refname}`).sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
32
+ return (0, import_node_crypto.createHash)("sha256").update(lines.join("\n")).digest("hex");
33
+ }
34
+ function canonicalizeLocalProjectMirrorRefs(refs) {
35
+ const canonical = [];
36
+ for (const { refname, objectName } of refs) {
37
+ if (refname.startsWith(PROJECT_MIRROR_REF_PREFIX)) {
38
+ canonical.push({ refname: `refs/heads/${refname.slice(PROJECT_MIRROR_REF_PREFIX.length)}`, objectName });
39
+ } else if (refname.startsWith(PROJECT_SERVER_SYNCED_REF_PREFIX)) {
40
+ canonical.push({ refname, objectName });
41
+ }
42
+ }
43
+ return canonical;
44
+ }
45
+ // Annotate the CommonJS export names for ESM import in node:
46
+ 0 && (module.exports = {
47
+ PROJECT_MIRROR_REF_PREFIX,
48
+ PROJECT_SERVER_SYNCED_REF_PREFIX,
49
+ canonicalizeLocalProjectMirrorRefs,
50
+ hashProjectMirrorRefs
51
+ });
@@ -32,6 +32,7 @@ __export(project_worktrees_exports, {
32
32
  ProjectBranchCreationRollbackIncompleteError: () => ProjectBranchCreationRollbackIncompleteError,
33
33
  applyObservedProjectMirrorHeads: () => applyObservedProjectMirrorHeads,
34
34
  assertProjectMirrorHeadsPushed: () => assertProjectMirrorHeadsPushed,
35
+ computeLocalProjectMirrorRefsToken: () => computeLocalProjectMirrorRefsToken,
35
36
  createLinkedProjectBranch: () => createLinkedProjectBranch,
36
37
  createOrRetryLinkedProjectBranch: () => createOrRetryLinkedProjectBranch,
37
38
  deleteLinkedProjectBranch: () => deleteLinkedProjectBranch,
@@ -57,6 +58,7 @@ var import_node_os = __toESM(require("node:os"), 1);
57
58
  var import_node_path = __toESM(require("node:path"), 1);
58
59
  var import_git_process_environment = require("./git-process-environment.cjs");
59
60
  var import_managed_paths = require("./managed-paths.cjs");
61
+ var import_project_mirror_refs_token = require("./project-mirror-refs-token.cjs");
60
62
  var import_working_tree_mirror = require("./working-tree-mirror.cjs");
61
63
  const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
62
64
  const PROJECT_WORKTREE_SNAPSHOT_MANIFEST = "transaction.json";
@@ -1294,6 +1296,7 @@ ${deleted.stdout}`)) {
1294
1296
  `Delete hidden project mirror for ${input.branchName}: ${deleted.stderr || deleted.stdout || `git exited ${deleted.exitCode}`}`
1295
1297
  );
1296
1298
  }
1299
+ await gitResultAsync(input.gitDirectory, ["update-ref", "-d", `refs/r5d/mirror/${input.branchName}`]);
1297
1300
  }
1298
1301
  async function pushProjectMirrorHeads(input) {
1299
1302
  const results = [];
@@ -1351,6 +1354,7 @@ async function pushProjectMirrorHeads(input) {
1351
1354
  if (pushed.exitCode !== 0) {
1352
1355
  throw new Error(`Push hidden project mirror for ${branchName}: ${pushed.stderr || pushed.stdout || `git exited ${pushed.exitCode}`}`);
1353
1356
  }
1357
+ await gitAsync(checkoutPath, ["update-ref", `refs/r5d/mirror/${branchName}`, head], `record pushed mirror tip ${branchName}`);
1354
1358
  results.push({ branchName, head, pushed: true });
1355
1359
  }
1356
1360
  return results;
@@ -1363,21 +1367,23 @@ function assertProjectMirrorHeadsPushed(results) {
1363
1367
  }
1364
1368
  async function observeProjectMirrorHeads(input) {
1365
1369
  const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
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
- );
1379
- if (fetch.exitCode !== 0)
1380
- throw new Error(`Refresh project head mirror: ${fetch.stderr || fetch.stdout || `git exited ${fetch.exitCode}`}`);
1370
+ if (input.refreshRemote !== false) {
1371
+ const fetch = await gitResultAsync(
1372
+ primaryPath,
1373
+ [
1374
+ ...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
1375
+ "fetch",
1376
+ "--no-recurse-submodules",
1377
+ "--prune",
1378
+ input.mirrorUrl,
1379
+ "+refs/heads/*:refs/r5d/mirror/*",
1380
+ "+refs/r5d/server-synced/*:refs/r5d/server-synced/*"
1381
+ ],
1382
+ PROJECT_GIT_TRANSPORT_TIMEOUT_MS
1383
+ );
1384
+ if (fetch.exitCode !== 0)
1385
+ throw new Error(`Refresh project head mirror: ${fetch.stderr || fetch.stdout || `git exited ${fetch.exitCode}`}`);
1386
+ }
1381
1387
  const observations = [];
1382
1388
  for (const branchName of [...input.branchNames].sort()) {
1383
1389
  const checkoutPath = branchPath(input.projectRoot, branchName);
@@ -1423,6 +1429,24 @@ async function observeProjectMirrorHeads(input) {
1423
1429
  }
1424
1430
  return observations;
1425
1431
  }
1432
+ async function computeLocalProjectMirrorRefsToken(input) {
1433
+ const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
1434
+ const rows = await gitAsync(
1435
+ primaryPath,
1436
+ ["for-each-ref", "--format=%(objectname) %(refname)", import_project_mirror_refs_token.PROJECT_MIRROR_REF_PREFIX, import_project_mirror_refs_token.PROJECT_SERVER_SYNCED_REF_PREFIX],
1437
+ "list cached project mirror refs"
1438
+ );
1439
+ const refs = rows.split(/\r?\n/).filter(Boolean).map((row) => {
1440
+ const separator = row.indexOf(" ");
1441
+ const objectName = separator < 0 ? "" : row.slice(0, separator);
1442
+ const refname = separator < 0 ? "" : row.slice(separator + 1);
1443
+ if (!/^[0-9a-f]{40,64}$/.test(objectName) || refname.length === 0) {
1444
+ throw new Error(`Invalid cached mirror ref listing row: ${row}`);
1445
+ }
1446
+ return { refname, objectName };
1447
+ });
1448
+ return (0, import_project_mirror_refs_token.hashProjectMirrorRefs)((0, import_project_mirror_refs_token.canonicalizeLocalProjectMirrorRefs)(refs));
1449
+ }
1426
1450
  async function applyObservedProjectMirrorHeads(input) {
1427
1451
  const results = [];
1428
1452
  for (const observation of [...input.observations].sort((left, right) => left.branchName.localeCompare(right.branchName))) {
@@ -1505,6 +1529,7 @@ const projectWorktreesTestHarness = {
1505
1529
  ProjectBranchCreationRollbackIncompleteError,
1506
1530
  applyObservedProjectMirrorHeads,
1507
1531
  assertProjectMirrorHeadsPushed,
1532
+ computeLocalProjectMirrorRefsToken,
1508
1533
  createLinkedProjectBranch,
1509
1534
  createOrRetryLinkedProjectBranch,
1510
1535
  deleteLinkedProjectBranch,
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;
@@ -4110,6 +4133,17 @@ async function startWorker(options) {
4110
4133
  for (const result of results) {
4111
4134
  lastObservedProjectHeads.set(projectBranchKey(project.projectId, result.branchName), result.head);
4112
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
+ }
4113
4147
  }
4114
4148
  const publishedTombstoneIds = [
4115
4149
  ...new Set(
@@ -4135,6 +4169,7 @@ async function startWorker(options) {
4135
4169
  credentialHelper: deletion.credentialHelper,
4136
4170
  credentialUsername: deletion.credentialUsername
4137
4171
  });
4172
+ fetchedProjectMirrorRefsTokens.delete(deletion.projectId);
4138
4173
  }
4139
4174
  if (deletion.tombstoneId && !deletion.projectDeleted) {
4140
4175
  projectWorkspaceState = projectWorkspaceStateStore.recordMirrorRefDeletion({
@@ -4145,42 +4180,101 @@ async function startWorker(options) {
4145
4180
  pendingMirrorDeletes.delete(key);
4146
4181
  if (deletion.projectDeleted && !projectConfigById.has(deletion.projectId) && ![...pendingMirrorDeletes.values()].some((pending) => pending.projectId === deletion.projectId)) {
4147
4182
  removeProjectCredentialStore(deletion.projectId);
4183
+ advertisedProjectMirrorRefsTokens.delete(deletion.projectId);
4184
+ fetchedProjectMirrorRefsTokens.delete(deletion.projectId);
4185
+ lastProjectMirrorFetchAtMs.delete(deletion.projectId);
4148
4186
  }
4149
4187
  lastObservedProjectHeads.delete(key);
4150
4188
  }
4151
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
+ };
4152
4228
  const observeProjectHeadsBeforeOuterWorkspace = async (input) => {
4153
- const bundles = [];
4154
- for (const project of projectConfigById.values()) {
4155
- if (project.executionDisabled || !readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
4156
- try {
4157
- const connection = projectConnection(project);
4158
- const projectRoot = configuredProjectRoot(projectsRoot, project);
4159
- const primaryBranchName = primaryProjectBranch(project);
4160
- bundles.push({
4161
- projectId: project.projectId,
4162
- projectRoot,
4163
- primaryBranchName,
4164
- observations: await observeProjectMirrorHeads({
4165
- projectRoot,
4166
- primaryBranchName,
4167
- branchNames: project.branches.map(({ branchName }) => branchName),
4168
- mirrorUrl: connection.mirrorUrl,
4169
- credentialHelper: connection.credentialHelper,
4170
- credentialUsername: connection.mirrorCredentialUsername,
4171
- allowNonFastForward: input.allowNonFastForward,
4172
- branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId]))
4173
- })
4174
- });
4175
- } catch (error) {
4176
- 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) {
4177
4269
  process.stderr.write(
4178
- `[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)
4179
4271
  `
4180
4272
  );
4181
4273
  }
4274
+ return bundles.filter((bundle) => bundle !== null);
4275
+ } finally {
4276
+ lastWorkspaceSyncMirrorObservationMs = Date.now() - observationStartedAt;
4182
4277
  }
4183
- return bundles;
4184
4278
  };
4185
4279
  const applyObservedProjectHeadsAfterInboundWorkspace = async (bundles, activeMountIds, requireObservedHeads, integratedWorkspaceHead) => {
4186
4280
  const active = new Set(activeMountIds);
@@ -4449,6 +4543,7 @@ async function startWorker(options) {
4449
4543
  result = await workspaceSyncSingleFlight.runExclusive(async () => {
4450
4544
  queueEnteredAt = Date.now();
4451
4545
  syncStartedAt = Date.now();
4546
+ lastWorkspaceSyncMirrorObservationMs = 0;
4452
4547
  if (!deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
4453
4548
  return await failedWorkspaceSyncResult(
4454
4549
  attemptId,
@@ -4533,7 +4628,9 @@ async function startWorker(options) {
4533
4628
  telemetry: {
4534
4629
  totalMs: finishedAt - requestedAt,
4535
4630
  queueMs: queueEnteredAt - requestedAt,
4536
- 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,
4537
4634
  synchronizeMs: finishedAt - syncStartedAt
4538
4635
  }
4539
4636
  };
@@ -5251,7 +5348,8 @@ async function startWorker(options) {
5251
5348
  workspaceIncidentConfigDeferralV1: true,
5252
5349
  workspaceConfigResetToCanonicalV1: true,
5253
5350
  projectBranchDeletionFastAckV1: true,
5254
- projectMirrorLeaseV1: true
5351
+ projectMirrorLeaseV1: true,
5352
+ projectMirrorRefsTokensV1: true
5255
5353
  },
5256
5354
  projectRoot: projectsRoot,
5257
5355
  artifactRoot,
@@ -5289,6 +5387,17 @@ async function startWorker(options) {
5289
5387
  if (message.type === "connected") {
5290
5388
  return;
5291
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
+ }
5292
5401
  if (message.type === "workspace_config") {
5293
5402
  const receiptGeneration = ++workspaceConfigurationReceiptGeneration;
5294
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.94",
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
+ };
@@ -4,6 +4,12 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { gitCredentialUsernameConfigKey, gitTransportSecurityArgs, workerGitProcessEnvironment } from "./git-process-environment.mjs";
6
6
  import { validateManagedBranchName } from "./managed-paths.mjs";
7
+ import {
8
+ canonicalizeLocalProjectMirrorRefs,
9
+ hashProjectMirrorRefs,
10
+ PROJECT_MIRROR_REF_PREFIX,
11
+ PROJECT_SERVER_SYNCED_REF_PREFIX
12
+ } from "./project-mirror-refs-token.mjs";
7
13
  import { mirrorWorkingTree } from "./working-tree-mirror.mjs";
8
14
  const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
9
15
  const PROJECT_WORKTREE_SNAPSHOT_MANIFEST = "transaction.json";
@@ -1241,6 +1247,7 @@ ${deleted.stdout}`)) {
1241
1247
  `Delete hidden project mirror for ${input.branchName}: ${deleted.stderr || deleted.stdout || `git exited ${deleted.exitCode}`}`
1242
1248
  );
1243
1249
  }
1250
+ await gitResultAsync(input.gitDirectory, ["update-ref", "-d", `refs/r5d/mirror/${input.branchName}`]);
1244
1251
  }
1245
1252
  async function pushProjectMirrorHeads(input) {
1246
1253
  const results = [];
@@ -1298,6 +1305,7 @@ async function pushProjectMirrorHeads(input) {
1298
1305
  if (pushed.exitCode !== 0) {
1299
1306
  throw new Error(`Push hidden project mirror for ${branchName}: ${pushed.stderr || pushed.stdout || `git exited ${pushed.exitCode}`}`);
1300
1307
  }
1308
+ await gitAsync(checkoutPath, ["update-ref", `refs/r5d/mirror/${branchName}`, head], `record pushed mirror tip ${branchName}`);
1301
1309
  results.push({ branchName, head, pushed: true });
1302
1310
  }
1303
1311
  return results;
@@ -1310,21 +1318,23 @@ function assertProjectMirrorHeadsPushed(results) {
1310
1318
  }
1311
1319
  async function observeProjectMirrorHeads(input) {
1312
1320
  const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
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
- );
1326
- if (fetch.exitCode !== 0)
1327
- throw new Error(`Refresh project head mirror: ${fetch.stderr || fetch.stdout || `git exited ${fetch.exitCode}`}`);
1321
+ if (input.refreshRemote !== false) {
1322
+ const fetch = await gitResultAsync(
1323
+ primaryPath,
1324
+ [
1325
+ ...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
1326
+ "fetch",
1327
+ "--no-recurse-submodules",
1328
+ "--prune",
1329
+ input.mirrorUrl,
1330
+ "+refs/heads/*:refs/r5d/mirror/*",
1331
+ "+refs/r5d/server-synced/*:refs/r5d/server-synced/*"
1332
+ ],
1333
+ PROJECT_GIT_TRANSPORT_TIMEOUT_MS
1334
+ );
1335
+ if (fetch.exitCode !== 0)
1336
+ throw new Error(`Refresh project head mirror: ${fetch.stderr || fetch.stdout || `git exited ${fetch.exitCode}`}`);
1337
+ }
1328
1338
  const observations = [];
1329
1339
  for (const branchName of [...input.branchNames].sort()) {
1330
1340
  const checkoutPath = branchPath(input.projectRoot, branchName);
@@ -1370,6 +1380,24 @@ async function observeProjectMirrorHeads(input) {
1370
1380
  }
1371
1381
  return observations;
1372
1382
  }
1383
+ async function computeLocalProjectMirrorRefsToken(input) {
1384
+ const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
1385
+ const rows = await gitAsync(
1386
+ primaryPath,
1387
+ ["for-each-ref", "--format=%(objectname) %(refname)", PROJECT_MIRROR_REF_PREFIX, PROJECT_SERVER_SYNCED_REF_PREFIX],
1388
+ "list cached project mirror refs"
1389
+ );
1390
+ const refs = rows.split(/\r?\n/).filter(Boolean).map((row) => {
1391
+ const separator = row.indexOf(" ");
1392
+ const objectName = separator < 0 ? "" : row.slice(0, separator);
1393
+ const refname = separator < 0 ? "" : row.slice(separator + 1);
1394
+ if (!/^[0-9a-f]{40,64}$/.test(objectName) || refname.length === 0) {
1395
+ throw new Error(`Invalid cached mirror ref listing row: ${row}`);
1396
+ }
1397
+ return { refname, objectName };
1398
+ });
1399
+ return hashProjectMirrorRefs(canonicalizeLocalProjectMirrorRefs(refs));
1400
+ }
1373
1401
  async function applyObservedProjectMirrorHeads(input) {
1374
1402
  const results = [];
1375
1403
  for (const observation of [...input.observations].sort((left, right) => left.branchName.localeCompare(right.branchName))) {
@@ -1451,6 +1479,7 @@ export {
1451
1479
  ProjectBranchCreationRollbackIncompleteError,
1452
1480
  applyObservedProjectMirrorHeads,
1453
1481
  assertProjectMirrorHeadsPushed,
1482
+ computeLocalProjectMirrorRefsToken,
1454
1483
  createLinkedProjectBranch,
1455
1484
  createOrRetryLinkedProjectBranch,
1456
1485
  deleteLinkedProjectBranch,
@@ -125,6 +125,15 @@ type WorkerServerMessage = {
125
125
  confirmationReason?: string;
126
126
  resetToCanonical?: boolean;
127
127
  requiredAncestorHeads?: string[];
128
+ } | {
129
+ /**
130
+ * Content-derived tokens naming each canonical project repository's
131
+ * observable ref state, keyed by project id. Replace-all: projects
132
+ * absent from a message have no advertised token and observation falls
133
+ * back to fetching. See `project-mirror-fetch-policy.ts`.
134
+ */
135
+ type: "project_mirror_refs_tokens";
136
+ tokens: Record<string, string>;
128
137
  } | {
129
138
  type: "create_project_branch";
130
139
  requestId: string;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Decides whether a periodic observation must fetch a project's hidden mirror
3
+ * or may reuse the locally cached mirror refs. The server advertises a
4
+ * content-derived token per project (hash of the exact ref namespaces the
5
+ * observation fetch would transfer); when the advertised token equals the
6
+ * token the local refs already cover, the fetch would be a no-op.
7
+ *
8
+ * Skipping is fail-open: any missing or mismatched information fetches. The
9
+ * backstop bounds the damage of a server-side token bug (for example a token
10
+ * computed against the wrong repository path) to one interval; every
11
+ * git-transport writer is observed by the server's on-disk recompute, so the
12
+ * backstop is insurance against bugs, not against known unobserved writers.
13
+ */
14
+ export declare const PROJECT_MIRROR_FETCH_BACKSTOP_MS: number;
15
+ export type ProjectMirrorFetchDecision = {
16
+ fetch: true;
17
+ reason: "skipping_disabled" | "no_advertised_token" | "never_fetched" | "token_changed" | "backstop_elapsed";
18
+ } | {
19
+ fetch: false;
20
+ reason: "token_match";
21
+ };
22
+ export declare function projectMirrorFetchDecision(input: {
23
+ advertisedToken: string | undefined;
24
+ fetchedToken: string | undefined;
25
+ lastFetchAtMs: number | undefined;
26
+ nowMs: number;
27
+ skippingEnabled: boolean;
28
+ backstopMs?: number;
29
+ }): ProjectMirrorFetchDecision;
@@ -0,0 +1,19 @@
1
+ export declare const PROJECT_MIRROR_REF_PREFIX = "refs/r5d/mirror/";
2
+ export declare const PROJECT_SERVER_SYNCED_REF_PREFIX = "refs/r5d/server-synced/";
3
+ export type ProjectMirrorRef = {
4
+ refname: string;
5
+ objectName: string;
6
+ };
7
+ /**
8
+ * Hash a ref listing into a mirror-refs token. Both sides sort here rather
9
+ * than trusting git's listing order: renaming `refs/r5d/mirror/*` back to
10
+ * `refs/heads/*` changes sort neighborhoods.
11
+ */
12
+ export declare function hashProjectMirrorRefs(refs: readonly ProjectMirrorRef[]): string;
13
+ /**
14
+ * Rename locally fetched mirror refs into the namespaces the server hashes.
15
+ * Rows outside the two observed namespaces are dropped; the caller lists only
16
+ * those namespaces, so a dropped row means an unexpected listing, and dropping
17
+ * it can only make tokens differ — which fails open into a fetch.
18
+ */
19
+ export declare function canonicalizeLocalProjectMirrorRefs(refs: readonly ProjectMirrorRef[]): ProjectMirrorRef[];
@@ -212,11 +212,32 @@ export type ProjectMirrorHeadObservation = {
212
212
  * Fetch and pin the exact hidden-mirror tips that may be applied after a
213
213
  * causally later outer-workspace fetch and hydration. Observation deliberately
214
214
  * does not mutate a live branch or its index.
215
+ *
216
+ * With `refreshRemote: false` the network fetch is skipped and the tips come
217
+ * from the locally cached `refs/r5d/mirror/*` / `refs/r5d/server-synced/*`
218
+ * refs of an earlier fetch. That is safe in the stale direction (a newer
219
+ * mirror tip waits for a later cycle, exactly like a tip that lands mid-cycle
220
+ * today) and in the fresh direction (hidden-mirror publication is tree-first,
221
+ * so any locally cached tip's covering outer tree was published before the
222
+ * fetch that cached it, hence before this cycle's outer fetch).
215
223
  */
216
224
  export declare function observeProjectMirrorHeads(input: Omit<ProjectMirrorHeadUpdateInput, "branchMutationAllowed"> & {
217
225
  allowNonFastForward: boolean;
218
226
  branchIds?: ReadonlyMap<string, string>;
227
+ refreshRemote?: boolean;
219
228
  }): Promise<ProjectMirrorHeadObservation[]>;
229
+ /**
230
+ * Compute the mirror-refs token for the locally cached mirror state — the
231
+ * token the server will advertise once it hashes the same refs. After a
232
+ * successful lease push (which updates `refs/r5d/mirror/*` above), this
233
+ * matches the server's next advertisement unless something else also moved
234
+ * the canonical repository, in which case the mismatch fails open into a
235
+ * fetch. Formula twin: `server/git/project-mirror-refs-token.ts`.
236
+ */
237
+ export declare function computeLocalProjectMirrorRefsToken(input: {
238
+ projectRoot: string;
239
+ primaryBranchName: string;
240
+ }): Promise<string>;
220
241
  /** Apply only previously observed object ids; this function never refetches. */
221
242
  export declare function applyObservedProjectMirrorHeads(input: {
222
243
  projectRoot: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.94",
3
+ "version": "0.0.95",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",