npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@solknight48/pi-loop-guard

v0.1.0

Published

Bounds LLM degeneration loops in pi: collapses duplicate tool-call batches, blocks stuck repeated calls, aborts runaway repeated text, and rewrites token-limit truncation errors so they stop re-arming the loop.

Readme

pi-loop-guard

A pi extension that bounds the blast radius of LLM degeneration loops.

Why

A pi session (0.84.0) degenerated into a repetition loop: the model emitted one assistant message containing several hundred byte-identical tool calls — all of which were executed, with every full result appended to context. When the response hit the output token limit mid-batch, the injected error said "Re-issue the tool call with complete arguments", which told the degenerating model to emit the same batch again. Each round-trip further poisoned the context until the user killed the session.

Model-side degeneration can't be prevented by a harness — but it can be contained. This package contains it.

The four guards

  1. Intra-turn duplicate collapse — identical (tool, args) calls within one turn run once; the rest are blocked with a short stub reason. A degenerate batch costs one execution instead of hundreds, and the model gets a clear "already in this batch, do not repeat" signal.
  2. Cross-turn stuck detection — the same exact call issued in every one of the last K turns is blocked with a "you are stuck, stop and ask the user" reason. Catches the slow one-call-per-turn loop. Two precision rules keep it off legitimate workflows: the window is scoped to a single agent run (a user re-asking the same thing across prompts never trips it), and a call whose recent results differ is treated as observing changing state (polling, re-reading an edited file) and allowed — the block engages only when the last K−1 executions returned identical content, or when no execution evidence exists at all.
  3. Stream degeneration abort — assistant output that ends with the same chunk (≥24 chars) repeated ≥8 times consecutively aborts the turn. Whitespace-only chunks are ignored.
  4. Truncation re-arm neutralizer — the error results pi injects for a batch cut off by the output token limit ("… Re-issue the tool call with complete arguments.") are rewritten so they no longer instruct the model to re-emit the failed batch (re-issuing the single truncated call is still allowed). This guard hooks message_end, not tool_result: in pi 0.84.0 the truncation path builds those results without going through the tool pipeline, so the tool_result extension event never fires for them; the message_end replacement is mutated into the live run's context before the next LLM call.

Install

pi install npm:@solknight48/pi-loop-guard
pi install git:github.com/solknight48/pi-loop-guard   # or straight from git
pi install /path/to/pi-loop-guard                     # or from a local clone

Or try it for one run without installing:

pi -e /path/to/pi-loop-guard

Usage

Once loaded it works silently; a 🛡 loop-guard status appears in the footer.

/loop-guard         show status and per-session counters
/loop-guard on|off  toggle all guards at runtime
/loop-guard reset   zero the counters and clear the stuck-call window

reset is the escape hatch when guard 2 blocks a call you deliberately want to retry. The footer status shows 🛡 loop-guard OFF while disabled.

Configuration

Environment variables (read at extension load):

| Variable | Default | Meaning | |---|---|---| | PI_LOOP_GUARD_MAX_DUP_PER_TURN | 1 | Identical calls allowed per turn; the rest are blocked | | PI_LOOP_GUARD_MAX_CONSEC_TURNS | 4 | Same call in this many consecutive turns ⇒ blocked as stuck | | PI_LOOP_GUARD_TEXT_GUARD | 1 | Set 0 to disable the stream-abort guard | | PI_LOOP_GUARD_MIN_UNIT | 24 | Minimum repeated-chunk length (chars) for the stream guard | | PI_LOOP_GUARD_MIN_REPEATS | 8 | Consecutive repeats that trigger the stream guard |

False-positive notes

  • Polling that actually observes change (a growing log, a file being edited) does not trip guard 2 — differing results suspend the block. Polling whose output is byte-identical 4 turns running does trip it, deliberately: the call is gaining nothing, and the block reason tells the model to add a sleep or vary the command. Use /loop-guard reset to retry a blocked call, or /loop-guard off if a workflow needs raw repetition.
  • The flip side: a degenerate loop whose output contains noise (timestamps, counters) won't be caught by guard 2 — results differ every time. Guards 1 and 3 still bound it within a turn.
  • The stream guard only fires on long repeated chunks (≥24 chars, ≥8 consecutive copies), so repeated short tokens (log separators, table rows) do not trigger it. Known limits: repetition units longer than 400 chars are not detected, and the final <512 chars of a message go unchecked (the scan is throttled to every 512 streamed chars).

Development

npm install   # installs jiti (dev-only; the extension itself has zero runtime deps)
npm test      # unit tests via a mock ExtensionAPI

Without a local npm install, the test runner falls back to the jiti bundled with a globally installed pi (or set PI_PACKAGE_DIR to a pi package directory). The mock encodes assumptions about pi's real event flow; the facts it depends on are listed at the top of test/run-tests.cjs — re-verify them when upgrading pi.

The extension is a single file: extensions/loop-guard.ts.

License

MIT