# How the no-mistakes CI Monitor Handles Idle Timeouts and Re-Arming

> Discover how the no-mistakes CI monitor prevents false timeouts on active PRs by re-arming to an anchor that resets on upstream branch advances ensuring stale requests terminate.

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

---

**The no-mistakes CI monitor tracks idle timeouts relative to a re-armable anchor that resets to the current time whenever the upstream default branch advances, preventing false timeouts on active PRs while ensuring stale requests eventually terminate.**

The CI monitor is implemented by the `CIStep` type in [`internal/pipeline/steps/ci.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci.go) and serves as the core mechanism for watching pull request CI status without letting jobs run indefinitely. Understanding how it balances **idle timeout** enforcement with **re-arming** logic is essential for configuring reliable CI pipelines in the no-mistakes repository.

## Idle Timeout Configuration and Semantics

The monitor evaluates three termination conditions during each polling cycle: the PR is merged or closed, the monitor receives a cancellation signal, or the configured idle timeout elapses. The timeout value originates from `config.CITimeout` and supports three distinct behaviors based on its numeric value:

- **Negative values** (`< 0`): Disable the timeout entirely, allowing the monitor to run indefinitely
- **Zero**: Use the system default defined in `config.DefaultCITimeout`
- **Positive values**: Set a finite idle timeout duration

This logic appears around lines 101-108 of [`internal/pipeline/steps/ci.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci.go), where the step initializes its timeout boundaries based on the global configuration.

## Timeout Measurement with Re-Arming Anchors

Rather than measuring against a fixed start time, the monitor employs a dual-timestamp system to enable intelligent re-arming. The `started` field records when the CI step begins and remains fixed for the entire run, while `timeoutAnchor` serves as the mutable reference point for timeout calculations.

The idle timeout is computed as `now() - timeoutAnchor`. Initially, `timeoutAnchor` equals `started` (lines 125-130), meaning the timeout begins counting from step initiation. However, unlike a static deadline, `timeoutAnchor` advances forward when specific external conditions change, effectively re-arming the timeout mechanism.

## Re-Arming Logic on Base Branch Advancement

The monitor re-arms—resetting the timeout countdown—whenever the upstream default branch demonstrates forward progress. After each poll, the step resolves the current tip SHA of the base branch (`baseBranchTip`) and compares it against the previously observed tip (`lastBaseTip`). If the values differ, indicating the base branch has advanced, the monitor executes the re-arming block between lines 156-176:

```go
// Conceptual flow derived from internal/pipeline/steps/ci.go
if currentTip != lastBaseTip {
    timeoutAnchor = now()  // Re-arm: reset the timeout window
    lastBaseTip = currentTip
}

```

This design ensures that PRs against actively developing base branches remain monitored without timing out, while still protecting against PRs that stagnate without CI activity. The resolution operation respects a `defaultBaseBranchTipResolveWindow` of 30 seconds, trimmed automatically if the remaining timeout is shorter.

## Polling Loop and Timeout Enforcement

Each iteration of the monitoring loop follows a strict sequence. First, the step checks whether `now() - timeoutAnchor` exceeds the configured timeout (lines 152-154). If exceeded, `timeoutOutcome()` constructs the terminal state—whether due to merge, closure, CI failure, or pure timeout.

The polling cycle proceeds only if the timeout has not elapsed, fetching updated check statuses and PR states before evaluating the re-arming conditions. This creates a deterministic flow where timeout checks always precede state updates, preventing race conditions between re-arming and termination.

## Grace Period for Empty CI Checks

Before declaring a timeout on PRs with no reported checks, the monitor enforces a `checksGracePeriod` (default 60 seconds) defined around lines 141-149. This buffer accommodates CI providers that register checks asynchronously, preventing the monitor from terminating valid PRs simply because the CI system hasn't reported status yet.

## Code Examples

### Configuring a CI Step with Custom Timeout Behavior

When constructing a `CIStep` manually or in tests, you interact with the timeout configuration through the struct fields and configuration objects:

```go
ciStep := &steps.CIStep{
    // The timeout is typically injected via config.CITimeout
    // Negative value: unlimited timeout
    // Zero: uses config.DefaultCITimeout  
    // Positive: specific duration
    
    // Custom base branch resolver for re-arming detection
    baseBranchTip: func(ctx context.Context) (string, bool) {
        tip, err := git.GetRemoteTip(ctx, repoURL, "origin/main")
        return tip, err == nil
    },
}

```

### Testing Re-Arming Behavior

The following test pattern verifies that `timeoutAnchor` advances when the base branch tip changes:

```go
func TestCIMonitorRearmsOnBaseBranchAdvance(t *testing.T) {
    step := &steps.CIStep{
        now: func() time.Time { return time.Unix(0, 0) },
    }

    // Mock advancing base branch tip
    call := 0
    step.baseBranchTip = func(ctx context.Context) (string, bool) {
        if call == 0 {
            call++
            return "sha-original", true
        }
        return "sha-advanced", true  // Triggers re-arm
    }

    // Execute step logic...
    // Verify that timeoutAnchor moved forward after second poll
}

```

## Summary

- **File location**: Core logic resides in [`internal/pipeline/steps/ci.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci.go) within the `CIStep` type
- **Timeout values**: Negative disables timeouts, zero uses `config.DefaultCITimeout`, positive sets finite limits
- **Re-arming trigger**: Advances in the upstream default branch tip reset `timeoutAnchor` to `now()`, extending the monitoring window
- **Dual timestamps**: `started` remains fixed while `timeoutAnchor` moves to enable re-arming without losing step history
- **Protection mechanisms**: 30-second resolution window for tip detection and 60-second grace period for empty CI checks prevent premature termination

## Frequently Asked Questions

### What triggers a re-arm in the no-mistakes CI monitor?

A re-arm occurs when the monitor detects that the upstream default branch tip SHA has changed since the last poll. Specifically, when `baseBranchTip` returns a value different from `lastBaseTip`, the step updates `timeoutAnchor` to the current time (lines 156-176 in [`internal/pipeline/steps/ci.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci.go)), effectively resetting the idle timeout countdown.

### How does the CI monitor handle negative or zero timeout configurations?

When `config.CITimeout` is negative, the monitor runs without any idle timeout (unlimited duration). When set to zero, the monitor falls back to `config.DefaultCITimeout`. Only positive values create a finite timeout window measured against the re-armable anchor.

### What is the purpose of the checks grace period?

The `checksGracePeriod` (default 60 seconds) prevents the monitor from timing out PRs that have no CI checks reported yet. This accommodates latency in CI provider APIs, ensuring the monitor doesn't terminate legitimate PRs simply because the CI system hasn't registered status updates.

### How does the monitor differentiate between a stale PR and an active base branch?

The monitor uses `timeoutAnchor` rather than a fixed start time for timeout calculations. If the base branch advances (indicating ongoing development), the re-arming mechanism pushes `timeoutAnchor` forward, preventing timeout. If the base branch remains static and no CI activity occurs, `timeoutAnchor` stays fixed and the timeout eventually elapses, identifying the PR as stale.