pi-loop-police

Pi extension: detects and interrupts infinite thinking-block and tool-call loops in real time before they exhaust your context window.

Packages

Package details

extensionskill

Install pi-loop-police from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:pi-loop-police
Package
pi-loop-police
Version
1.3.0
Published
Jun 27, 2026
Downloads
515/mo · 515/wk
Author
ncsebaxzero
License
MIT
Types
extension, skill
Size
45.2 KB
Dependencies
0 dependencies · 0 peers
Pi manifest JSON
{
  "extensions": [
    "./extensions"
  ],
  "skills": [
    "./skills"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

pi-loop-police

A pi extension that detects and breaks infinite loops in real time — before they waste your context window.

Small reasoning models (Qwen, DeepSeek, etc.) are prone to two kinds of loops:

  1. Thinking block loop — the model repeats the same phrases inside its <think> block over and over until the thinking quota is exhausted.
  2. Tool call loop — the model calls the same sequence of tools identically across turns, cycling indefinitely until the global context runs out.

Loop Police catches both mid-stream (not after the fact), aborts the looping output, trims it from context, and injects a recovery message so the model can continue with a fresh perspective.

Install

pi install git:github.com/sebaxzero/pi-loop-police.git

Or install project-locally (adds to .pi/settings.json only):

pi install git:github.com/sebaxzero/pi-loop-police.git -l

How it works

Thinking loop detection (two layers, mid-stream)

Layer 1 — character-level: Every 50 streamed characters, the extension checks whether the last ≥ 80 characters of the thinking block appear verbatim immediately before them (exact adjacent repetition). This catches the fastest, most common form of loop mid-stream.

Layer 2 — semantic-level: Simultaneously, the thinking text is split into paragraphs and each paragraph is fingerprinted by its first 60 characters. If the same fingerprint appears 3 or more times, the model is cycling through the same reasoning steps even if the wording varies slightly between passes.

On match (either layer):

  • ctx.abort() stops the stream immediately.
  • message_end trims the repeated portion and replaces it with [THINKING LOOP — truncated by loop-police] or [SEMANTIC LOOP — truncated by loop-police].
  • A recovery message is injected into context and triggers a new turn.

Cross-turn reasoning stagnation

After each clean (non-aborted) turn, the thinking text is stored. When the last N turns (default: 4) all have Jaccard word-set similarity ≥ 85% with their neighbor, the model is spinning without progress even though no single turn tripped the within-turn detectors. A recovery message is injected and the stagnation window is cleared.

File read repetition

Before each tool call, if the tool name looks like a file-read (read, view, cat, etc.) and the same path has been read 4 or more times, the call is blocked and a recovery message is injected.

Search expansion spiral

Before each search tool call (grep, search, find, glob, rg, etc.), the extension tracks how many distinct paths a given search pattern has been applied to. When the same pattern reaches 3 or more different paths, the call is blocked — the model is widening its search rather than acting on what it already found.

Tool call sequence loop

Before each tool executes, the extension hashes toolName + stableStringify(args) and appends it to a flat history. It then checks whether the last W calls are identical to the W calls immediately before them. On match:

  • The repeated call is blocked ({ block: true }).
  • A recovery message is injected explaining that the sequence is repeating and asking the model to reconsider.

Detection is exact — only identical repetitions trigger it, not similar ones.

Command

/loop-police                   — show current detection state and all config values
/loop-police reset             — clear all state (useful if a false positive fires)
/loop-police set KEY=VAL       — tune a config value live, no restart needed
/loop-police set KEY=VAL KEY=VAL ...  — set multiple values at once

Example: /loop-police set FILE_READ_LIMIT=6 STAGNATION_WINDOW=5

Configuration

Persistent configuration lives in extensions/loop-police.json (auto-created on first load with defaults). You can ask the agent to edit it directly, or tune values live with /loop-police set KEY=VAL.

Defaults:

MIN_THINKING_WINDOW: 80     // shortest repeating phrase to flag (chars)
MAX_THINKING_WINDOW: 2000   // longest phrase checked
CHECK_STRIDE: 50            // re-run detection every N new streamed chars
PARA_MIN_LEN: 40            // shortest paragraph to fingerprint
PARA_FINGERPRINT_LEN: 60    // chars used as paragraph identity key
PARA_LOOP_THRESHOLD: 3      // same paragraph fingerprint N times → semantic loop
STAGNATION_WINDOW: 4        // turns of similar thinking → stagnation
STAGNATION_THRESHOLD: 0.85  // Jaccard similarity threshold for stagnation
FILE_READ_LIMIT: 4          // reads of same file path before blocking
SEARCH_EXPAND_LIMIT: 3      // unique paths for same search pattern before blocking

Increase MIN_THINKING_WINDOW or PARA_LOOP_THRESHOLD if you get false positives on thinking loops. Increase FILE_READ_LIMIT for projects where legitimately re-reading files is common.

Compatibility

Designed for OpenAI-compatible reasoning models (Qwen3, DeepSeek-R1, etc.) used via pi. Pi normalizes all provider thinking formats to { type: "thinking", thinking: string } content blocks, so this extension works regardless of the underlying provider.

Works alongside pi-canary, which silently verifies agent context awareness using hidden canary tokens. When loop-police aborts a turn, pi-canary yields gracefully and does not fire its own recovery.

License

MIT


Built with Claude.