refactor: add timeout constants for agent execution tiers
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

Add TimeoutAgentExecution (22m) to handlers for synchronous SDLC
execution, and TimeoutAgent{Default,Medium,Heavy} (12/22/47m) to
workers for tiered agent task execution. Aligns with SDLC action
complexity tiers and prevents inline duration literals.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
jordan 2026-02-11 10:48:24 -07:00
parent 542bc722ab
commit 84af398d85
18 changed files with 206 additions and 34 deletions

View File

@ -67,7 +67,7 @@ func main() {
Logger: log, Logger: log,
})) }))
r.Use(middleware.Recoverer) r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(10 * time.Minute)) r.Use(middleware.Timeout(50 * time.Minute))
// Mount server routes // Mount server routes
server.Mount(r) server.Mount(r)
@ -78,7 +78,7 @@ func main() {
Addr: addr, Addr: addr,
Handler: r, Handler: r,
ReadTimeout: 30 * time.Second, ReadTimeout: 30 * time.Second,
WriteTimeout: 15 * time.Minute, // Long timeout for streaming responses WriteTimeout: 55 * time.Minute, // Long timeout for streaming responses
IdleTimeout: 60 * time.Second, IdleTimeout: 60 * time.Second,
} }

View File

@ -381,6 +381,8 @@ func main() {
agentRegistry, agentRegistry,
gitCommitter, gitCommitter,
projectRepo, projectRepo,
buildService,
database.DB,
) )
// Create app // Create app
@ -700,7 +702,7 @@ func main() {
workQueueRepo, workQueueRepo,
workerRegistryRepo, workerRegistryRepo,
&worker.QueueMaintenanceConfig{ &worker.QueueMaintenanceConfig{
StaleTaskTimeout: 30 * time.Minute, StaleTaskTimeout: 60 * time.Minute,
StaleWorkerTimeout: 2 * time.Minute, StaleWorkerTimeout: 2 * time.Minute,
CleanupAge: 7 * 24 * time.Hour, CleanupAge: 7 * 24 * time.Hour,
MaintenancePeriod: 1 * time.Minute, MaintenancePeriod: 1 * time.Minute,

View File

@ -34,7 +34,7 @@ type ClientConfig struct {
// NewClient creates a new claudebox client. // NewClient creates a new claudebox client.
func NewClient(cfg ClientConfig) *Client { func NewClient(cfg ClientConfig) *Client {
if cfg.Timeout == 0 { if cfg.Timeout == 0 {
cfg.Timeout = 10 * time.Minute cfg.Timeout = 50 * time.Minute // Safety net; per-request context cancellation provides real timeout
} }
return &Client{ return &Client{
baseURL: strings.TrimSuffix(cfg.BaseURL, "/"), baseURL: strings.TrimSuffix(cfg.BaseURL, "/"),

View File

@ -18,8 +18,8 @@ func TestNewClient_DefaultTimeout(t *testing.T) {
BaseURL: "http://localhost:8080", BaseURL: "http://localhost:8080",
}) })
if client.httpClient.Timeout != 10*time.Minute { if client.httpClient.Timeout != 50*time.Minute {
t.Errorf("expected default timeout 10m, got %v", client.httpClient.Timeout) t.Errorf("expected default timeout 50m, got %v", client.httpClient.Timeout)
} }
} }

View File

@ -32,7 +32,7 @@ type WorkerSDLCExecutorConfig struct {
// DB for fetching project git clone URLs. // DB for fetching project git clone URLs.
DB *sql.DB DB *sql.DB
// Timeout is the maximum wait time for task completion (default: 2 minutes). // Timeout is the maximum wait time for task completion (default: 10 minutes).
Timeout time.Duration Timeout time.Duration
Logger *slog.Logger Logger *slog.Logger
@ -42,7 +42,7 @@ type WorkerSDLCExecutorConfig struct {
func NewWorkerSDLCExecutor(cfg WorkerSDLCExecutorConfig) *WorkerSDLCExecutor { func NewWorkerSDLCExecutor(cfg WorkerSDLCExecutorConfig) *WorkerSDLCExecutor {
timeout := cfg.Timeout timeout := cfg.Timeout
if timeout == 0 { if timeout == 0 {
timeout = 2 * time.Minute timeout = 10 * time.Minute
} }
logger := cfg.Logger logger := cfg.Logger
if logger == nil { if logger == nil {

View File

@ -34,6 +34,10 @@ type BuildSpec struct {
// GitCloneURL is the HTTPS URL for cloning the project repository. // GitCloneURL is the HTTPS URL for cloning the project repository.
// Required for builds that use AutoCommit/AutoPush on shared worker pods. // Required for builds that use AutoCommit/AutoPush on shared worker pods.
GitCloneURL string `json:"git_clone_url,omitempty"` GitCloneURL string `json:"git_clone_url,omitempty"`
// TimeoutSeconds overrides the default agent execution timeout.
// 0 means use the default (10 minutes). Valid range: 60-5400 (1m to 90m).
TimeoutSeconds int `json:"timeout_seconds,omitempty"`
} }
// Validate checks that the BuildSpec has required fields. // Validate checks that the BuildSpec has required fields.
@ -41,6 +45,9 @@ func (s *BuildSpec) Validate() error {
if s.Prompt == "" { if s.Prompt == "" {
return ErrPromptRequired return ErrPromptRequired
} }
if s.TimeoutSeconds != 0 && (s.TimeoutSeconds < 60 || s.TimeoutSeconds > 5400) {
return fmt.Errorf("timeout_seconds must be between 60 and 5400 (got %d)", s.TimeoutSeconds)
}
if s.CallbackURL != "" { if s.CallbackURL != "" {
if err := ValidateCallbackURL(s.CallbackURL); err != nil { if err := ValidateCallbackURL(s.CallbackURL); err != nil {
return err return err

View File

@ -43,13 +43,14 @@ func (h *BuildsHandler) Mount(r api.Router) {
// StartBuildRequest is the request body for POST /projects/{id}/builds. // StartBuildRequest is the request body for POST /projects/{id}/builds.
type StartBuildRequest struct { type StartBuildRequest struct {
Prompt string `json:"prompt"` Prompt string `json:"prompt"`
Template string `json:"template,omitempty"` Template string `json:"template,omitempty"`
Variables map[string]string `json:"variables,omitempty"` Variables map[string]string `json:"variables,omitempty"`
AutoCommit bool `json:"auto_commit"` AutoCommit bool `json:"auto_commit"`
AutoPush bool `json:"auto_push"` AutoPush bool `json:"auto_push"`
CallbackURL string `json:"callback_url,omitempty"` CallbackURL string `json:"callback_url,omitempty"`
GitCloneURL string `json:"git_clone_url,omitempty"` // Required when auto_commit or auto_push is true GitCloneURL string `json:"git_clone_url,omitempty"` // Required when auto_commit or auto_push is true
TimeoutSeconds int `json:"timeout_seconds,omitempty"` // 0 = default (10m), valid range: 60-5400
} }
// StartBuildResponse is the response for POST /projects/{id}/builds. // StartBuildResponse is the response for POST /projects/{id}/builds.
@ -153,13 +154,14 @@ func (h *BuildsHandler) StartBuild(w http.ResponseWriter, r *http.Request) {
} }
spec := domain.BuildSpec{ spec := domain.BuildSpec{
Prompt: req.Prompt, Prompt: req.Prompt,
Template: req.Template, Template: req.Template,
Variables: req.Variables, Variables: req.Variables,
AutoCommit: req.AutoCommit, AutoCommit: req.AutoCommit,
AutoPush: req.AutoPush, AutoPush: req.AutoPush,
CallbackURL: req.CallbackURL, CallbackURL: req.CallbackURL,
GitCloneURL: req.GitCloneURL, GitCloneURL: req.GitCloneURL,
TimeoutSeconds: req.TimeoutSeconds,
} }
// Validate git_clone_url is provided when auto_commit or auto_push is enabled // Validate git_clone_url is provided when auto_commit or auto_push is enabled

View File

@ -50,7 +50,7 @@ func (h *SDLCOrchestratorHandler) Execute(w http.ResponseWriter, r *http.Request
return return
} }
ctx, cancel := context.WithTimeout(r.Context(), TimeoutLongRunning) ctx, cancel := context.WithTimeout(r.Context(), TimeoutAgentExecution)
defer cancel() defer cancel()
result, err := h.orchestrator.ExecuteAction(ctx, projectID, &req) result, err := h.orchestrator.ExecuteAction(ctx, projectID, &req)

View File

@ -24,6 +24,8 @@ func setupOrchestratorHandler(exec *testSDLCExecutor) (*SDLCOrchestratorHandler,
nil, // no agent registry for handler tests nil, // no agent registry for handler tests
nil, // no git committer for handler tests nil, // no git committer for handler tests
repo, repo,
nil, // no build service for handler tests
nil, // no db for handler tests
) )
handler := NewSDLCOrchestratorHandler(orchestrator) handler := NewSDLCOrchestratorHandler(orchestrator)

View File

@ -37,4 +37,9 @@ const (
// TimeoutLongRunning is for agent/command execution that streams output. // TimeoutLongRunning is for agent/command execution that streams output.
// 10 minutes. Claude Code commands can run extended operations. // 10 minutes. Claude Code commands can run extended operations.
TimeoutLongRunning = 10 * time.Minute TimeoutLongRunning = 10 * time.Minute
// TimeoutAgentExecution is for synchronous agent execution via /sdlc/execute.
// Accommodates medium-tier actions (20m) plus headroom for classify + post-processing.
// Heavy actions are dispatched async and return immediately.
TimeoutAgentExecution = 22 * time.Minute
) )

View File

@ -138,6 +138,40 @@ const (
ActionIdle ActionType = "IDLE" ActionIdle ActionType = "IDLE"
) )
// Action timeout tiers. Each action maps to one of these based on expected duration.
const (
// ActionTimeoutQuick is for fast actions: spec, design, tasks, qa-plan, branch, merge, archive.
ActionTimeoutQuick = 10 * time.Minute
// ActionTimeoutMedium is for actions requiring codebase analysis: review, audit.
ActionTimeoutMedium = 20 * time.Minute
// ActionTimeoutHeavy is for long-running actions: implement, fix-review, remediate, run-qa, fix-qa.
ActionTimeoutHeavy = 45 * time.Minute
)
// ActionTimeout returns the timeout duration for a given action type.
func ActionTimeout(action ActionType) time.Duration {
switch action {
case ActionReviewCode, ActionAuditCode:
return ActionTimeoutMedium
case ActionImplementTask, ActionFixReviewIssues, ActionRemediateAudit, ActionRunQA, ActionFixQAFailures:
return ActionTimeoutHeavy
default:
return ActionTimeoutQuick
}
}
// IsHeavyAction returns true for actions that should be dispatched asynchronously.
func IsHeavyAction(action ActionType) bool {
switch action {
case ActionImplementTask, ActionFixReviewIssues, ActionRemediateAudit, ActionRunQA, ActionFixQAFailures:
return true
default:
return false
}
}
// TaskStatus tracks the state of an implementation task. // TaskStatus tracks the state of an implementation task.
type TaskStatus string type TaskStatus string

View File

@ -55,6 +55,9 @@ func (s *BuildService) StartBuild(ctx context.Context, projectID string, spec do
if spec.GitCloneURL != "" { if spec.GitCloneURL != "" {
taskSpec["git_clone_url"] = spec.GitCloneURL taskSpec["git_clone_url"] = spec.GitCloneURL
} }
if spec.TimeoutSeconds > 0 {
taskSpec["timeout_seconds"] = spec.TimeoutSeconds
}
// Create work task // Create work task
task := &domain.WorkTask{ task := &domain.WorkTask{
@ -156,6 +159,9 @@ func (s *BuildService) StartBuildWithSDLCContext(ctx context.Context, projectID
if spec.GitCloneURL != "" { if spec.GitCloneURL != "" {
taskSpec["git_clone_url"] = spec.GitCloneURL taskSpec["git_clone_url"] = spec.GitCloneURL
} }
if spec.TimeoutSeconds > 0 {
taskSpec["timeout_seconds"] = spec.TimeoutSeconds
}
// Add SDLC context for callback routing // Add SDLC context for callback routing
if sdlcCtx != nil { if sdlcCtx != nil {
taskSpec["sdlc_context"] = sdlcCtx taskSpec["sdlc_context"] = sdlcCtx

View File

@ -2,8 +2,8 @@ package service
import ( import (
"context" "context"
"database/sql"
"fmt" "fmt"
"time"
"github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/domain"
"github.com/orchard9/rdev/internal/logging" "github.com/orchard9/rdev/internal/logging"
@ -32,6 +32,8 @@ type SDLCOrchestratorService struct {
agentRegistry port.CodeAgentRegistry agentRegistry port.CodeAgentRegistry
gitCommitter PodGitCommitter gitCommitter PodGitCommitter
projectRepo port.ProjectRepository projectRepo port.ProjectRepository
buildService *BuildService // For async dispatch of heavy actions
db *sql.DB // For git URL lookup
} }
// NewSDLCOrchestratorService creates a new orchestrator service. // NewSDLCOrchestratorService creates a new orchestrator service.
@ -40,12 +42,16 @@ func NewSDLCOrchestratorService(
agentRegistry port.CodeAgentRegistry, agentRegistry port.CodeAgentRegistry,
gitCommitter PodGitCommitter, gitCommitter PodGitCommitter,
projectRepo port.ProjectRepository, projectRepo port.ProjectRepository,
buildService *BuildService,
db *sql.DB,
) *SDLCOrchestratorService { ) *SDLCOrchestratorService {
return &SDLCOrchestratorService{ return &SDLCOrchestratorService{
sdlcService: sdlcService, sdlcService: sdlcService,
agentRegistry: agentRegistry, agentRegistry: agentRegistry,
gitCommitter: gitCommitter, gitCommitter: gitCommitter,
projectRepo: projectRepo, projectRepo: projectRepo,
buildService: buildService,
db: db,
} }
} }
@ -62,6 +68,8 @@ type ExecutionResult struct {
Output string `json:"output,omitempty"` Output string `json:"output,omitempty"`
Next *sdlc.Classification `json:"next,omitempty"` Next *sdlc.Classification `json:"next,omitempty"`
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
TaskID string `json:"task_id,omitempty"` // Set when dispatched async
Async bool `json:"async,omitempty"` // True when enqueued to work queue
} }
// ResolveRequest describes a blocker resolution. // ResolveRequest describes a blocker resolution.
@ -94,12 +102,14 @@ func (s *SDLCOrchestratorService) ExecuteAction(ctx context.Context, projectID s
Action: cl.Action, Action: cl.Action,
} }
switch cl.Action { switch {
case sdlc.ActionTransition: case cl.Action == sdlc.ActionTransition:
err = s.executeTransition(ctx, projectID, cl) err = s.executeTransition(ctx, projectID, cl)
case sdlc.ActionIdle, sdlc.ActionBlocked, sdlc.ActionAwaitApproval: case cl.Action == sdlc.ActionIdle || cl.Action == sdlc.ActionBlocked || cl.Action == sdlc.ActionAwaitApproval:
result.Output = cl.Message result.Output = cl.Message
result.Success = true result.Success = true
case sdlc.IsHeavyAction(cl.Action) && s.buildService != nil:
err = s.executeAgentActionAsync(ctx, projectID, cl, req, result)
default: default:
err = s.executeAgentAction(ctx, projectID, cl, req, result) err = s.executeAgentAction(ctx, projectID, cl, req, result)
} }
@ -160,7 +170,7 @@ func (s *SDLCOrchestratorService) executeAgentAction(ctx context.Context, projec
agentReq := &domain.AgentRequest{ agentReq := &domain.AgentRequest{
Prompt: prompt, Prompt: prompt,
ProjectID: project.ID, ProjectID: project.ID,
Timeout: 10 * time.Minute, Timeout: sdlc.ActionTimeout(cl.Action),
Metadata: map[string]string{ Metadata: map[string]string{
"pod_name": project.PodName, "pod_name": project.PodName,
"namespace": "rdev", "namespace": "rdev",
@ -191,6 +201,75 @@ func (s *SDLCOrchestratorService) executeAgentAction(ctx context.Context, projec
return nil return nil
} }
// executeAgentActionAsync dispatches a heavy SDLC action through the work queue.
// Returns immediately with task_id and async=true.
func (s *SDLCOrchestratorService) executeAgentActionAsync(ctx context.Context, projectID string, cl *sdlc.Classification, req *ExecuteRequest, result *ExecutionResult) error {
log := logging.FromContext(ctx).WithService("sdlc_orchestrator")
gitCloneURL, err := s.getProjectGitURL(ctx, projectID)
if err != nil {
return fmt.Errorf("resolve git URL for async dispatch: %w", err)
}
timeout := sdlc.ActionTimeout(cl.Action)
buildSpec := domain.BuildSpec{
Prompt: cl.NextCommand,
AutoCommit: true,
AutoPush: true,
GitCloneURL: gitCloneURL,
TimeoutSeconds: int(timeout.Seconds()),
}
sdlcCtx := map[string]any{
"feature": cl.Feature,
"action": string(cl.Action),
}
taskID, err := s.buildService.StartBuildWithSDLCContext(ctx, projectID, buildSpec, sdlcCtx)
if err != nil {
return fmt.Errorf("enqueue async action: %w", err)
}
log.Info("dispatched heavy action async",
logging.FieldProjectID, projectID,
"feature", cl.Feature,
"action", string(cl.Action),
"task_id", taskID,
"timeout_seconds", int(timeout.Seconds()),
)
result.TaskID = taskID
result.Async = true
result.Success = true
result.Output = fmt.Sprintf("Action %s dispatched asynchronously (task_id: %s)", cl.Action, taskID)
return nil
}
// getProjectGitURL retrieves the git clone URL for a project from the database.
func (s *SDLCOrchestratorService) getProjectGitURL(ctx context.Context, projectID string) (string, error) {
if s.db == nil {
return "", fmt.Errorf("database not configured")
}
var gitCloneHTTP sql.NullString
err := s.db.QueryRowContext(ctx,
`SELECT git_clone_http FROM projects WHERE id = $1`,
projectID,
).Scan(&gitCloneHTTP)
if err != nil {
if err == sql.ErrNoRows {
return "", domain.ErrProjectNotFound
}
return "", fmt.Errorf("query project: %w", err)
}
if !gitCloneHTTP.Valid || gitCloneHTTP.String == "" {
return "", fmt.Errorf("project %s has no git URL configured", projectID)
}
return gitCloneHTTP.String, nil
}
// ResolveBlocker unblocks a feature and re-classifies. // ResolveBlocker unblocks a feature and re-classifies.
func (s *SDLCOrchestratorService) ResolveBlocker(ctx context.Context, projectID string, req *ResolveRequest) (*ExecutionResult, error) { func (s *SDLCOrchestratorService) ResolveBlocker(ctx context.Context, projectID string, req *ResolveRequest) (*ExecutionResult, error) {
if err := s.sdlcService.UnblockFeature(ctx, projectID, req.Feature); err != nil { if err := s.sdlcService.UnblockFeature(ctx, projectID, req.Feature); err != nil {

View File

@ -70,7 +70,7 @@ func (a *mockCodeAgent) Execute(_ context.Context, _ *domain.AgentRequest, handl
func newTestOrchestrator(exec *mockSDLCExecutor, repo *mockProjectRepo, registry port.CodeAgentRegistry, committer PodGitCommitter) *SDLCOrchestratorService { func newTestOrchestrator(exec *mockSDLCExecutor, repo *mockProjectRepo, registry port.CodeAgentRegistry, committer PodGitCommitter) *SDLCOrchestratorService {
sdlcSvc := NewSDLCService(exec, repo) sdlcSvc := NewSDLCService(exec, repo)
return NewSDLCOrchestratorService(sdlcSvc, registry, committer, repo) return NewSDLCOrchestratorService(sdlcSvc, registry, committer, repo, nil, nil)
} }
func TestOrchestrator_ExecuteAction_Idle(t *testing.T) { func TestOrchestrator_ExecuteAction_Idle(t *testing.T) {

View File

@ -156,12 +156,18 @@ func (b *BuildExecutor) Execute(ctx context.Context, task *domain.WorkTask) *dom
} }
} }
// Derive agent timeout from spec or use default
agentTimeout := TimeoutWorkExecution
if timeoutSec, ok := task.Spec["timeout_seconds"].(float64); ok && timeoutSec > 0 {
agentTimeout = time.Duration(timeoutSec) * time.Second
}
// Build the agent request with pod metadata for Claude Code adapter // Build the agent request with pod metadata for Claude Code adapter
agentReq := &domain.AgentRequest{ agentReq := &domain.AgentRequest{
Prompt: spec.Prompt, Prompt: spec.Prompt,
ProjectID: domain.ProjectID(task.ProjectID), ProjectID: domain.ProjectID(task.ProjectID),
WorkingDir: workDir, WorkingDir: workDir,
Timeout: 10 * time.Minute, Timeout: agentTimeout,
Metadata: map[string]string{ Metadata: map[string]string{
"pod_name": podName, "pod_name": podName,
"namespace": b.namespace, "namespace": b.namespace,

View File

@ -125,11 +125,17 @@ func (e *HTTPBuildExecutor) Execute(ctx context.Context, task *domain.WorkTask)
var output strings.Builder var output strings.Builder
const maxOutputSize = 1 << 20 // 1MB const maxOutputSize = 1 << 20 // 1MB
// Derive timeout from spec or use default (10 minutes)
timeoutSec := 600
if ts, ok := task.Spec["timeout_seconds"].(float64); ok && ts > 0 {
timeoutSec = int(ts)
}
// Use streaming execution // Use streaming execution
execErr := e.client.ExecuteStream(ctx, &claudeboxclient.ExecuteRequest{ execErr := e.client.ExecuteStream(ctx, &claudeboxclient.ExecuteRequest{
Prompt: spec.Prompt, Prompt: spec.Prompt,
WorkingDir: e.workDir, WorkingDir: e.workDir,
Timeout: 600, // 10 minutes Timeout: timeoutSec,
}, func(evt claudeboxclient.StreamEvent) { }, func(evt claudeboxclient.StreamEvent) {
// Map event types // Map event types
eventType := BuildEventOutput eventType := BuildEventOutput

View File

@ -24,7 +24,20 @@ const (
// 30 seconds. These may involve multiple DB operations. // 30 seconds. These may involve multiple DB operations.
TimeoutMaintenance = 30 * time.Second TimeoutMaintenance = 30 * time.Second
// TimeoutWorkExecution is for executing work items (commands, builds, agent tasks). // TimeoutWorkExecution is the default timeout for executing work items
// (commands, builds, agent tasks). Used when no spec-level timeout is provided.
// 10 minutes. Long-running operations that stream output. // 10 minutes. Long-running operations that stream output.
TimeoutWorkExecution = 10 * time.Minute TimeoutWorkExecution = 10 * time.Minute
// TimeoutAgentDefault is for standard agent tasks (artifact generation).
// 12 minutes. Slightly above TimeoutWorkExecution to account for overhead.
TimeoutAgentDefault = 12 * time.Minute
// TimeoutAgentMedium is for agent tasks requiring codebase analysis (review, audit).
// 22 minutes. Matches medium SDLC action tier plus overhead.
TimeoutAgentMedium = 22 * time.Minute
// TimeoutAgentHeavy is for long-running agent tasks (implementation, fixes, QA).
// 47 minutes. Matches heavy SDLC action tier plus overhead.
TimeoutAgentHeavy = 47 * time.Minute
) )

View File

@ -73,7 +73,7 @@ func DefaultWorkExecutorConfig() *WorkExecutorConfig {
Capabilities: []string{"build"}, Capabilities: []string{"build"},
PollPeriod: 5 * time.Second, PollPeriod: 5 * time.Second,
HeartbeatPeriod: 30 * time.Second, HeartbeatPeriod: 30 * time.Second,
TaskTimeout: 15 * time.Minute, TaskTimeout: 50 * time.Minute,
} }
} }
@ -244,7 +244,7 @@ func (e *WorkExecutor) tryClaimAndExecute() {
"type", task.Type, "type", task.Type,
) )
taskCtx, taskCancel := context.WithTimeout(e.ctx, e.taskTimeout) taskCtx, taskCancel := context.WithTimeout(e.ctx, e.taskTimeoutFor(task))
defer taskCancel() defer taskCancel()
result := e.executeTask(taskCtx, task) result := e.executeTask(taskCtx, task)
@ -278,6 +278,16 @@ func (e *WorkExecutor) tryClaimAndExecute() {
} }
} }
// taskTimeoutFor returns the timeout for a specific task, derived from its spec
// if available, falling back to the configured default.
func (e *WorkExecutor) taskTimeoutFor(task *domain.WorkTask) time.Duration {
if timeoutSec, ok := task.Spec["timeout_seconds"].(float64); ok && timeoutSec > 0 {
// Add 2 minutes headroom for git clone/push around the agent execution
return time.Duration(timeoutSec)*time.Second + 2*time.Minute
}
return e.taskTimeout
}
// executeTask routes a task to the appropriate handler based on its type. // executeTask routes a task to the appropriate handler based on its type.
func (e *WorkExecutor) executeTask(ctx context.Context, task *domain.WorkTask) *domain.BuildResult { func (e *WorkExecutor) executeTask(ctx context.Context, task *domain.WorkTask) *domain.BuildResult {
switch task.Type { switch task.Type {