现象
浏览器侧 attachClientBake 的等待窗口是硬编码的:
const RETRY_DELAYS_MS = [800, 1600, 3200, 6000, 8000]
五次退避合计 19.6 秒,之后 return false。而服务端的 deadline 是:
DEADLINE_S = float(os.getenv("WINDUP_RENDER3D_CLIENT_BAKE_DEADLINE_S", "900"))
900 秒。两个数字差 46 倍。
后果
action worker 挂出 spec 之前如果队列堵了超过 20 秒,浏览器已经放弃了,而任务还要在 RUNNING 上傻等到 900 秒才判 浏览器出帧超时。用户看到的是一次没有任何解释的失败,中间那 880 秒什么都没发生。
放弃还是静默的:
void runClientBake(taskId).catch((cause) => onAsyncError(asError(cause)))
返回 false 不走 catch,所以「没挂上」这件事一个字都不会报。
缓解与不足
重开工作流页会走 resume() → watchGeneration → 重新 runClientBake,所以能接回来——但要靠用户碰巧刷新,且仍在 900 秒之内。
建议
窗口对齐服务端 deadline(由 spec 里的 deadline_at 推,而不是各写各的常量),并在放弃时报一次可见错误。
复现
2026-08-28 task 761 验证 ④ 全链路时,用探针把窗口放长才等到 spec;按 19.6 秒的窗口第一次是等不到的。
现象
浏览器侧
attachClientBake的等待窗口是硬编码的:五次退避合计 19.6 秒,之后
return false。而服务端的 deadline 是:900 秒。两个数字差 46 倍。
后果
action worker 挂出 spec 之前如果队列堵了超过 20 秒,浏览器已经放弃了,而任务还要在 RUNNING 上傻等到 900 秒才判
浏览器出帧超时。用户看到的是一次没有任何解释的失败,中间那 880 秒什么都没发生。放弃还是静默的:
返回
false不走catch,所以「没挂上」这件事一个字都不会报。缓解与不足
重开工作流页会走
resume()→watchGeneration→ 重新runClientBake,所以能接回来——但要靠用户碰巧刷新,且仍在 900 秒之内。建议
窗口对齐服务端 deadline(由 spec 里的
deadline_at推,而不是各写各的常量),并在放弃时报一次可见错误。复现
2026-08-28 task 761 验证 ④ 全链路时,用探针把窗口放长才等到 spec;按 19.6 秒的窗口第一次是等不到的。