@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.
@@ -2,7 +2,9 @@ import { createHash } from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { managedBranchPath } from "./managed-paths.mjs";
5
+ import { WorkspaceMutationGate } from "./workspace-mutation-gate.mjs";
5
6
  const WORKSPACE_BRANCH = "main";
7
+ const WORKSPACE_INTENT_REF_PREFIX = "refs/r5d/workspace-intents/";
6
8
  const MAX_WORKSPACE_SYNC_DIFF_BYTES = 5 * 1024 * 1024;
7
9
  const WORKSPACE_PERIODIC_SCAN_INTERVAL_MS = 5e3;
8
10
  const MAX_REPORTED_WORKSPACE_PATHS = 2e3;
@@ -1072,10 +1074,27 @@ function commitMessage(input) {
1072
1074
  ...input.confirmedLargeDiff ? { confirmedLargeDiff: true, confirmationReason: input.confirmationReason } : {}
1073
1075
  });
1074
1076
  }
1075
- function isNonFastForward(result) {
1076
- const output = `${result.stdout}
1077
- ${result.stderr}`.toLowerCase();
1078
- return output.includes("non-fast-forward") || output.includes("fetch first") || output.includes("[rejected]");
1077
+ function assertWorkspaceQuarantineRef(quarantineRef) {
1078
+ const suffix = quarantineRef.slice(WORKSPACE_INTENT_REF_PREFIX.length);
1079
+ 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)) {
1080
+ throw new Error(`Workspace candidate ref must be below ${WORKSPACE_INTENT_REF_PREFIX}`);
1081
+ }
1082
+ const result = Bun.spawnSync(["git", "check-ref-format", quarantineRef], {
1083
+ stdout: "pipe",
1084
+ stderr: "pipe",
1085
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
1086
+ });
1087
+ if (result.exitCode !== 0) {
1088
+ throw new Error(`Invalid workspace candidate ref ${JSON.stringify(quarantineRef)}`);
1089
+ }
1090
+ }
1091
+ function uploadWorkspaceCandidate(input, candidateHead) {
1092
+ const quarantineRef = input.quarantineRef;
1093
+ if (!quarantineRef) {
1094
+ throw new Error("A server-issued workspace candidate ref is required before uploading a canonical workspace candidate");
1095
+ }
1096
+ assertWorkspaceQuarantineRef(quarantineRef);
1097
+ return runGitResult(input, input.shadowRoot, ["push", "origin", `HEAD:${quarantineRef}`]);
1079
1098
  }
