# How the CI Monitor Timeout Is Interpreted and Re‑Armed in no‑mistakes

> Discover how no-mistakes interprets CI monitor timeout from ci_timeout configuration and re-arms it when the base branch advances. Learn about unlimited, 7-day, and Go duration settings.

- Repository: [Kun Chen/no-mistakes](https://github.com/kunchenguid/no-mistakes)
- Tags: internals
- Published: 2026-07-17

---

**The CI monitor timeout in no‑mistakes is interpreted from the `ci_timeout` configuration as either an unlimited duration, a 7‑day default, or a parsed Go duration, and is dynamically re‑armed whenever the base branch advances during polling.**

The `no‑mistakes` repository provides a continuous integration automation tool that monitors pull requests for status checks and mergeability. Understanding how the **CI monitor timeout** is configured and dynamically adjusted during execution is critical for preventing premature termination on long‑running or frequently rebased PRs.

## CI Monitor Timeout Configuration and Parsing

The timeout behavior originates in the global configuration file (`~/.no-mistakes/config.yaml`) via the `ci_timeout` key, which is loaded into the `Config.CITimeout` field. The interpretation logic resides in [`internal/config/config.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/config/config.go).

### Sentinel Values and Default Duration

The `parseCITimeout` function (lines 49‑66) implements specific parsing rules that map user input to internal constants defined in lines 21‑31:

- **Negative durations** or the keywords `unlimited`, `none`, `off`, and `never` are mapped to `CITimeoutUnlimited` (`time.Duration(-1)`), instructing the monitor to **never self‑terminate**.
- **Zero or omitted values** resolve to `DefaultCITimeout`, which equals **7 days**.
- **Positive strings** (e.g., `"48h"`, `"2h30m"`) are parsed as standard Go `time.Duration` values.

```go
// internal/config/config.go
const (
    CITimeoutUnlimited = time.Duration(-1)
    DefaultCITimeout   = 7 * 24 * time.Hour
)

func parseCITimeout(s string) (time.Duration, error) {
    switch strings.ToLower(s) {
    case "unlimited", "none", "off", "never":
        return CITimeoutUnlimited, nil
    }
    d, err := time.ParseDuration(s)
    if err != nil {
        return 0, err
    }
    if d <= 0 {
        return CITimeoutUnlimited, nil
    }
    return d, nil
}

```

## Re‑Arming Logic During Pipeline Execution

During a CI run, the monitor does not use a simple global timer. Instead, it employs an **anchored timeout** that slides forward when the upstream base branch advances.

### The Timeout Anchor Pattern

In [`internal/pipeline/steps/ci.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci.go) (lines 88‑92), the step initializes two temporal reference points:

- `started`: Captures the absolute start time for logging and pacing purposes.
- `timeoutAnchor`: The floating reference against which the timeout is evaluated, initially set equal to `started`.

The monitor checks expiration via `time.Since(timeoutAnchor) >= ciTimeout`, but only if `ciTimeout` is not `CITimeoutUnlimited`.

### Base Branch Advancement Detection

The re‑arming mechanism activates in the polling loop (lines 188‑196). On each iteration, the code resolves the current base branch tip via `baseBranchTip`. If the resolved tip differs from `lastTip`, the monitor **re‑arms** by resetting `timeoutAnchor` to the current time:

```go
// internal/pipeline/steps/ci.go (simplified)
started := time.Now()
timeoutAnchor := started
var lastTip string

for {
    // Timeout check (skipped if unlimited)
    if ciTimeout != CITimeoutUnlimited && time.Since(timeoutAnchor) >= ciTimeout {
        reportTimeout()
        return
    }

    tip, got := resolveBaseBranchTip(ctx, workDir, upstream, baseSHA, defaultBranch)
    if got && tip != lastTip {
        // Re‑arm: base branch moved forward
        timeoutAnchor = time.Now()
        lastTip = tip
    }

    // Poll CI checks, respecting overall pacing...
    time.Sleep(pollInterval)
}

```

This design ensures that PRs actively kept up‑to‑date with the base branch via rebasing can run indefinitely, while stagnant PRs terminate after the configured idle period. The re‑arming affects only the timeout horizon; it does not alter the `started` timestamp or the poll‑interval pacing.

## Summary

- Configure the **CI monitor timeout** via `ci_timeout` in `~/.no‑mistakes/config.yaml`.
- Negative values or keywords like `unlimited` disable timeouts via the `CITimeoutUnlimited` sentinel (`time.Duration(-1)`).
- Omitting the setting defaults to **7 days** (`DefaultCITimeout`).
- During execution, [`internal/pipeline/steps/ci.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci.go) maintains a sliding `timeoutAnchor` that resets whenever `baseBranchTip` detects a new base branch commit.
- This re‑arming mechanism allows long‑running PRs to remain monitored as long as they are actively rebased, while preventing indefinite waits on stale branches.

## Frequently Asked Questions

### What happens if I set `ci_timeout` to `unlimited`?

The `parseCITimeout` function in [`internal/config/config.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/config/config.go) maps the strings `unlimited`, `none`, `off`, or `never`—or any negative duration—to the `CITimeoutUnlimited` sentinel value. When the CI step in [`internal/pipeline/steps/ci.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci.go) detects this value, it skips the `time.Since(timeoutAnchor) >= ciTimeout` check entirely, allowing the monitor to run until the PR merges or the process receives an external termination signal.

### Does pushing new commits to the PR branch re‑arm the timeout?

No. The re‑arming logic specifically checks for changes to the **base branch tip**, not the PR branch. The code compares the resolved `baseBranchTip` against the stored `lastTip` variable. Only when the upstream base branch advances does `timeoutAnchor` reset to `time.Now()`. Activity on the PR branch itself updates CI statuses but does not affect the timeout anchor.

### How does the 7‑day default behave if the base branch advances daily?

The timeout window slides forward continuously. Since `timeoutAnchor` resets to the current time on each base branch advancement, the `time.Since(timeoutAnchor)` calculation never accumulates beyond one day. Consequently, the monitor will not terminate due to timeout while the base branch maintains daily activity; termination only occurs if the base branch remains static for more than 7 consecutive days.

### Can I set a timeout shorter than the poll interval?

Technically yes, but practical constraints apply. The timeout evaluation occurs at the start of each loop iteration in [`internal/pipeline/steps/ci.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci.go). If the timeout is shorter than the poll interval, the monitor will detect the expiration on the subsequent iteration. However, the actual termination latency includes the current sleep duration and any grace‑period handling, so sub‑minute timeouts may not terminate with millisecond precision.