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

opencode-infinity-loop-guard

v1.1.2

Published

OpenCode loop guard: blocks repeated tool calls and aborts repetitive streaming text with bounded automatic continuation.

Readme

OpenCode Infinity Loop Guard

Maintainer: Joel Buchholz. MIT © 2026 Joel Buchholz.

Blocks the third identical tool call before execution, without blocking different calls. Also detects repetitive assistant prose while it is streaming, aborts the affected turn and can automatically resume useful work. Supported on Windows, WSL, Linux and macOS with OpenCode 1.18.0+ and Node.js 20+.

Features

  • Per-call blocking. The third matching call is stopped before execution; different calls remain usable.
  • Streaming protection. Watches assistant text snapshots and deltas, stopping repeated sentence blocks during generation.
  • Controlled continuation. After a successful abort and confirmed idle status, sends a synthetic [ANTI-LOOP STREAM] instruction to continue the original task with a different useful action. At most two automatic continuations per user request by default.
  • Independent counters. Each tool and argument combination has its own repetition counter in each session.
  • Explicit retry decisions. Use loop_decide to justify a fresh cycle for a blocked call.
  • Cosmetic-change detection. JSON key order and shell descriptions/timeouts do not bypass matching.
  • No extra service or token. Uses the existing OpenCode client. Automatic continuation starts another model request through your configured provider and can consume tokens.

System requirements

Runtime requirements

  • OpenCode 1.18.0 or newer with npm plugin support.
  • Node.js 20 or newer.
  • No interactive desktop or external services required.

Development/build requirements

These are required only when building or testing from source:

  • Node.js 20 or newer.
  • npm.

Platform support

The plugin runs inside OpenCode on native Windows, WSL, Linux and macOS. It uses Node.js built-ins and the OpenCode plugin API. Log paths use the runtime user's home directory; session state is stored in memory.

Install in native Windows OpenCode

Open PowerShell:

opencode --version
opencode plugin -g [email protected]
opencode

To replace an already configured version:

opencode plugin -g -f [email protected]

OpenCode writes the plugin to the global config and installs npm plugin dependencies automatically. No manual JSON editing or separate global npm installation is needed. Restart running OpenCode instances after installation. The CLI installation was verified with OpenCode 1.18.29.

Install in OpenCode running in WSL or Linux

Open the WSL or Linux terminal:

opencode --version
opencode plugin -g [email protected]
opencode

If the package is already configured:

opencode plugin -g -f [email protected]

The same commands apply on macOS. Run them inside the environment where OpenCode is installed. Write the package spec with a plain @; no backslash is needed.

Uninstall

Remove the plugin entry and restart OpenCode. If installed globally, also run npm uninstall -g opencode-infinity-loop-guard. The optional log and skill can be removed separately.

Exact behavior

Each session tracks tool name plus normalized arguments separately.

  1. The first and second matching calls run normally within the default 60-second window.
  2. The third matching call is rejected before execution with [ANTI-LOOP].
  3. Only that matching call is blocked. Different arguments or another tool remain usable immediately, with independent counters. No loop_decide call is required merely to change approach.
  4. If the new call repeats three times, only that call is blocked as well.
  5. An already blocked key stays blocked until an explicit continue decision or the session/process is discarded. The time window expires old counts, not established blocks.

JSON object keys are sorted for comparison. For bash, shell and powershell, description and timeout are ignored because changing those fields does not change the command. Command strings themselves are preserved exactly; this is not semantic command analysis.

Different calls returning the same result are not blocked together. There is no global tool gate and no requirement to approve every later command. The tool guard counts calls regardless of success or failure and does not cancel an already running tool by itself. The separate stream guard can abort an entire session turn. Concurrent requests are counted in the before-hook, and sessions are isolated.

Example: A, A, A(blocked), B, B, B(blocked), C succeeds for C. Blocking A does not interfere with B or C.

Streaming loops: abort and continue

