diff --git a/packages/cli/src/shared/orchestrate.ts b/packages/cli/src/shared/orchestrate.ts index 32f1c9ad..717986e6 100644 --- a/packages/cli/src/shared/orchestrate.ts +++ b/packages/cli/src/shared/orchestrate.ts @@ -251,7 +251,11 @@ export async function runOrchestration( installed = await uploadAndExtractTarball(cloud.runner, localTarball.localPath); localTarball.cleanup(); } - if (!installed && useTarball && !agent.skipTarball) { + // Only try remote tarball download when we didn't already have a local tarball. + // If the local tarball was available but upload/extract failed, the remote + // download would face the same extraction issues — skip it to save ~150s + // and fall through to live install immediately. + if (!installed && !localTarball && useTarball && !agent.skipTarball) { const tarball = options?.tryTarball ?? tryTarballInstall; installed = await tarball(cloud.runner, agentName); } diff --git a/packages/cli/src/sprite/sprite.ts b/packages/cli/src/sprite/sprite.ts index 907e7a51..04707962 100644 --- a/packages/cli/src/sprite/sprite.ts +++ b/packages/cli/src/sprite/sprite.ts @@ -576,6 +576,12 @@ export async function uploadFileSprite(localPath: string, remotePath: string): P // Compute the parent directory in TypeScript to avoid shell interpolation const parentDir = posixDirname(normalizedRemote); + // 180s timeout — prevents indefinite hangs during tarball uploads in fast mode. + // Without this, large file uploads (e.g. 300MB openclaw tarball) or stalled + // Sprite connections can block the entire provisioning pipeline past the + // E2E provision timeout (720s), causing agent binary not-found failures. + const UPLOAD_TIMEOUT_MS = 180_000; + await spriteRetry("sprite upload", async () => { // Upload the file to the temp path, then mkdir + mv using array args // to avoid shell string interpolation (command injection risk). @@ -604,8 +610,13 @@ export async function uploadFileSprite(localPath: string, remotePath: string): P ); // Drain stderr before awaiting exit to prevent pipe buffer deadlock const stderrText = new Response(proc.stderr).text(); - const exitCode = await proc.exited; - if (exitCode !== 0) { + const uploadTimer = setTimeout(() => killWithTimeout(proc), UPLOAD_TIMEOUT_MS); + const uploadResult = await asyncTryCatch(() => proc.exited); + clearTimeout(uploadTimer); + if (!uploadResult.ok) { + throw new Error(`upload timed out for ${remotePath}`); + } + if (uploadResult.data !== 0) { throw new Error(`upload mkdir failed for ${remotePath}: ${await stderrText}`); } @@ -632,8 +643,13 @@ export async function uploadFileSprite(localPath: string, remotePath: string): P }, ); const mvStderrText = new Response(mvProc.stderr).text(); - const mvExitCode = await mvProc.exited; - if (mvExitCode !== 0) { + const mvTimer = setTimeout(() => killWithTimeout(mvProc), 60_000); + const mvResult = await asyncTryCatch(() => mvProc.exited); + clearTimeout(mvTimer); + if (!mvResult.ok) { + throw new Error(`upload mv timed out for ${remotePath}`); + } + if (mvResult.data !== 0) { throw new Error(`upload mv failed for ${remotePath}: ${await mvStderrText}`); } });