gastownhall/beads · error

no timeout set

Error message

no timeout set

What it means

Returned by checkTimer when a timer gate has Timeout == 0 (cmd/bd/gate.go:1166). A timer gate without a configured duration can never expire, so evaluation fails with 'no timeout set' and the reason 'timer gate without timeout configured'.

Source

Thrown at cmd/bd/gate.go:1166

	// Evaluate status
	switch status.State {
	case "MERGED":
		return true, false, fmt.Sprintf("PR '%s' was merged", status.Title), nil
	case "CLOSED":
		return false, true, fmt.Sprintf("PR '%s' was closed without merging", status.Title), nil
	case "OPEN":
		return false, false, fmt.Sprintf("PR '%s' is still open", status.Title), nil
	default:
		return false, false, fmt.Sprintf("PR '%s' state: %s", status.Title, status.State), nil
	}
}

// checkTimer checks a timer gate for expiration
// Note: timers resolve but never escalate (escalated is always false by design)
func checkTimer(gate *types.Issue, now time.Time) (resolved, escalated bool, reason string, err error) { //nolint:unparam // escalated intentionally always false
	if gate.Timeout == 0 {
		return false, false, "timer gate without timeout configured", fmt.Errorf("no timeout set")
	}

	expiresAt := gate.CreatedAt.Add(gate.Timeout)
	if now.After(expiresAt) {
		expired := now.Sub(expiresAt).Round(time.Second)
		return true, false, fmt.Sprintf("timer expired %s ago", expired), nil
	}

	remaining := expiresAt.Sub(now).Round(time.Second)
	return false, false, fmt.Sprintf("expires in %s", remaining), nil
}

// issueGetter is the one storage method checkBeadGate needs, split out so
// tests can fake the lookup without standing up a Dolt store.
type issueGetter interface {
	GetIssue(ctx context.Context, id string) (*types.Issue, error)
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Set a timeout on the gate (e.g. `bd update <gate-id>` with a valid duration, or recreate with `--timeout 30m`)
  2. Verify the gate type is really 'timer' — other gate types don't need Timeout
  3. Check creation code/flags so the duration is parsed and assigned before persisting
  4. Validate Timeout > 0 in your gate-creation wrapper before saving

Example fix

// before
bd create "wait for CI" --type timer   // no timeout
// after
bd create "wait for CI" --type timer --timeout 30m
Defensive patterns

Strategy: validation

Validate before calling

if gate.Type == "timer" && gate.Timeout <= 0 {
    return errors.New("timer gate requires a positive timeout")
}

Try / catch

if _, _, _, err := checkTimer(gate, time.Now()); err != nil && err.Error() == "no timeout set" {
    // fix the gate config: set --timeout and re-create/update the gate
}

Prevention

When it happens

Trigger: A gate of type timer is created or evaluated with gate.Timeout left at its zero value — e.g. created without a --timeout flag or with a timeout that failed to parse into a duration.

Common situations: Manual gate creation forgetting the timeout field, YAML/JSON config omitting 'timeout', a type mismatch causing the duration to silently default to 0, programmatic creation passing an unset time.Duration.

Understand the failure class

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/b090d4532d4e472b. Report an issue: GitHub.