From bffcd3e7116ac567d13f44dbdd9d353eb6000345 Mon Sep 17 00:00:00 2001 From: hiroTamada Date: Mon, 30 Mar 2026 10:30:33 -0400 Subject: [PATCH] Add heartbeats to StreamBuildEvents wait-for-log-file loop When a build is queued, StreamBuildEvents waits for the log file to appear before streaming logs. During this wait, no events are sent on the SSE connection. Infrastructure proxies (e.g. Railway) terminate idle SSE connections after ~60 seconds, causing the Kernel API to see a "build timed out" error even though the build hasn't started yet. Add a 15-second heartbeat ticker to keep the SSE connection alive while waiting for the log file. The existing 30-second heartbeat in the main streaming loop already handles the active-build case. Made-with: Cursor --- lib/builds/manager.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/builds/manager.go b/lib/builds/manager.go index e15ad316..97ec0e20 100644 --- a/lib/builds/manager.go +++ b/lib/builds/manager.go @@ -1127,7 +1127,11 @@ func (m *manager) StreamBuildEvents(ctx context.Context, id string, follow bool) if !follow || isComplete { return } - // Wait for log file to appear, or for build to complete + // Wait for log file to appear, or for build to complete. + // Send heartbeats while waiting so SSE connections survive + // infrastructure idle timeouts (e.g. load balancer 60s timeout). + waitHeartbeat := time.NewTicker(15 * time.Second) + defer waitHeartbeat.Stop() for { select { case <-ctx.Done(): @@ -1138,11 +1142,16 @@ func (m *manager) StreamBuildEvents(ctx context.Context, id string, follow bool) case <-ctx.Done(): return } - // Check if build completed if event.Status == StatusReady || event.Status == StatusFailed || event.Status == StatusCancelled { return } - // Non-terminal status event - keep waiting for log file + continue + case <-waitHeartbeat.C: + select { + case out <- BuildEvent{Type: EventTypeHeartbeat, Timestamp: time.Now()}: + case <-ctx.Done(): + return + } continue case <-time.After(500 * time.Millisecond): if _, err := os.Stat(logPath); err == nil { @@ -1152,6 +1161,7 @@ func (m *manager) StreamBuildEvents(ctx context.Context, id string, follow bool) } break } + waitHeartbeat.Stop() } // Build tail command args