@ricsam/r5d-worker 0.0.51 → 0.0.54

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.
@@ -0,0 +1,92 @@
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 workspace_mutation_gate_exports = {};
20
+ __export(workspace_mutation_gate_exports, {
21
+ WorkspaceMutationGate: () => WorkspaceMutationGate
22
+ });
23
+ module.exports = __toCommonJS(workspace_mutation_gate_exports);
24
+ class WorkspaceMutationGate {
25
+ activeMutations = 0;
26
+ syncActive = false;
27
+ waiters = [];
28
+ acquireMutation() {
29
+ return new Promise((resolve) => {
30
+ this.waiters.push({ kind: "mutation", resolve });
31
+ this.drain();
32
+ });
33
+ }
34
+ async runMutation(operation) {
35
+ const release = await this.acquireMutation();
36
+ try {
37
+ return await operation();
38
+ } finally {
39
+ release();
40
+ }
41
+ }
42
+ async runSync(operation) {
43
+ const release = await new Promise((resolve) => {
44
+ this.waiters.push({ kind: "sync", resolve });
45
+ this.drain();
46
+ });
47
+ try {
48
+ return await operation();
49
+ } finally {
50
+ release();
51
+ }
52
+ }
53
+ drain() {
54
+ if (this.syncActive || this.waiters.length === 0) return;
55
+ const first = this.waiters[0];
56
+ if (first?.kind === "sync") {
57
+ if (this.activeMutations > 0) return;
58
+ this.waiters.shift();
59
+ this.syncActive = true;
60
+ first.resolve(this.releaseSync());
61
+ return;
62
+ }
63
+ while (this.waiters[0]?.kind === "mutation") {
64
+ const waiter = this.waiters.shift();
65
+ if (!waiter || waiter.kind !== "mutation") break;
66
+ this.activeMutations += 1;
67
+ waiter.resolve(this.releaseMutation());
68
+ }
69
+ }
70
+ releaseMutation() {
71
+ let released = false;
72
+ return () => {
73
+ if (released) return;
74
+ released = true;
75
+ this.activeMutations -= 1;
76
+ this.drain();
77
+ };
78
+ }
79
+ releaseSync() {
80
+ let released = false;
81
+ return () => {
82
+ if (released) return;
83
+ released = true;
84
+ this.syncActive = false;
85
+ this.drain();
86
+ };
87
+ }
88
+ }
89
+ // Annotate the CommonJS export names for ESM import in node:
90
+ 0 && (module.exports = {
91
+ WorkspaceMutationGate
92
+ });
@@ -30,10 +30,12 @@ var workspace_sync_exports = {};
30
30
  __export(workspace_sync_exports, {
31
31
  MAX_WORKSPACE_SYNC_DIFF_BYTES: () => MAX_WORKSPACE_SYNC_DIFF_BYTES,
32
32
  WORKSPACE_BRANCH: () => WORKSPACE_BRANCH,
33
+ WORKSPACE_INTENT_REF_PREFIX: () => WORKSPACE_INTENT_REF_PREFIX,
33
34
  WORKSPACE_NATIVE_PLANS_RELATIVE_PATH: () => WORKSPACE_NATIVE_PLANS_RELATIVE_PATH,
34
35
  WORKSPACE_PERIODIC_SCAN_INTERVAL_MS: () => WORKSPACE_PERIODIC_SCAN_INTERVAL_MS,
35
36
  WorkspaceSyncSingleFlight: () => WorkspaceSyncSingleFlight,
36
37
  assertCanonicalCheckoutIndexMatchesWorktree: () => assertCanonicalCheckoutIndexMatchesWorktree,
38
+ assertWorkspaceQuarantineRef: () => assertWorkspaceQuarantineRef,
37
39
  calculateWorkspaceDiffFingerprint: () => calculateWorkspaceDiffFingerprint,
38
40
  encodeWorkspaceBranch: () => encodeWorkspaceBranch,
39
41
  mirrorShadowWorkspaceToVisible: () => mirrorShadowWorkspaceToVisible,
@@ -49,7 +51,9 @@ var import_node_crypto = require("node:crypto");
49
51
  var import_node_fs = __toESM(require("node:fs"), 1);
50
52
  var import_node_path = __toESM(require("node:path"), 1);
51
53
  var import_managed_paths = require("./managed-paths.cjs");
54
+ var import_workspace_mutation_gate = require("./workspace-mutation-gate.cjs");
52
55
  const WORKSPACE_BRANCH = "main";
56
+ const WORKSPACE_INTENT_REF_PREFIX = "refs/r5d/workspace-intents/";
53
57
  const MAX_WORKSPACE_SYNC_DIFF_BYTES = 5 * 1024 * 1024;
54
58
  const WORKSPACE_PERIODIC_SCAN_INTERVAL_MS = 5e3;
55
59
  const MAX_REPORTED_WORKSPACE_PATHS = 2e3;
@@ -1119,10 +1123,27 @@ function commitMessage(input) {
1119
1123
  ...input.confirmedLargeDiff ? { confirmedLargeDiff: true, confirmationReason: input.confirmationReason } : {}
1120
1124
  });
1121
1125
  }
