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

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. 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.
  • Positive durations set a finite idle timeout measured from the last significant event.
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:

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:

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 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.
// 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:

// 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 (lines 64-107 and 19-38), with configuration defaults defined in 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. 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →