fix: retry transient ssh download empty blocks

This commit is contained in:
Codex
2026-06-02 08:58:01 +00:00
parent 7de9782dc2
commit b6099492bc
2 changed files with 54 additions and 12 deletions
+37 -11
View File
@@ -53,6 +53,7 @@ class SshFileTransferError extends Error {
const fileTransferReadBlockBytes = 45_000;
const fileTransferWriteB64ArgvLimit = 48_000;
const fileTransferWriteB64ChunkChars = 12_000;
const fileTransferReadBlockMaxAttempts = 3;
export function isSshFileTransferOperation(args: string[]): boolean {
const subcommand = args[0] ?? "";
@@ -204,18 +205,8 @@ async function readRemoteFileVerified(
let actualBytes = 0;
let chunkCount = 0;
for (let blockIndex = 0; actualBytes < remote.bytes; blockIndex += 1) {
const read = await checkedFileTransfer(invocation, executor, builders, "read-b64-block", [remotePath, String(blockIndex), String(readBlockBytes)]);
const encoded = read.stdout.replace(/\s+/gu, "");
const encoded = await readRemoteBase64BlockWithRetry(invocation, executor, builders, remotePath, readBlockBytes, blockIndex, remote.bytes, actualBytes);
const chunk = encoded.length === 0 ? Buffer.alloc(0) : Buffer.from(encoded, "base64");
if (chunk.length === 0) {
throw new SshFileTransferError("remote download returned an empty block before EOF", {
route: invocation.route.raw,
remotePath,
blockIndex,
expectedBytes: remote.bytes,
actualBytes,
});
}
chunks.push(chunk);
actualBytes += chunk.length;
chunkCount += 1;
@@ -226,6 +217,41 @@ async function readRemoteFileVerified(
return { remote, content, chunks: chunkCount };
}
async function readRemoteBase64BlockWithRetry(
invocation: ParsedSshInvocation,
executor: SshRemoteCommandExecutor,
builders: SshFileTransferCommandBuilders,
remotePath: string,
readBlockBytes: number,
blockIndex: number,
expectedBytes: number,
actualBytes: number,
): Promise<string> {
const attemptErrors: Array<Record<string, unknown>> = [];
for (let attempt = 1; attempt <= fileTransferReadBlockMaxAttempts; attempt += 1) {
try {
const read = await checkedFileTransfer(invocation, executor, builders, "read-b64-block", [remotePath, String(blockIndex), String(readBlockBytes)]);
const encoded = read.stdout.replace(/\s+/gu, "");
if (encoded.length > 0) return encoded;
attemptErrors.push({ attempt, exitCode: read.exitCode, stdoutBytes: read.stdout.length, stderrTail: read.stderr.slice(-500) });
} catch (error) {
attemptErrors.push({
attempt,
error: error instanceof Error ? error.message : String(error),
});
}
}
throw new SshFileTransferError("remote download returned an empty block before EOF after retries", {
route: invocation.route.raw,
remotePath,
blockIndex,
attempts: fileTransferReadBlockMaxAttempts,
expectedBytes,
actualBytes,
attemptErrors,
});
}
async function statRemoteFile(
invocation: ParsedSshInvocation,
executor: SshRemoteCommandExecutor,