# How CI Monitor Timeout Re-arming Works in no-mistakes When the Base Branch Advances

> Understand CI monitor timeout re-arming in no-mistakes. Learn how an advancing base branch keeps PRs monitored indefinitely and stalled PRs eventually time out.

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

---

**The CI monitor resets its idle-timeout countdown by updating the `timeoutAnchor` to the current time whenever the base branch tip advances, allowing green pull requests that are repeatedly rebased onto an advancing base to remain monitored indefinitely while stalled ones eventually time out.**

The `no-mistakes` repository implements a **CI monitor** (the `CIStep`) that babysits open pull requests until they merge, close, or exceed a configured idle timeout. Understanding **CI monitor timeout re-arming** is essential for teams managing long-running pull requests, as the mechanism determines whether rebasing onto an advancing default branch extends the monitoring window or if the step terminates due to inactivity.

## Configuring the Idle Timeout Duration

The timeout value originates from `Config.CITimeout`, a duration parsed from the `ci_timeout` entry in the global configuration file. As defined in [[`internal/config/config.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/config/config.go)](https://github.com/kunchenguid/no-mistakes/blob/main/internal/config/config.go#L22-L28), this setting controls how long the monitor waits for base branch activity before terminating.

```yaml

# ~/.no-mistakes/config.yaml

ci_timeout: "168h"   # 7 days idle timeout (default)

```

To disable timeout entirely and allow the monitor to run forever, use the `unlimited` keyword or any non-positive value:

```yaml
ci_timeout: "unlimited"   # never times out

```

## Initializing the Timeout Anchor

When the `CIStep` starts, it establishes two critical timestamps. According to [[`internal/pipeline/steps/ci.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci.go)](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci.go#L25-L30), the step records `started := now()` and initializes `timeoutAnchor := started`. While `started` remains fixed for the step's lifetime to maintain stable poll intervals, the `timeoutAnchor` serves as the moving reference point for idle-timeout calculations.

## Detecting Base Branch Advances

The monitor detects base branch movement through the `baseBranchTip` resolver, which retrieves the SHA of the upstream default branch on every poll cycle (defaulting to a 30-second window). In [[`internal/pipeline/steps/ci.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci.go) lines 56-74](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci.go#L56-L74), the code compares the resolved tip against `lastBaseTip` to determine if the base has advanced.

## Re-arming Logic and Timeout Evaluation

When the resolver returns a tip that differs from `lastBaseTip`, the monitor logs the advance and **updates `timeoutAnchor` to the current time** (`now()`). This operation effectively restarts the idle-timeout countdown. Before each poll and after the base-branch check, the monitor evaluates `now().Sub(timeoutAnchor) >= timeout`. If the anchor has not been refreshed for longer than the configured `CITimeout`, the step exits with a timeout outcome.

This design ensures that **only base branch advances** reset the timeout anchor. Other activities, such as CI checks re-running or status updates, do not affect the idle-timeout calculation.

## Testing and Custom Implementation

The test suite in [[`internal/pipeline/steps/ci_test.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci_test.go)](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci_test.go) validates timeout behavior and re-arming logic by simulating base-branch advances with different `CITimeout` values. For custom implementations, you can override the poll interval and stub the `baseBranchTip` resolver:

```go
// In a custom pipeline you can override the poll interval for testing:
step := &steps.CIStep{
    pollIntervalOverride: 5 * time.Second,
    // baseBranchTip can be stubbed in tests to simulate advances.
}

```

## Summary

- **`timeoutAnchor` mechanism**: The monitor initializes `timeoutAnchor` at startup but moves it forward only when the base branch tip advances.
- **Exclusive re-arming**: Only base branch SHA changes update the anchor; CI status changes or other events do not reset the idle timeout.
- **Configuration**: Set `ci_timeout` in `~/.no-mistakes/config.yaml` using duration strings like `"168h"` or `"unlimited"` to disable timeouts.
- **Stable polling**: The `started` timestamp remains fixed, ensuring consistent poll intervals regardless of re-arming activity.
- **Termination condition**: The step exits when `now().Sub(timeoutAnchor)` exceeds `Config.CITimeout`, unless configured to run indefinitely.

## Frequently Asked Questions

### What happens if the base branch stops advancing?

If the base branch tip remains unchanged, the `timeoutAnchor` never updates. Once the duration between the current time and the anchor exceeds `Config.CITimeout`, the CI monitor terminates with a timeout outcome. The pull request remains open, but the monitoring step stops babysitting it.

### Can I disable timeout re-arming while keeping the timeout active?

No. According to the source code in [`internal/pipeline/steps/ci.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci.go), the re-arming logic is intrinsically linked to base branch advancement detection. The only way to prevent timeout re-arming is to disable the timeout entirely by setting `ci_timeout: "unlimited"` in the configuration.

### How does the idle timeout differ from total step runtime?

The idle timeout specifically measures time since the last base branch advance using `timeoutAnchor`, not total execution time. Because the `started` timestamp never changes, the poll interval and pacing remain stable indefinitely, while only the idle window resets when the base advances.

### Where is the default poll interval configured?

The default 30-second poll window is hardcoded in the `CIStep` implementation within [`internal/pipeline/steps/ci.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci.go). You can override this value programmatically by setting `pollIntervalOverride` when constructing the step instance, as demonstrated in the test suite.