1080
1099
  function resetShadowToRemote(input, opaqueWorkspaceRoots = []) {
1081
1100
  runGit(input, input.shadowRoot, ["fetch", "origin", "--prune"], "fetch canonical workspace before reset");
@@ -1094,6 +1113,7 @@ function baseResult(input, startingHead) {
1094
1113
  workerLabel: input.workerLabel,
1095
1114
  trigger: input.trigger,
1096
1115
  startingHead,
1116
+ expectedHead: startingHead,
1097
1117
  rebaseCount: 0,
1098
1118
  diffSizeBytes: 0,
1099
1119
  gitStatus: "",
@@ -1103,6 +1123,32 @@ function baseResult(input, startingHead) {
1103
1123
  localChangesDiscarded: false
1104
1124
  };
1105
1125
  }
1126
+ function convergeRedundantInboundCandidate(input, result) {
1127
+ if (input.trigger.type !== "inbound_head" || !input.skipVisibleMirror || input.trigger.canonicalCheckoutOnly) return null;
1128
+ const remoteHead = revParse(input, `origin/${WORKSPACE_BRANCH}`);
1129
+ if (!remoteHead) return null;
1130
+ const stagedTree = runGit(input, input.shadowRoot, ["write-tree"], "write inbound workspace candidate tree");
1131
+ const remoteTree = runGit(
1132
+ input,
1133
+ input.shadowRoot,
1134
+ ["rev-parse", "--verify", `${remoteHead}^{tree}`],
1135
+ "resolve authoritative workspace tree"
1136
+ );
1137
+ if (stagedTree !== remoteTree) return null;
1138
+ const localHead = revParse(input, "HEAD");
1139
+ runGit(input, input.shadowRoot, ["reset", "--hard", remoteHead], "converge redundant inbound workspace candidate");
1140
+ runGit(input, input.shadowRoot, ["clean", "-fd"], "clean redundant inbound workspace candidate");
1141
+ mirrorShadowWorkspaceToVisible(input);
1142
+ return {
1143
+ ...result,
1144
+ outcome: localHead === remoteHead ? "no_change" : "updated",
1145
+ publishedHead: remoteHead,
1146
+ diffSizeBytes: 0,
1147
+ gitStatus: gitStatus(input),
1148
+ affectedProjects: [],
1149
+ affectedPaths: []
1150
+ };
1151
+ }
1106
1152
  async function synchronizeWorkspace(rawInput) {
1107
1153
  let input = { ...rawInput, attemptId: rawInput.attemptId ?? crypto.randomUUID() };
1108
1154
  try {
@@ -1136,6 +1182,8 @@ async function synchronizeWorkspace(rawInput) {
1136
1182
  const stagePathspecs = mirroredGitlinkProjection.opaqueRoots.flatMap((root) => [`:(exclude,literal)${root}`]);
1137
1183
  runGit(input, input.shadowRoot, ["add", "-A", "--", ".", ...stagePathspecs], "stage workspace changes");
1138
1184
  forceStageCanonicalCheckoutFiles(input, mirroredGitlinkProjection.entries, mirroredGitlinkProjection.opaqueRoots);
1185
+ const redundantInboundCandidate = convergeRedundantInboundCandidate(input, result);
1186
+ if (redundantInboundCandidate) return redundantInboundCandidate;
1139
1187
  const sampledCanonicalCheckoutTreeHash = sampleCanonicalCheckoutTree(input);
1140
1188
  const stagedWorkingPaths = stagedPaths(input);
1141
1189
  const baseRevision = candidateBaseRevision(input);
@@ -1169,7 +1217,7 @@ async function synchronizeWorkspace(rawInput) {
1169
1217
  if (stagedWorkingPaths.length > 0) {
1170
1218
  runGit(input, input.shadowRoot, ["commit", "-m", commitMessage(input)], "commit workspace changes");
1171
1219
  }
1172
- const candidateHead = revParse(input, "HEAD");
1220
+ let candidateHead = revParse(input, "HEAD");
1173
1221
  if (candidateHead) assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, candidateHead);
1174
1222
  const hasRemoteMain = Boolean(revParse(input, `origin/${WORKSPACE_BRANCH}`));
1175
1223
  const hasUnpushedCommit = candidateHead ? !hasRemoteMain || !tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", candidateHead, `origin/${WORKSPACE_BRANCH}`]) : false;
@@ -1205,58 +1253,59 @@ async function synchronizeWorkspace(rawInput) {
1205
1253
  };
1206
1254
  }
1207
1255
  let rebaseCount = 0;
1208
- for (let pushAttempt = 0; pushAttempt < 3; pushAttempt += 1) {
1209
- const push = runGitResult(input, input.shadowRoot, ["push", "origin", `HEAD:refs/heads/${WORKSPACE_BRANCH}`]);
1210
- if (push.exitCode === 0) {
1211
- const publishedHead = revParse(input, "HEAD") ?? candidateHead;
1212
- if (publishedHead) assertPublishedCanonicalCheckoutTree(input, publishedHead, sampledCanonicalCheckoutTreeHash);
1213
- mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots);
1214
- return {
1215
- ...observed,
1216
- outcome: "published",
1217
- candidateHead: candidateHead ?? void 0,
1218
- publishedHead: publishedHead ?? void 0,
1219
- rebaseCount,
1220
- gitStatus: gitStatus(input)
1221
- };
1222
- }
1223
- if (!isNonFastForward(push)) {
1224
- return {
1225
- ...observed,
1226
- outcome: "failed",
1227
- candidateHead: candidateHead ?? void 0,
1228
- rebaseCount,
1229
- error: push.stderr || push.stdout || "Workspace push failed",
1230
- gitStatus: gitStatus(input)
1231
- };
1232
- }
1233
- runGit(input, input.shadowRoot, ["fetch", "origin", "--prune"], "fetch workspace after push race");
1256
+ const expectedHead = revParse(input, `origin/${WORKSPACE_BRANCH}`);
1257
+ if (candidateHead && expectedHead && !tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", expectedHead, candidateHead])) {
1234
1258
  const rebase = runGitResult(input, input.shadowRoot, ["rebase", `origin/${WORKSPACE_BRANCH}`]);
1235
1259
  rebaseCount += 1;
1236
1260
  if (rebase.exitCode !== 0) {
1237
1261
  const conflicted = runGitResult(input, input.shadowRoot, ["diff", "--name-only", "--diff-filter=U", "-z"]);
1238
- const discardedPaths = reportedPaths([.../* @__PURE__ */ new Set([...paths, ...conflicted.stdout.split("\0").filter(Boolean)])].sort());
1239
- const publishedHead = resetShadowToRemote(input, mirroredGitlinkProjection.opaqueRoots);
1262
+ const conflictPaths = reportedPaths([.../* @__PURE__ */ new Set([...paths, ...conflicted.stdout.split("\0").filter(Boolean)])].sort());
1263
+ runGit(input, input.shadowRoot, ["rebase", "--abort"], "preserve workspace candidate after rebase conflict");
1240
1264
  return {
1241
1265
  ...observed,
1242
- outcome: "conflict_reset",
1266
+ outcome: "failed",
1267
+ expectedHead,
1243
1268
  candidateHead: candidateHead ?? void 0,
1244
- publishedHead: publishedHead ?? void 0,
1245
1269
  rebaseCount,
1246
- discardedPaths,
1247
- localChangesDiscarded: true,
1270
+ affectedPaths: conflictPaths,
1271
+ error: `Workspace candidate conflicts with canonical head ${expectedHead}; local changes were preserved for reconciliation.`,
1248
1272
  gitStatus: gitStatus(input)
1249
1273
  };
1250
1274
  }
1275
+ candidateHead = revParse(input, "HEAD");
1251
1276
  assertPublishedCanonicalCheckoutTree(input, "HEAD", sampledCanonicalCheckoutTreeHash);
1252
1277
  assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, "HEAD");
1253
1278
  }
1279
+ if (!candidateHead) {
1280
+ return {
1281
+ ...observed,
1282
+ outcome: "failed",
1283
+ rebaseCount,
1284
+ error: "Workspace candidate does not have a commit",
1285
+ gitStatus: gitStatus(input)
1286
+ };
1287
+ }
1288
+ const upload = uploadWorkspaceCandidate(input, candidateHead);
1289
+ if (upload.exitCode !== 0) {
1290
+ return {
1291
+ ...observed,
1292
+ outcome: "failed",
1293
+ candidateHead,
1294
+ quarantineRef: input.quarantineRef,
1295
+ rebaseCount,
1296
+ error: upload.stderr || upload.stdout || "Workspace candidate upload failed",
1297
+ gitStatus: gitStatus(input)
1298
+ };
1299
+ }
1300
+ assertPublishedCanonicalCheckoutTree(input, candidateHead, sampledCanonicalCheckoutTreeHash);
1301
+ mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots);
1254
1302
  return {
1255
1303
  ...observed,
1256
- outcome: "failed",
1257
- candidateHead: candidateHead ?? void 0,
1304
+ outcome: "candidate_ready",
1305
+ expectedHead,
1306
+ candidateHead,
1307
+ quarantineRef: input.quarantineRef,
1258
1308
  rebaseCount,
1259
- error: "Workspace push did not converge after three attempts",
1260
1309
  gitStatus: gitStatus(input)
1261
1310
  };
1262
1311
  } catch (error) {
@@ -1299,9 +1348,10 @@ function calculateWorkspaceDiffFingerprint(rawInput) {
1299
1348
  return createHash("sha256").update(JSON.stringify({ stagedTree, localHead, remoteHead })).digest("hex");
1300
1349
  }
1301
1350
  class WorkspaceSyncSingleFlight {
1351
+ mutationGate = new WorkspaceMutationGate();
1302
1352
  queue = Promise.resolve();
1303
1353
  enqueue(task) {
1304
- const queued = this.queue.then(task, task);
1354
+ const queued = this.mutationGate.runSync(task);
1305
1355
  this.queue = queued.then(
1306
1356
  () => void 0,
1307
1357
  () => void 0
@@ -1320,6 +1370,12 @@ class WorkspaceSyncSingleFlight {
1320
1370
  fingerprintPrepared(prepare) {
1321
1371
  return this.enqueue(() => calculateWorkspaceDiffFingerprint(prepare()));
1322
1372
  }
1373
+ runMutation(operation) {
1374
+ return this.mutationGate.runMutation(operation);
1375
+ }
1376
+ acquireMutation() {
1377
+ return this.mutationGate.acquireMutation();
1378
+ }
1323
1379
  afterCurrent() {
1324
1380
  return this.queue;
1325
1381
  }
@@ -1327,10 +1383,12 @@ class WorkspaceSyncSingleFlight {
1327
1383
  export {
1328
1384
  MAX_WORKSPACE_SYNC_DIFF_BYTES,
1329
1385
  WORKSPACE_BRANCH,
1386
+ WORKSPACE_INTENT_REF_PREFIX,
1330
1387
  WORKSPACE_NATIVE_PLANS_RELATIVE_PATH,
1331
1388
  WORKSPACE_PERIODIC_SCAN_INTERVAL_MS,
1332
1389
  WorkspaceSyncSingleFlight,
1333
1390
  assertCanonicalCheckoutIndexMatchesWorktree,
1391
+ assertWorkspaceQuarantineRef,
1334
1392
  calculateWorkspaceDiffFingerprint,
1335
1393
  encodeWorkspaceBranch,
1336
1394
  mirrorShadowWorkspaceToVisible,
@@ -1,3 +1,10 @@
1
1
  export declare const WORKER_HEARTBEAT_INTERVAL_MS = 15000;
2
2
  export declare const WORKER_HEARTBEAT_TIMEOUT_MS = 45000;
3
3
  export declare function hasWorkerHeartbeatTimedOut(lastHeartbeatAt: number | null, now?: number, timeoutMs?: number): boolean;
4
+ export type WorkerHeartbeatBusyGrace = {
5
+ heartbeatAt: number;
6
+ expiresAt: number;
7
+ };
8
+ /** Grant one bounded grace period for one actual server-heartbeat generation. */
9
+ export declare function grantWorkerHeartbeatBusyGrace(lastHeartbeatAt: number | null, currentGrace: WorkerHeartbeatBusyGrace | null, now?: number, timeoutMs?: number, graceMs?: number): WorkerHeartbeatBusyGrace | null;
10
+ export declare function hasWorkerHeartbeatTimedOutWithBusyGrace(lastHeartbeatAt: number | null, busyGrace: WorkerHeartbeatBusyGrace | null, now?: number, timeoutMs?: number): boolean;
@@ -0,0 +1,8 @@
1
+ export type WorkspaceIncidentUpdate = {
2
+ incidentId: string | null;
3
+ status: "remediating" | "waiting_for_worker" | "resolved" | "confirmed" | "reset" | null;
4
+ };
5
+ export declare function applyWorkspaceIncidentUpdate(currentIncidentId: string | null, update: WorkspaceIncidentUpdate): string | null;
6
+ export declare function releasePendingWorkspaceHead(previousIncidentId: string | null, currentIncidentId: string | null, terminalIncidentId?: string | null): {
7
+ syncHead: string | null;
8
+ };
@@ -0,0 +1,19 @@
1
+ export type WorkspaceMutationLease = () => void;
2
+ /**
3
+ * Coordinates the visible workspace with its disposable sync shadow.
4
+ *
5
+ * Ordinary worktree activity may overlap other ordinary activity, but a sync
6
+ * gets exclusive access. Once a sync is queued, later mutations wait behind it
7
+ * so a busy shell cannot starve canonical publication indefinitely.
8
+ */
9
+ export declare class WorkspaceMutationGate {
10
+ private activeMutations;
11
+ private syncActive;
12
+ private readonly waiters;
13
+ acquireMutation(): Promise<WorkspaceMutationLease>;
14
+ runMutation<T>(operation: () => Promise<T> | T): Promise<T>;
15
+ runSync<T>(operation: () => Promise<T> | T): Promise<T>;
16
+ private drain;
17
+ private releaseMutation;
18
+ private releaseSync;
19
+ }
@@ -1,5 +1,7 @@
1
1
  import { type CheckoutPathSegments } from "./managed-paths";
2
+ import { type WorkspaceMutationLease } from "./workspace-mutation-gate";
2
3
  export declare const WORKSPACE_BRANCH = "main";
4
+ export declare const WORKSPACE_INTENT_REF_PREFIX = "refs/r5d/workspace-intents/";
3
5
  export declare const MAX_WORKSPACE_SYNC_DIFF_BYTES: number;
4
6
  export declare const WORKSPACE_PERIODIC_SCAN_INTERVAL_MS = 5000;
5
7
  export type WorkspaceProjectManifestEntry = {
@@ -30,7 +32,7 @@ export type WorkspaceSyncTrigger = {
30
32
  canonicalCheckoutOnly?: boolean;
31
33
  detail?: string;
32
34
  };
33
- export type WorkspaceSyncOutcome = "no_change" | "published" | "updated" | "conflict_reset" | "large_diff_blocked" | "reset" | "failed";
35
+ export type WorkspaceSyncOutcome = "no_change" | "candidate_ready" | "published" | "updated" | "conflict_reset" | "large_diff_blocked" | "reset" | "failed";
34
36
  export type WorkspaceSyncResult = {
35
37
  type: "workspace_sync";
36
38
  attemptId: string;
@@ -38,7 +40,9 @@ export type WorkspaceSyncResult = {
38
40
  trigger: WorkspaceSyncTrigger;
39
41
  outcome: WorkspaceSyncOutcome;
40
42
  startingHead: string | null;
43
+ expectedHead?: string | null;
41
44
  candidateHead?: string;
45
+ quarantineRef?: string;
42
46
  publishedHead?: string;
43
47
  rebaseCount: number;
44
48
  diffSizeBytes: number;
@@ -60,6 +64,11 @@ export type WorkspaceSyncInput = {
60
64
  shadowRoot: string;
61
65
  projects: WorkspaceProjectManifestEntry[];
62
66
  trigger: WorkspaceSyncTrigger;
67
+ /**
68
+ * Server-issued, attempt-scoped destination for candidate object uploads.
69
+ * Workers never publish the canonical workspace branch directly.
70
+ */
71
+ quarantineRef?: string;
63
72
  confirmedLargeDiff?: boolean;
64
73
  confirmationReason?: string;
65
74
  resetToCanonical?: boolean;
@@ -91,15 +100,19 @@ type GitlinkProjection = {
91
100
  export declare function assertCanonicalCheckoutIndexMatchesWorktree(checkoutPath: string): void;
92
101
  export declare function mirrorVisibleWorkspaceToShadow(input: WorkspaceSyncInput, excludedCheckouts?: ReadonlySet<string>): GitlinkProjection;
93
102
  export declare function mirrorShadowWorkspaceToVisible(input: WorkspaceSyncInput, opaqueWorkspaceRoots?: readonly string[]): void;
103
+ export declare function assertWorkspaceQuarantineRef(quarantineRef: string): void;
94
104
  export declare function synchronizeWorkspace(rawInput: WorkspaceSyncInput): Promise<WorkspaceSyncResult>;
95
105
  export declare function calculateWorkspaceDiffFingerprint(rawInput: WorkspaceSyncInput): string;
96
106
  export declare class WorkspaceSyncSingleFlight {
107
+ private readonly mutationGate;
97
108
  private queue;
98
109
  private enqueue;
99
110
  run(input: WorkspaceSyncInput): Promise<WorkspaceSyncResult>;
100
111
  runPrepared(prepare: () => WorkspaceSyncInput): Promise<WorkspaceSyncResult>;
101
112
  fingerprint(input: WorkspaceSyncInput): Promise<string>;
102
113
  fingerprintPrepared(prepare: () => WorkspaceSyncInput): Promise<string>;
114
+ runMutation<T>(operation: () => Promise<T> | T): Promise<T>;
115
+ acquireMutation(): Promise<WorkspaceMutationLease>;
103
116
  afterCurrent(): Promise<void>;
104
117
  }
105
118
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.51",
3
+ "version": "0.0.54",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",