The stream guard is enabled by default. It handles loops such as repeating “Let me check whether the containers started” and “The compose file is valid; I will check their status” without performing the next action.

  1. message.updated identifies assistant messages. message.part.updated supplies text snapshots; message.part.delta supplies incremental text. Snapshots replace the stored text so repeated event delivery does not count as repeated prose.

  2. Three consecutive identical normalized sentence blocks trigger detection. A block can contain one to eight completed sentences/lines; every sentence/line must contain at least 12 characters and three word tokens. Points inside filenames, dotfiles, URLs and version numbers are preserved, so announcements such as Ich aktualisiere die .env. and Ich aktualisiere README.md. are detected after three repetitions. Short meaningful status sentences are also detected after three repetitions. Case and whitespace are normalized. This detects the same sentence or an alternating sequence without treating every repeated short word as a loop. An additional check detects irregular sentence cycles: among the last 24 completed sentences/lines, at least 18 must belong to repeated sentences, there must be at most 12 distinct sentences, and a substantial sentence must occur at least streamThreshold + 2 times (five by default). The latest sentence must also be repeated. This catches changing sequences such as repeated edit announcements without claiming semantic understanding. Tool-part events clear the accumulated prose so real actions separate detection windows.

  3. The guard calls the actual OpenCode abort endpoint for that session. The plugin-provided SDK uses client.session.abort({ path: { id: sessionID } }). SDK v2 has a different argument shape; do not replace the plugin call with a v2 example blindly.

  4. Recovery runs outside the event hook, allowing abort and idle events to complete without a deadlock. After abort succeeds, the guard queries session status until idle (at most 10 seconds by default). An idle event alone is not treated as proof that abort succeeded.

  5. It sends a synthetic user instruction through session.promptAsync, preserving the interrupted assistant's agent and model when available. The instruction asks the agent to assess the last result, continue the original task with a different useful action, and stop with a specific explanation if no progress is possible. Existing tool restrictions and permissions still apply.

  6. After two automatic continuations, another detected loop is aborted without restarting. A new real user message resets this allowance. The guard's own synthetic messages do not reset it. New user input or session deletion cancels a pending continuation. A manually stopped turn is not automatically resumed unless the guard itself detected a loop.

Detection only examines live assistant text, not user text, tool output, reasoning parts, synthetic text or completed-message snapshots. Fenced code blocks and blockquotes are excluded. It is a conservative text heuristic, not semantic understanding: deliberate repetitive prose can trigger it, and paraphrased loops, unpunctuated output or loops inside code/reasoning can escape it. Automatic continuation is an instruction to the model, not a guarantee of progress.

Tracking is bounded: up to 100 sessions, eight recent messages per session, 32 text parts and 65,536 text characters per message. Oversized messages are excluded rather than truncating away code-fence context. If abort/status/continuation fails, the guard logs a warning and does not repeatedly retry. OpenCode toast notifications explain detected loops, exhausted recovery allowance and failures. Logs never include the repeated output or raw tool arguments.

Tools

| Tool | Purpose | | --- | --- | | loop_check | Inspect one planned call without granting permission | | loop_decide | Record a justified retry, changed approach or stop decision |

Decisions

loop_check takes toolName and argsJson (a JSON object encoded as a string) and reports only that call's count/block. It never grants permission.

loop_decide records an explicit assessment of a retry:

| Field | Meaning | | --- | --- | | decision | continue, change or stop | | observedResult | Concrete observation from previous attempts, at least 20 characters | | rationale | Why this action is useful, at least 20 characters | | expectedInformation | Expected new evidence or missing prerequisites, at least 20 characters | | nextTool | Exact tool to retry/change to | | argsJson | Its arguments as a JSON object encoded in a string |

  • continue: Authorizes a fresh counting cycle for exactly the specified key. On its next execution its old count/block is cleared and that execution counts as 1. The second matching execution runs normally; the third blocks again. Other keys and their counters are untouched. Unrelated calls do not consume this authorization.
  • change: Records a new approach, which must not be an already attempted/blocked key. It does not reset counters or block any tools. This decision is optional because different calls already run normally.
  • stop: Records abandoning the last blocked call and revokes any unused retry authorization for it. That call remains blocked, while different work continues freely. It does not reset the whole session. nextTool may be empty and argsJson may be {}.

loop_check and loop_decide are exempt from repetition counting. The plugin validates the presence of explanations but cannot determine whether an agent's reasoning is true or sensible.

