Skip to content

Commit a063316

Browse files
committed
feat: per-phase timeouts — each phase has its own tuned timeout
plan=8m, implement=40m, pr=3m, review=10m, merge=12m, communicate=10m. Previously all phases shared a single 30m timeout which was too long for fast phases (PR/plan) and too short for slow ones (implement with parallel tasks).
1 parent fd95979 commit a063316

4 files changed

Lines changed: 35 additions & 10 deletions

File tree

internal/evolution/git.go

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -252,12 +252,37 @@ func (e *Engine) runTests(ctx context.Context) (string, error) {
252252
return e.toolMap["run_tests"].Execute(ctx, nil)
253253
}
254254

255-
// defaultPhaseTimeout is the maximum duration for any evolution phase.
256-
const defaultPhaseTimeout = 30 * time.Minute
255+
// Per-phase timeouts tuned to each phase's actual workload.
256+
const (
257+
timeoutPlan = 8 * time.Minute // read + plan only, no code changes
258+
timeoutImplement = 40 * time.Minute // one timeout covers all parallel tasks
259+
timeoutPR = 3 * time.Minute // git + gh CLI only
260+
timeoutReview = 10 * time.Minute // diff read + agent review
261+
timeoutMerge = 12 * time.Minute // merge + CI poll (short, CI poll has its own 10m timer)
262+
timeoutCommunicate = 10 * time.Minute // journal + issue comments + learnings
263+
)
257264

258-
// withTimeout wraps a context with the default phase timeout.
265+
// withTimeout wraps a context with the default phase timeout (kept for legacy callers).
259266
func withTimeout(ctx context.Context) (context.Context, context.CancelFunc) {
260-
return context.WithTimeout(ctx, defaultPhaseTimeout)
267+
return context.WithTimeout(ctx, timeoutImplement)
268+
}
269+
270+
// withPhaseTimeout wraps a context with the timeout for a specific named phase.
271+
func withPhaseTimeout(ctx context.Context, phase string) (context.Context, context.CancelFunc) {
272+
d := timeoutImplement
273+
switch phase {
274+
case "plan":
275+
d = timeoutPlan
276+
case "pr":
277+
d = timeoutPR
278+
case "review":
279+
d = timeoutReview
280+
case "merge":
281+
d = timeoutMerge
282+
case "communicate":
283+
d = timeoutCommunicate
284+
}
285+
return context.WithTimeout(ctx, d)
261286
}
262287

263288
func (e *Engine) revert(ctx context.Context) error {

internal/evolution/phases.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const maxParallelTasks = 3
1717

1818
// RunPlanPhase runs the planning phase. Creates SESSION_PLAN.md via agent or fallback.
1919
func (e *Engine) RunPlanPhase(ctx context.Context, p iteragent.Provider, issues string) error {
20-
ctx, cancel := withTimeout(ctx)
20+
ctx, cancel := withPhaseTimeout(ctx, "plan")
2121
defer cancel()
2222

2323
identity, journal, day := readPlanContext(e.repoPath)
@@ -149,7 +149,7 @@ func appendPlanContext(sb *strings.Builder, learnings []byte, journal string, is
149149

150150
// RunImplementPhase reads SESSION_PLAN.md and executes tasks.
151151
func (e *Engine) RunImplementPhase(ctx context.Context, p iteragent.Provider) error {
152-
ctx, cancel := withTimeout(ctx)
152+
ctx, cancel := withPhaseTimeout(ctx, "implement")
153153
defer cancel()
154154

155155
planBytes, err := os.ReadFile(filepath.Join(e.repoPath, "SESSION_PLAN.md"))

internal/evolution/phases_communicate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717

1818
// RunCommunicatePhase writes journal, posts issue comments, records learnings.
1919
func (e *Engine) RunCommunicatePhase(ctx context.Context, p iteragent.Provider) error {
20-
ctx, cancel := withTimeout(ctx)
20+
ctx, cancel := withPhaseTimeout(ctx, "communicate")
2121
defer cancel()
2222

2323
planBytes, err := os.ReadFile(filepath.Join(e.repoPath, "SESSION_PLAN.md"))

internal/evolution/phases_pr.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
// RunPRPhase creates a feature branch from the current HEAD, pushes it, and opens a PR.
1515
// It is designed to run after RunImplementPhase has already committed changes to main.
1616
func (e *Engine) RunPRPhase(ctx context.Context) error {
17-
ctx, cancel := withTimeout(ctx)
17+
ctx, cancel := withPhaseTimeout(ctx, "pr")
1818
defer cancel()
1919

2020
day := e.readDayCount()
@@ -85,7 +85,7 @@ func (e *Engine) RunPRPhase(ctx context.Context) error {
8585

8686
// RunReviewPhase runs an AI self-review of the open PR.
8787
func (e *Engine) RunReviewPhase(ctx context.Context, p iteragent.Provider) error {
88-
ctx, cancel := withTimeout(ctx)
88+
ctx, cancel := withPhaseTimeout(ctx, "review")
8989
defer cancel()
9090

9191
if e.prNumber == 0 {
@@ -107,7 +107,7 @@ func (e *Engine) RunReviewPhase(ctx context.Context, p iteragent.Provider) error
107107

108108
// RunMergePhase merges the open PR, clears state, and returns to main.
109109
func (e *Engine) RunMergePhase(ctx context.Context) error {
110-
ctx, cancel := withTimeout(ctx)
110+
ctx, cancel := withPhaseTimeout(ctx, "merge")
111111
defer cancel()
112112

113113
if e.prNumber == 0 {

0 commit comments

Comments
 (0)