1122
- function isNonFastForward(result) {
1123
- const output = `${result.stdout}
1124
- ${result.stderr}`.toLowerCase();
1125
- return output.includes("non-fast-forward") || output.includes("fetch first") || output.includes("[rejected]");
1126
+ function assertWorkspaceQuarantineRef(quarantineRef) {
1127
+ const suffix = quarantineRef.slice(WORKSPACE_INTENT_REF_PREFIX.length);
1128
+ if (!quarantineRef.startsWith(WORKSPACE_INTENT_REF_PREFIX) || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(suffix)) {
1129
+ throw new Error(`Workspace candidate ref must be below ${WORKSPACE_INTENT_REF_PREFIX}`);
1130
+ }
1131
+ const result = Bun.spawnSync(["git", "check-ref-format", quarantineRef], {
1132
+ stdout: "pipe",
1133
+ stderr: "pipe",
1134
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
1135
+ });
1136
+ if (result.exitCode !== 0) {
1137
+ throw new Error(`Invalid workspace candidate ref ${JSON.stringify(quarantineRef)}`);
1138
+ }
1139
+ }
1140
+ function uploadWorkspaceCandidate(input, candidateHead) {
1141
+ const quarantineRef = input.quarantineRef;
1142
+ if (!quarantineRef) {
1143
+ throw new Error("A server-issued workspace candidate ref is required before uploading a canonical workspace candidate");
1144
+ }
1145
+ assertWorkspaceQuarantineRef(quarantineRef);
1146
+ return runGitResult(input, input.shadowRoot, ["push", "origin", `HEAD:${quarantineRef}`]);
1126
1147
  }
1127
1148
  function resetShadowToRemote(input, opaqueWorkspaceRoots = []) {
1128
1149
  runGit(input, input.shadowRoot, ["fetch", "origin", "--prune"], "fetch canonical workspace before reset");
@@ -1141,6 +1162,7 @@ function baseResult(input, startingHead) {
1141
1162
  workerLabel: input.workerLabel,
1142
1163
  trigger: input.trigger,
1143
1164
  startingHead,
1165
+ expectedHead: startingHead,
1144
1166
  rebaseCount: 0,
1145
1167
  diffSizeBytes: 0,
1146
1168
  gitStatus: "",
@@ -1150,6 +1172,32 @@ function baseResult(input, startingHead) {
1150
1172
  localChangesDiscarded: false
1151
1173
  };
1152
1174
  }
1175
+ function convergeRedundantInboundCandidate(input, result) {
1176
+ if (input.trigger.type !== "inbound_head" || !input.skipVisibleMirror || input.trigger.canonicalCheckoutOnly) return null;
1177
+ const remoteHead = revParse(input, `origin/${WORKSPACE_BRANCH}`);
1178
+ if (!remoteHead) return null;
1179
+ const stagedTree = runGit(input, input.shadowRoot, ["write-tree"], "write inbound workspace candidate tree");
1180
+ const remoteTree = runGit(
1181
+ input,
1182
+ input.shadowRoot,
1183
+ ["rev-parse", "--verify", `${remoteHead}^{tree}`],
1184
+ "resolve authoritative workspace tree"
1185
+ );
1186
+ if (stagedTree !== remoteTree) return null;
1187
+ const localHead = revParse(input, "HEAD");
1188
+ runGit(input, input.shadowRoot, ["reset", "--hard", remoteHead], "converge redundant inbound workspace candidate");
1189
+ runGit(input, input.shadowRoot, ["clean", "-fd"], "clean redundant inbound workspace candidate");
1190
+ mirrorShadowWorkspaceToVisible(input);
1191
+ return {
1192
+ ...result,
1193
+ outcome: localHead === remoteHead ? "no_change" : "updated",
1194
+ publishedHead: remoteHead,
1195
+ diffSizeBytes: 0,
1196
+ gitStatus: gitStatus(input),
1197
+ affectedProjects: [],
1198
+ affectedPaths: []
1199
+ };
1200
+ }
1153
1201
  async function synchronizeWorkspace(rawInput) {
1154
1202
  let input = { ...rawInput, attemptId: rawInput.attemptId ?? crypto.randomUUID() };
1155
1203
  try {
@@ -1183,6 +1231,8 @@ async function synchronizeWorkspace(rawInput) {
1183
1231
  const stagePathspecs = mirroredGitlinkProjection.opaqueRoots.flatMap((root) => [`:(exclude,literal)${root}`]);
1184
1232
  runGit(input, input.shadowRoot, ["add", "-A", "--", ".", ...stagePathspecs], "stage workspace changes");
1185
1233
  forceStageCanonicalCheckoutFiles(input, mirroredGitlinkProjection.entries, mirroredGitlinkProjection.opaqueRoots);
1234
+ const redundantInboundCandidate = convergeRedundantInboundCandidate(input, result);
1235
+ if (redundantInboundCandidate) return redundantInboundCandidate;
1186
1236
  const sampledCanonicalCheckoutTreeHash = sampleCanonicalCheckoutTree(input);
1187
1237
  const stagedWorkingPaths = stagedPaths(input);
1188
1238
  const baseRevision = candidateBaseRevision(input);
@@ -1216,7 +1266,7 @@ async function synchronizeWorkspace(rawInput) {
1216
1266
  if (stagedWorkingPaths.length > 0) {
1217
1267
  runGit(input, input.shadowRoot, ["commit", "-m", commitMessage(input)], "commit workspace changes");
1218
1268
  }
1219
- const candidateHead = revParse(input, "HEAD");
1269
+ let candidateHead = revParse(input, "HEAD");
1220
1270
  if (candidateHead) assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, candidateHead);
1221
1271
  const hasRemoteMain = Boolean(revParse(input, `origin/${WORKSPACE_BRANCH}`));
1222
1272
  const hasUnpushedCommit = candidateHead ? !hasRemoteMain || !tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", candidateHead, `origin/${WORKSPACE_BRANCH}`]) : false;
@@ -1252,58 +1302,59 @@ async function synchronizeWorkspace(rawInput) {
1252
1302
  };
1253
1303
  }
1254
1304
  let rebaseCount = 0;
1255
- for (let pushAttempt = 0; pushAttempt < 3; pushAttempt += 1) {
1256
- const push = runGitResult(input, input.shadowRoot, ["push", "origin", `HEAD:refs/heads/${WORKSPACE_BRANCH}`]);
1257
- if (push.exitCode === 0) {
1258
- const publishedHead = revParse(input, "HEAD") ?? candidateHead;
1259
- if (publishedHead) assertPublishedCanonicalCheckoutTree(input, publishedHead, sampledCanonicalCheckoutTreeHash);
1260
- mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots);
1261
- return {
1262
- ...observed,
1263
- outcome: "published",
1264
- candidateHead: candidateHead ?? void 0,
1265
- publishedHead: publishedHead ?? void 0,
1266
- rebaseCount,
1267
- gitStatus: gitStatus(input)
1268
- };
1269
- }
1270
- if (!isNonFastForward(push)) {
1271
- return {
1272
- ...observed,
1273
- outcome: "failed",
1274
- candidateHead: candidateHead ?? void 0,
1275
- rebaseCount,
1276
- error: push.stderr || push.stdout || "Workspace push failed",
1277
- gitStatus: gitStatus(input)
1278
- };
1279
- }
1280
- runGit(input, input.shadowRoot, ["fetch", "origin", "--prune"], "fetch workspace after push race");
1305
+ const expectedHead = revParse(input, `origin/${WORKSPACE_BRANCH}`);
1306
+ if (candidateHead && expectedHead && !tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", expectedHead, candidateHead])) {
1281
1307
  const rebase = runGitResult(input, input.shadowRoot, ["rebase", `origin/${WORKSPACE_BRANCH}`]);
1282
1308
  rebaseCount += 1;
1283
1309
  if (rebase.exitCode !== 0) {
1284
1310
  const conflicted = runGitResult(input, input.shadowRoot, ["diff", "--name-only", "--diff-filter=U", "-z"]);
1285
- const discardedPaths = reportedPaths([.../* @__PURE__ */ new Set([...paths, ...conflicted.stdout.split("\0").filter(Boolean)])].sort());
1286
- const publishedHead = resetShadowToRemote(input, mirroredGitlinkProjection.opaqueRoots);
1311
+ const conflictPaths = reportedPaths([.../* @__PURE__ */ new Set([...paths, ...conflicted.stdout.split("\0").filter(Boolean)])].sort());
1312
+ runGit(input, input.shadowRoot, ["rebase", "--abort"], "preserve workspace candidate after rebase conflict");
1287
1313
  return {
1288
1314
  ...observed,
1289
- outcome: "conflict_reset",
1315
+ outcome: "failed",
1316
+ expectedHead,
1290
1317
  candidateHead: candidateHead ?? void 0,
1291
- publishedHead: publishedHead ?? void 0,
1292
1318
  rebaseCount,
1293
- discardedPaths,
1294
- localChangesDiscarded: true,
1319
+ affectedPaths: conflictPaths,
1320
+ error: `Workspace candidate conflicts with canonical head ${expectedHead}; local changes were preserved for reconciliation.`,
1295
1321
  gitStatus: gitStatus(input)
1296
1322
  };
1297
1323
  }
1324
+ candidateHead = revParse(input, "HEAD");
1298
1325
  assertPublishedCanonicalCheckoutTree(input, "HEAD", sampledCanonicalCheckoutTreeHash);
1299
1326
  assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, "HEAD");
1300
1327
  }
1328
+ if (!candidateHead) {
1329
+ return {
1330
+ ...observed,
1331
+ outcome: "failed",
1332
+ rebaseCount,
1333
+ error: "Workspace candidate does not have a commit",
1334
+ gitStatus: gitStatus(input)
1335
+ };
1336
+ }
1337
+ const upload = uploadWorkspaceCandidate(input, candidateHead);
1338
+ if (upload.exitCode !== 0) {
1339
+ return {
1340
+ ...observed,
1341
+ outcome: "failed",
1342
+ candidateHead,
1343
+ quarantineRef: input.quarantineRef,
1344
+ rebaseCount,
1345
+ error: upload.stderr || upload.stdout || "Workspace candidate upload failed",
1346
+ gitStatus: gitStatus(input)
1347
+ };
1348
+ }
1349
+ assertPublishedCanonicalCheckoutTree(input, candidateHead, sampledCanonicalCheckoutTreeHash);
1350
+ mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots);
1301
1351
  return {
1302
1352
  ...observed,
1303
- outcome: "failed",
1304
- candidateHead: candidateHead ?? void 0,
1353
+ outcome: "candidate_ready",
1354
+ expectedHead,
1355
+ candidateHead,
1356
+ quarantineRef: input.quarantineRef,
1305
1357
  rebaseCount,
1306
- error: "Workspace push did not converge after three attempts",
1307
1358
  gitStatus: gitStatus(input)
1308
1359
  };
1309
1360
  } catch (error) {
@@ -1346,9 +1397,10 @@ function calculateWorkspaceDiffFingerprint(rawInput) {
1346
1397
  return (0, import_node_crypto.createHash)("sha256").update(JSON.stringify({ stagedTree, localHead, remoteHead })).digest("hex");
1347
1398
  }
1348
1399
  class WorkspaceSyncSingleFlight {
1400
+ mutationGate = new import_workspace_mutation_gate.WorkspaceMutationGate();
1349
1401
  queue = Promise.resolve();
1350
1402
  enqueue(task) {
1351
- const queued = this.queue.then(task, task);
1403
+ const queued = this.mutationGate.runSync(task);
1352
1404
  this.queue = queued.then(
1353
1405
  () => void 0,
1354
1406
  () => void 0
@@ -1367,6 +1419,12 @@ class WorkspaceSyncSingleFlight {
1367
1419
  fingerprintPrepared(prepare) {
1368
1420
  return this.enqueue(() => calculateWorkspaceDiffFingerprint(prepare()));
1369
1421
  }
1422
+ runMutation(operation) {
1423
+ return this.mutationGate.runMutation(operation);
1424
+ }
1425
+ acquireMutation() {
1426
+ return this.mutationGate.acquireMutation();
1427
+ }
1370
1428
  afterCurrent() {
1371
1429
  return this.queue;
1372
1430
  }
@@ -1375,10 +1433,12 @@ class WorkspaceSyncSingleFlight {
1375
1433
  0 && (module.exports = {
1376
1434
  MAX_WORKSPACE_SYNC_DIFF_BYTES,
1377
1435
  WORKSPACE_BRANCH,
1436
+ WORKSPACE_INTENT_REF_PREFIX,
1378
1437
  WORKSPACE_NATIVE_PLANS_RELATIVE_PATH,
1379
1438
  WORKSPACE_PERIODIC_SCAN_INTERVAL_MS,
1380
1439
  WorkspaceSyncSingleFlight,
1381
1440
  assertCanonicalCheckoutIndexMatchesWorktree,
1441
+ assertWorkspaceQuarantineRef,
1382
1442
  calculateWorkspaceDiffFingerprint,
1383
1443
  encodeWorkspaceBranch,
1384
1444
  mirrorShadowWorkspaceToVisible,
@@ -3,8 +3,19 @@ const WORKER_HEARTBEAT_TIMEOUT_MS = 45e3;
3
3
  function hasWorkerHeartbeatTimedOut(lastHeartbeatAt, now = Date.now(), timeoutMs = WORKER_HEARTBEAT_TIMEOUT_MS) {
4
4
  return lastHeartbeatAt !== null && now - lastHeartbeatAt >= timeoutMs;
5
5
  }
6
+ function grantWorkerHeartbeatBusyGrace(lastHeartbeatAt, currentGrace, now = Date.now(), timeoutMs = WORKER_HEARTBEAT_TIMEOUT_MS, graceMs = WORKER_HEARTBEAT_INTERVAL_MS) {
7
+ if (!hasWorkerHeartbeatTimedOut(lastHeartbeatAt, now, timeoutMs)) return currentGrace;
8
+ if (currentGrace?.heartbeatAt === lastHeartbeatAt) return currentGrace;
9
+ return { heartbeatAt: lastHeartbeatAt, expiresAt: now + graceMs };
10
+ }
11
+ function hasWorkerHeartbeatTimedOutWithBusyGrace(lastHeartbeatAt, busyGrace, now = Date.now(), timeoutMs = WORKER_HEARTBEAT_TIMEOUT_MS) {
12
+ if (lastHeartbeatAt !== null && busyGrace?.heartbeatAt === lastHeartbeatAt && now < busyGrace.expiresAt) return false;
13
+ return hasWorkerHeartbeatTimedOut(lastHeartbeatAt, now, timeoutMs);
14
+ }
6
15
  export {
7
16
  WORKER_HEARTBEAT_INTERVAL_MS,
8
17
  WORKER_HEARTBEAT_TIMEOUT_MS,
9
- hasWorkerHeartbeatTimedOut
18
+ grantWorkerHeartbeatBusyGrace,
19
+ hasWorkerHeartbeatTimedOut,
20
+ hasWorkerHeartbeatTimedOutWithBusyGrace
10
21
  };