patchrelay 0.41.5 → 0.41.7

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "service": "patchrelay",
3
- "version": "0.41.5",
4
- "commit": "8cba4e264b7e",
5
- "builtAt": "2026-04-13T11:03:19.900Z"
3
+ "version": "0.41.7",
4
+ "commit": "604350d6627f",
5
+ "builtAt": "2026-04-13T18:24:22.746Z"
6
6
  }
@@ -101,7 +101,7 @@ function buildFacts(issue, selected) {
101
101
  facts.push({ text: "awaiting review", color: "yellow" });
102
102
  }
103
103
  if (issue.factoryState === "awaiting_queue") {
104
- facts.push({ text: "merge queue", color: "cyan" });
104
+ facts.push({ text: "downstream ready", color: "cyan" });
105
105
  }
106
106
  // Check status — compact
107
107
  const checksFact = prChecksFact(issue);
@@ -401,7 +401,7 @@ function buildFactSegments(issue, issueContext) {
401
401
  && (isAwaitingReviewState(issue.prReviewState) || (!issue.prReviewState && issue.factoryState === "pr_open")))
402
402
  facts.push([{ text: "awaiting review", color: "yellow" }]);
403
403
  if (issue.factoryState === "awaiting_queue")
404
- facts.push([{ text: "merge queue", color: "cyan" }]);
404
+ facts.push([{ text: "downstream ready", color: "cyan" }]);
405
405
  if (issue.waitingReason && issue.sessionState === "waiting_input")
406
406
  facts.push([{ text: issue.waitingReason, color: "yellow" }]);
407
407
  const checks = prChecksFact({
@@ -1,6 +1,17 @@
1
1
  import { existsSync, lstatSync, realpathSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { ensureDir, execCommand } from "./utils.js";
4
+ const FETCH_RETRY_DELAYS_MS = [0, 1_000];
5
+ const TRANSIENT_FETCH_ERROR_PATTERNS = [
6
+ /connection reset by peer/i,
7
+ /recv failure/i,
8
+ /tls handshake timeout/i,
9
+ /connection timed out/i,
10
+ /operation timed out/i,
11
+ /timed out after \d+ms/i,
12
+ /remote end hung up unexpectedly/i,
13
+ /unexpected disconnect while reading sideband packet/i,
14
+ ];
4
15
  export class WorktreeManager {
5
16
  config;
6
17
  constructor(config) {
@@ -84,9 +95,7 @@ export class WorktreeManager {
84
95
  // Fetch latest main so the branch forks from a clean, up-to-date base.
85
96
  // This prevents branch contamination when local HEAD has drifted.
86
97
  // freshenWorktree in run-orchestrator acts as a secondary safety net.
87
- const fetchResult = await execCommand(this.config.runner.gitBin, ["-C", repoPath, "fetch", "origin", "main"], {
88
- timeoutMs: 60_000,
89
- });
98
+ const fetchResult = await this.fetchWithTransientRetry(repoPath, "main");
90
99
  if (fetchResult.exitCode !== 0) {
91
100
  throw new Error(`Failed to fetch origin/main before creating issue worktree: ${fetchResult.stderr?.slice(0, 300) ?? "git fetch failed"}`);
92
101
  }
@@ -95,6 +104,34 @@ export class WorktreeManager {
95
104
  throw new Error(`Failed to create issue worktree at ${worktreePath}: ${addResult.stderr?.slice(0, 300) ?? "git worktree add failed"}`);
96
105
  }
97
106
  }
107
+ async fetchWithTransientRetry(repoPath, branchName) {
108
+ let lastResult;
109
+ let lastError;
110
+ for (let attempt = 0; attempt <= FETCH_RETRY_DELAYS_MS.length; attempt += 1) {
111
+ if (attempt > 0) {
112
+ await delay(FETCH_RETRY_DELAYS_MS[attempt - 1] ?? 0);
113
+ }
114
+ try {
115
+ const result = await execCommand(this.config.runner.gitBin, ["-C", repoPath, "fetch", "origin", branchName], {
116
+ timeoutMs: 60_000,
117
+ });
118
+ if (result.exitCode === 0 || !isTransientFetchFailure(result.stderr)) {
119
+ return result;
120
+ }
121
+ lastResult = result;
122
+ }
123
+ catch (error) {
124
+ if (!isTransientFetchFailure(error instanceof Error ? error.message : String(error))) {
125
+ throw error;
126
+ }
127
+ lastError = error instanceof Error ? error : new Error(String(error));
128
+ }
129
+ }
130
+ if (lastResult) {
131
+ return lastResult;
132
+ }
133
+ throw lastError ?? new Error(`Failed to fetch origin/${branchName}`);
134
+ }
98
135
  async assertTrustedExistingWorktree(repoPath, worktreeRoot, worktreePath, options) {
99
136
  const worktreeStats = lstatSync(worktreePath);
100
137
  if (worktreeStats.isSymbolicLink()) {
@@ -145,3 +182,15 @@ function isPathWithinRoot(rootPath, candidatePath) {
145
182
  const relative = path.relative(rootPath, candidatePath);
146
183
  return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
147
184
  }
185
+ function isTransientFetchFailure(message) {
186
+ if (!message) {
187
+ return false;
188
+ }
189
+ return TRANSIENT_FETCH_ERROR_PATTERNS.some((pattern) => pattern.test(message));
190
+ }
191
+ function delay(delayMs) {
192
+ if (delayMs <= 0) {
193
+ return Promise.resolve();
194
+ }
195
+ return new Promise((resolve) => setTimeout(resolve, delayMs));
196
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.41.5",
3
+ "version": "0.41.7",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {