# How the CI Monitor Idle Timeout Works and Re-Arms on Base Branch Advances in No-Mistakes

> Learn how the CI monitor idle timeout in kunchenguid/no-mistakes works. Discover how it re-arms on base branch advances preventing premature expiration of long-running checks.

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

---

**The CI monitor in `kunchenguid/no-mistakes` implements a sliding idle timeout that automatically resets whenever the upstream base branch advances, ensuring long-running pull request checks do not expire prematurely during active repository development.**

The continuous integration monitoring system in the `kunchenguid/no-mistakes` repository handles long-lived polling cycles for open pull requests. Understanding the mechanics of its **CI monitor idle timeout**—including how it measures elapsed time and re-arms when the base branch moves—is critical for configuring robust automation that accommodates active development workflows.

## Configuring the Idle Timeout Duration

The timeout value originates from `Config.CITimeout` in [`internal/pipeline/steps/ci.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci.go). The implementation supports three distinct configuration modes at lines 64-71:

- **Negative values** (`< 0`) or the keyword **unlimited** disable the timeout completely, allowing the monitor to run indefinitely until the PR merges or closes.
- **Zero** (`0`) indicates no user-defined value, triggering the fallback to `config.DefaultCITimeout` (90 minutes) defined in [`internal/config/config.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/config/config.go).
- **Positive durations** set a finite idle timeout measured from the last significant event.

```go
timeout := sctx.Config.CITimeout               // (lines 64-66)
unlimited := timeout < 0                        // (lines 68-69)
if timeout == 0 {                               // (lines 70-71)
    timeout = config.DefaultCITimeout
}

```

## Timeout Anchoring and Measurement

Rather than using a static deadline, the monitor tracks two distinct timestamps to manage the **idle timeout** calculation. At initialization (lines 92-93), the code captures:

- **`started`** – A fixed reference marking when the monitor began, used for pacing poll intervals and grace periods.
- **`timeoutAnchor`** – A movable reference point initially set to `started` but updated dynamically when the base branch advances.

Each loop iteration evaluates whether the elapsed time since `timeoutAnchor` exceeds the configured limit:

```go
if !unlimited && now().Sub(timeoutAnchor) >= timeout {
    return timeoutOutcome()
}

```

This logic appears at lines 99-107, where `timeoutOutcome` determines whether to return a CI-failure or merge-conflict outcome based on the current state.

## Re-Arming When the Base Branch Advances

The **re-arming** mechanism prevents timeout expiration during active upstream development. The monitor periodically resolves the current tip SHA of the upstream default branch via the `baseBranchTip` callback (defaulting to `resolveDefaultBranchTip`). Within the polling loop (lines 19-38), it compares the resolved tip against `lastBaseTip`:

```go
if tip != lastBaseTip {                       // (lines 33-36)
    sctx.Log(fmt.Sprintf("base branch advanced (%s..%s), re-arming CI monitor timeout",
        shortSHA(lastBaseTip), shortSHA(tip)))
    timeoutAnchor = now()                     // (lines 35-36)
    lastBaseTip = tip
}

```

When the SHA differs, indicating the base branch has advanced, the code resets `timeoutAnchor` to the current time, effectively extending the monitoring window. This re-arming only occurs when the monitor is not in unlimited mode and the tip resolves successfully within the `resolveWindow` (default 30 seconds), ensuring the check does not block the main polling cycle.

## Execution Flow and Termination Conditions

The `Execute` method in [`internal/pipeline/steps/ci.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci.go) orchestrates the monitoring lifecycle:

1. **Initialization** – Determine `timeout`, `unlimited` flag, and set `started`/`timeoutAnchor`.
2. **Polling Loop** – Continuously execute until one of three termination conditions occurs:
   - The PR merges or closes.
   - The idle timeout expires relative to `timeoutAnchor`.
   - Manual abort via `axi abort` or context cancellation.
3. **Re-arming Check** – Resolve the base-branch tip; if changed, update `timeoutAnchor` and `lastBaseTip`.
4. **State Evaluation** – Check PR mergeability and CI check statuses, handling auto-fixes or reporting failures.

```go
// Example: Creating a CI step with default timeout configuration
runConfig := pipeline.StepConfig{
    CITimeout: 0, // 0 triggers 90-minute default
}
ciStep := &steps.CIStep{}
outcome, err := ciStep.Execute(&pipeline.StepContext{
    Ctx:     ctx,
    Config:  runConfig,
    // ... additional required fields
})

```

To manually terminate a monitor before timeout expiration, use the abort functionality:

```go
// Abort a running CI monitor by run ID
err := pipeline.AbortCIMonitor(ctx, runID)
if err != nil {
    log.Printf("Failed to abort CI monitor: %v", err)
}

```

## Summary

- The **CI monitor idle timeout** in `kunchenguid/no-mistakes` defaults to 90 minutes but supports unlimited or custom durations via `Config.CITimeout`.
- The `timeoutAnchor` variable provides a movable reference point distinct from the fixed `started` timestamp, enabling dynamic deadline extension.
- When the upstream base branch advances (detected via SHA changes in `baseBranchTip`), the monitor **re-arms** by setting `timeoutAnchor = now()`, resetting the idle countdown.
- The implementation resides primarily in [`internal/pipeline/steps/ci.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci.go) (lines 64-107 and 19-38), with configuration defaults defined in [`internal/config/config.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/config/config.go).
- Termination occurs only when the PR closes/merges, the timeout expires without re-arming, or an explicit abort signal interrupts the context.

## Frequently Asked Questions

### What happens when the CI monitor idle timeout expires?

When the elapsed time since `timeoutAnchor` exceeds the configured duration, the monitor invokes `timeoutOutcome()` at lines 99-107, returning either a CI-failure outcome or a merge-conflict outcome depending on the current pull request state, effectively terminating the watch cycle.

### How does the monitor detect that the base branch has advanced?

The monitor resolves the upstream default branch tip SHA via the `baseBranchTip` callback within each polling iteration (lines 19-38). If the resolved `tip` differs from `lastBaseTip`, the code logs the advancement and re-arms the timeout anchor to the current time, extending the monitoring window.

### Can I disable the idle timeout completely?

Yes. Setting `Config.CITimeout` to a negative value (`< 0`) or the keyword **unlimited** disables the timeout mechanism entirely, as implemented at lines 68-69 in [`internal/pipeline/steps/ci.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci.go). In this mode, the monitor continues until the PR merges, closes, or receives a manual abort signal.

### How do I manually abort a running CI monitor?

Execute `axi abort` from the command line or programmatically call `pipeline.AbortCIMonitor(ctx, runID)`, which signals the monitor's context to cancel. The polling loop checks for context cancellation at the start of each iteration and exits gracefully when detected.