Recommended global context

Add the following text to ~/.config/opencode/AGENTS.md. A separate GLOBAL-CONTEXT.md needs to be referenced by your OpenCode instructions configuration; its name alone does not load it.

## Repetitions and decisions

Before repeating a tool call, identify what changed and what new information
it can produce. Use loop_check when a repetition may be pointless.

An [ANTI-LOOP] tool block applies only to the matching tool and arguments.
Switch to a genuinely different approach normally; no global unlock is needed.
Do not merely change a description, timeout or cosmetic command text to evade it.

Retry the blocked call only after loop_decide with decision continue and
concrete observedResult, rationale and expectedInformation. State exactly
which tool and arguments should run. A fresh counting cycle then starts
for that call only; other work remains unaffected.

After three failed approaches to one problem, stop that approach, summarize
the limitation and continue independent useful work. Do not reset counters
just to keep retrying. Justified polling must have a clear expected change
and a stopping condition.

When [ANTI-LOOP STREAM] appears, the previous output stream was stopped for
repeating prose. Continue the original task from the last verified result.
Do not repeat the announcement, restart completed work or bypass permissions.
Choose and perform a concrete, different next action. If progress is impossible,
explain the specific blocker once. Do not automatically restart yourself after
the guard's recovery allowance has been exhausted.

Configuration and log

Optional wrapper instead of the npm entry:

import { AntiLoop } from "opencode-infinity-loop-guard";
export default async ctx => AntiLoop({
  ...ctx,
  config: {
    threshold: 3, windowMs: 60000,
    streamGuard: true, streamThreshold: 3,
    streamRecovery: true, streamMaxRecoveries: 2, streamIdleWaitMs: 10000
  }
});

threshold is an integer >=2 and windowMs a positive number. logFile defaults to ~/.config/opencode/loop-detector.log. Logs include events/tool names, not raw arguments or output. The legacy notify option is accepted but has no effect.

| Streaming option | Default | Meaning | | --- | --- | --- | | streamGuard | true | Enable live text-loop detection and abort | | streamThreshold | 3 | Consecutive block repetitions, integer 3–10 | | streamRecovery | true | Automatically continue after a confirmed abort; set false for stop-only behavior | | streamMaxRecoveries | 2 | Automatic continuations per real user request, integer 0–5 | | streamIdleWaitMs | 10000 | Idle wait deadline, 100–60000 milliseconds |

The same options can be supplied as OpenCode npm plugin options where supported. Tool-call counters are not reset by a streaming recovery. Set streamGuard: false to retain only the tool-call guard.

antiLoopSkillContent() returns optional complementary instructions. installAntiLoopSkill() writes them to ~/.config/opencode/skills/anti-loop/SKILL.md; this is optional and does not change the technical guard.

Smoke test from OpenCode

Ask the agent to call telegram_status three times in a disposable test session without connecting Telegram. The third call should be blocked. Then call a different harmless tool: it should succeed without loop_decide. Repeat the different call twice more and verify it blocks on its own third attempt.

Build and verification from source

npm ci
npm test
npm pack --dry-run

Tests cover per-call blocking, new-call independence, recovery cycles, independent counters, session isolation, normalized arguments, unchanged results from different calls, decisions and concurrent calls.

Streaming tests additionally cover snapshot/delta handling, the repeated-container-status example, code and user-text exclusions, abort ordering, failure handling, idle timeout, recovery limits, intervening user input and the real SDK request payloads.

npm run test:live starts an isolated local OpenCode server and a localhost-only fake model. The model deliberately streams repeated text; the test verifies that the provider stream closes and that the resumed agent produces the expected response. It uses no real model credentials. Set OPENCODE_TEST_BINARY to your OpenCode executable outside the default Windows installation. LOOP_GUARD_ENTRY can point to an installed dist/index.js to verify the npm build. Temporary integration-test files remain under the system temp directory for inspection.

Publishing

The npm package name is opencode-infinity-loop-guard.

npm login
npm publish --access public

The package declares win32, linux and darwin as installable platforms.

Maintainer

Joel Buchholz

License

MIT © 2026 Joel Buchholz