Skip to content

Commit 473bbbf

Browse files
authored
🤖 fix: retry bundle creation on transient SSH failures (#1254)
## Problem The "pack-objects died" error occurs intermittently during `git bundle create` when SSH connections drop during transfer. ## Solution Wrap the entire `syncProjectToRemote` call with retry logic at the `initWorkspace` level rather than retrying just the bundle step. **Benefits:** - Single retry point (no extra helper method) - Catches failures in any sync step, not just bundle creation - Cleans up partial remote state before retry - Simpler and more maintainable Retries up to 3 times for transient errors: - `pack-objects died` - `Connection reset` - `Connection closed` - `Broken pipe` Uses linear backoff (1s, 2s) between attempts. --- _Generated with `mux` • Model: `anthropic:claude-opus-4-5` • Thinking: `high`_
1 parent cb30e65 commit 473bbbf

File tree

1 file changed

+41
-12
lines changed

1 file changed

+41
-12
lines changed

src/node/runtime/SSHRuntime.ts

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,6 @@ export class SSHRuntime implements Runtime {
640640
initLogger.logStep(`Creating git bundle...`);
641641

642642
await new Promise<void>((resolve, reject) => {
643-
// Check if aborted before spawning
644643
if (abortSignal?.aborted) {
645644
reject(new Error("Bundle creation aborted"));
646645
return;
@@ -933,18 +932,48 @@ export class SSHRuntime implements Runtime {
933932
const { projectPath, branchName, trunkBranch, workspacePath, initLogger, abortSignal } = params;
934933

935934
try {
936-
// 1. Sync project to remote (opportunistic rsync with scp fallback)
935+
// 1. Sync project to remote with retry for transient SSH failures
936+
// Errors like "pack-objects died" occur when SSH drops mid-transfer
937937
initLogger.logStep("Syncing project files to remote...");
938-
try {
939-
await this.syncProjectToRemote(projectPath, workspacePath, initLogger, abortSignal);
940-
} catch (error) {
941-
const errorMsg = getErrorMessage(error);
942-
initLogger.logStderr(`Failed to sync project: ${errorMsg}`);
943-
initLogger.logComplete(-1);
944-
return {
945-
success: false,
946-
error: `Failed to sync project: ${errorMsg}`,
947-
};
938+
const maxSyncAttempts = 3;
939+
for (let attempt = 1; attempt <= maxSyncAttempts; attempt++) {
940+
try {
941+
await this.syncProjectToRemote(projectPath, workspacePath, initLogger, abortSignal);
942+
break;
943+
} catch (error) {
944+
const errorMsg = getErrorMessage(error);
945+
const isRetryable =
946+
errorMsg.includes("pack-objects died") ||
947+
errorMsg.includes("Connection reset") ||
948+
errorMsg.includes("Connection closed") ||
949+
errorMsg.includes("Broken pipe");
950+
951+
if (!isRetryable || attempt === maxSyncAttempts) {
952+
initLogger.logStderr(`Failed to sync project: ${errorMsg}`);
953+
initLogger.logComplete(-1);
954+
return {
955+
success: false,
956+
error: `Failed to sync project: ${errorMsg}`,
957+
};
958+
}
959+
960+
// Clean up partial remote state before retry
961+
log.info(`Sync failed (attempt ${attempt}/${maxSyncAttempts}), will retry: ${errorMsg}`);
962+
try {
963+
const rmStream = await this.exec(`rm -rf ${shescape.quote(workspacePath)}`, {
964+
cwd: "~",
965+
timeout: 30,
966+
});
967+
await rmStream.exitCode;
968+
} catch {
969+
// Ignore cleanup errors
970+
}
971+
972+
initLogger.logStep(
973+
`Sync failed, retrying (attempt ${attempt + 1}/${maxSyncAttempts})...`
974+
);
975+
await new Promise((r) => setTimeout(r, attempt * 1000));
976+
}
948977
}
949978
initLogger.logStep("Files synced successfully");
950979

0 commit comments

Comments
 (0)