# How the no-mistakes CI Monitor Handles Idle Timeouts: Re-arming Logic Explained

> Discover how the no-mistakes CI monitor manages idle timeouts with its re-arming logic. Learn how active PRs stay alive while stale ones are removed.

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

---

**The no-mistakes CI monitor tracks idle timeouts against a movable anchor that resets whenever the upstream default branch advances, ensuring active PRs do not time out while stale PRs are cleaned up.**

The continuous integration monitor in the **kunchenguid/no-mistakes** repository implements a sophisticated idle timeout mechanism that distinguishes between stalled pull requests and those progressing against an active base branch. Implemented by the `CIStep` type in [`internal/pipeline/steps/ci.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci.go), the monitor uses a re-arming anchor pattern to dynamically extend timeouts when the default branch advances. Understanding this logic is essential for configuring CI pipelines that balance responsiveness with tolerance for upstream activity.

## Timeout Configuration and Semantics

The CI monitor watches a PR until one of three terminal conditions occurs: the PR is merged or closed, the monitor is cancelled, or the configured idle timeout elapses. The timeout duration is controlled by `config.CITimeout`, which supports three distinct behaviors based on its numeric value. This logic is implemented around lines 101–108 of [`internal/pipeline/steps/ci.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci.go).

### Parsing the CITimeout Value

The monitor interprets the timeout configuration using the following rules:

*   **Negative values (`< 0`)**: Disable the idle timeout completely, causing the monitor to run indefinitely until the PR closes or the process is cancelled.
*   **Zero**: Falls back to `config.DefaultCITimeout`, a sensible default defined in the configuration package.
*   **Positive values**: Define a finite idle timeout measured in seconds (or the relevant time unit).

## The Re-arming Anchor Pattern

Rather than measuring idle time from a fixed start point, the monitor tracks two separate timestamps to distinguish between overall execution time and idle time relative to base branch activity.

### Tracking started and timeoutAnchor

The step initializes two critical timestamps at lines 124–130:

*   **`started`**: Captures the moment the CI step begins and remains fixed for the entire run.
*   **`timeoutAnchor`**: Initially set to the same value as `started`, but designed to move forward.

The idle timeout is calculated as `now() - timeoutAnchor`. When the upstream default branch advances, `timeoutAnchor` is moved forward to the current time, effectively resetting the idle timer without restarting the entire step.

### Re-arming When the Base Branch Advances

After each polling iteration, the step resolves the current tip SHA of the upstream default branch (`baseBranchTip`) within a resolution window of `defaultBaseBranchTipResolveWindow = 30s`. If the resolved tip differs from the previously observed tip (`lastBaseTip`), the monitor *re-arms* by setting `timeoutAnchor = now()` (lines 156–176).

If the remaining timeout is shorter than the standard 30-second window, the resolution window is automatically trimmed to prevent exceeding the timeout during the check. This re-arming logic ensures that PRs tracking an actively evolving base branch remain open, while PRs stalled against a static branch eventually time out.

## Polling Loop and Timeout Enforcement

Each iteration of the polling loop first checks whether the elapsed time since `timeoutAnchor` exceeds the configured timeout (lines 152–154). If the threshold is exceeded, `timeoutOutcome()` constructs the appropriate terminal state (merged, closed, CI failure, or simple timeout).

### Grace Period for Empty CI Checks

Before declaring a timeout based on missing CI data, the step respects a `checksGracePeriod` (defaulting to 60 seconds). This grace period, referenced at lines 141–149, prevents premature termination when CI providers have not yet registered checks or delivered webhooks. The monitor only treats "no checks reported" as a terminal condition after this grace period has elapsed.

## Practical Implementation Examples

The following examples demonstrate how to configure the `CIStep` and simulate its re-arming behavior in tests.

```go
// Example: Creating a CIStep with a custom timeout and base branch resolver
ciStep := &steps.CIStep{
    // The timeout is typically sourced from config.CITimeout.
    // A negative value disables the timeout.
    // A zero value uses config.DefaultCITimeout.
    // Here we illustrate a custom resolver for the base branch tip.
    baseBranchTip: func(ctx context.Context) (string, bool) {
        // Custom resolver reading from a remote-tracking branch.
        tip, err := git.GetRemoteTip(ctx, repoURL, "origin/main")
        return tip, err == nil
    },
}

```

```go
// Example: Simulating timeout re-arming in a unit test
func TestCIMonitorRearmsOnBaseBranchAdvance(t *testing.T) {
    step := &steps.CIStep{
        now: func() time.Time { return time.Unix(0, 0) },
    }

    // Mock baseBranchTip to return advancing SHAs.
    call := 0
    step.baseBranchTip = func(ctx context.Context) (string, bool) {
        if call == 0 {
            call++
            return "sha1", true // Initial tip
        }
        return "sha2", true // Tip advanced → triggers re-arm
    }

    // Execution would verify that timeoutAnchor moves forward
    // when the base branch SHA changes from sha1 to sha2.
}

```

## Summary

*   The `CIStep` type in [`internal/pipeline/steps/ci.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci.go) implements idle timeout logic using a re-arming anchor pattern that distinguishes between global execution time and idle time relative to base branch activity.
*   Timeout values originate from `config.CITimeout`, where negative values disable timeouts, zero triggers the `config.DefaultCITimeout` fallback, and positive values specify finite durations.
*   The monitor maintains a fixed `started` timestamp and a movable `timeoutAnchor` that resets to `now()` whenever the upstream default branch tip advances (lines 156–176).
*   Each poll iteration validates the idle duration against `timeoutAnchor` (lines 152–154), with a 30-second resolution window for tip detection.
*   A 60-second `checksGracePeriod` (lines 141–149) prevents false timeouts during CI provider initialization latency.

## Frequently Asked Questions

### What happens if the CITimeout configuration is set to a negative value?

When `config.CITimeout` is less than zero, the monitor runs without an idle timeout limit, continuing to poll indefinitely until the pull request is merged, closed, or the process receives a cancellation signal.

### How does the re-arming mechanism prevent timeouts on active base branches?

The monitor resolves the current base branch tip SHA after each poll; if it differs from `lastBaseTip`, the code updates `timeoutAnchor` to the current time (lines 156–176). This resets the idle timer measurement, allowing the PR to remain monitored while the base branch continues to evolve.

### Where is the default timeout value defined when CITimeout is zero?

If `config.CITimeout` is zero, the system substitutes `config.DefaultCITimeout`, which is declared in the repository's configuration defaults (typically located in [`internal/config/defaults.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/config/defaults.go) or an adjacent configuration file).

### What is the purpose of the checksGracePeriod in the CI monitor?

The `checksGracePeriod` (defaulting to 60 seconds) provides a buffer after CI initiation before the monitor treats the absence of reported checks as a terminal failure condition. This accommodates network latency and webhook delivery delays from CI providers.