Files
hope-voice-api/.artifacts/20260831-105429-opencode-loop-guard/brief.md
T
2026-08-31 19:50:46 -04:00

6.8 KiB

Implementation Brief: OpenCode Loop Guard Plugin

Context

OpenCode is configured globally at ~/.config/opencode/. A local Qwen model frequently enters execution loops, repeating a small sequence of tool calls such as A -> B -> C until it consumes many turns. The built-in doom_loop permission only catches three consecutive identical tool-name and exact-argument calls. This task adds a complementary global plugin that detects periodic cycles across all tools.

Deliverables

  1. Create /Users/shane/.config/opencode/plugins/loop-guard.ts.
  2. Create focused tests at /Users/shane/.config/opencode/plugins/loop-guard.test.ts if the test runner can execute them directly; otherwise create a standalone test script under the same directory that uses no new dependencies.
  3. Do not modify ~/.config/opencode/opencode.json; plugins in the global plugin directory are auto-discovered.
  4. Do not add npm dependencies. Use only Node/Bun built-ins and existing @opencode-ai/plugin types.
  5. Write implementation report to /Users/shane/dev/voice-ios/.artifacts/20260831-105429-opencode-loop-guard/implementation.md.

Plugin Contract

Export a default plugin function typed as Plugin from @opencode-ai/plugin:

import type { Plugin } from "@opencode-ai/plugin"

export default (async (input) => ({ ... })) satisfies Plugin

The plugin must:

  • Maintain an in-memory map keyed by session ID.
  • On every tool.execute.before hook, append a canonical signature for { tool, args }.
  • Inspect the recent history for a repeating suffix cycle of length 1 through 4.
  • Detect a cycle only when the pattern has repeated at least three complete times after the current call is included.
  • If detected, throw an Error to block that tool execution.
  • Otherwise allow execution by returning normally.

Canonicalization Requirements

Canonical signatures must be stable and deterministic:

  • Include the exact tool name.
  • For object arguments, recursively sort object keys before serialization.
  • Preserve arrays in order.
  • Preserve string values exactly; do not trim, lowercase, collapse whitespace, or otherwise rewrite command text.
  • Use a compact JSON representation with sorted keys. A stable replacer is acceptable, but key sorting must be explicit and recursive.
  • Treat undefined fields as absent so key presence does not create spurious differences if the provider omits optional arguments.

Detection Algorithm

For each session history array H, after appending the current signature:

  1. For cycle length L = 1..4:
    • Require H.length >= L * 3.
    • Let the candidate pattern be the last L signatures.
    • Verify that the preceding 2L signatures consist of exactly two repetitions of that same pattern.
    • If yes, a three-repetition cycle is detected.
  2. Prefer the shortest matching cycle length for diagnostics.
  3. Do not inspect more than 16 recent signatures; keep this bounded and simple.

This means:

  • A A A A detects length 1 on the fourth call.
  • A B A B A B detects length 2 on the sixth call.
  • A B C A B C A B C detects length 3 on the ninth call.
  • A B C D A B C D A B C D detects length 4 on the twelfth call.
  • A B C A B C A B does not detect because only two complete repetitions plus a partial cycle exist.

Blocking Message

The thrown error message should be concise and model-facing. It must:

  • State that repeated tool-call cycling was detected and this exact call was blocked.
  • Describe the repeating shape without dumping raw arguments, for example 3 calls repeated 3 times.
  • Instruct the model to stop retrying the same sequence unchanged.
  • Ask it to compare expected versus observed results from prior attempts.
  • Require either a materially different tool or arguments based on new evidence, or a concise explanation of why progress is blocked and what information is needed.

Avoid including full command strings, file contents, tokens, environment values, or other sensitive data in the message.

State Management

  • Store at most 32 signatures per session.
  • Prune older entries when the limit is exceeded.
  • Clear state for a session on session.idle and session.deleted events if those event types are available on the SDK event union; guard with type-safe narrowing so the plugin still compiles if event names differ.
  • The plugin should be safe under concurrent tool calls within one assistant turn: use synchronous in-memory updates and no external I/O in the hook.
  • Do not write per-call state to disk.

Logging

Use input.client.app.log() when available, wrapped so a logging failure cannot block tool execution. Log only:

  • session ID
  • detected cycle length
  • number of repetitions
  • tool names in the cycle (not arguments)
  • outcome blocked

Do not log argument values.

Tests

Write tests that verify at least:

  1. Single-tool repetition is blocked on the fourth identical call and allowed for the first three.
  2. A two-call cycle is blocked only after A B A B A B.
  3. A three-call cycle is blocked only after A B C A B C A B C.
  4. A four-call cycle is blocked only after one full 12-call sequence.
  5. Changing any argument in the repeated call prevents detection.
  6. Object key order does not affect equality.
  7. Two sessions do not share history.
  8. History is bounded to the configured maximum.
  9. Session cleanup removes state.

Tests should exercise the exported detection/canonicalization logic directly rather than relying on a live OpenCode session. Structure the plugin so pure functions are exported for tests while the default export remains the plugin entry point. Example:

export function canonicalSignature(tool: string, args: unknown): string
export function detectCycle(history: string[]): { length: number; repetitions: number } | null
export function createLoopGuard(options?: { maxHistory?: number }): {
  record(sessionID: string, tool: string, args: unknown): { cycleLength: number; repetitions: number } | null
  clear(sessionID: string): void
}

Run the tests with Bun if available:

bun test /Users/shane/.config/opencode/plugins/loop-guard.test.ts

If bun test is unavailable, run the standalone script and record the exact command.

Verification

  • Type-check the plugin if a local TypeScript compiler is available; otherwise use Bun's loader/runtime as evidence.
  • Run the focused tests and capture pass/fail output in the implementation report.
  • Do not restart OpenCode or start a live model session.
  • Do not commit changes to any repository.
  • Do not modify files outside the global plugin directory and this task artifact directory.

Report Requirements

implementation.md must include:

  • task ID
  • status: complete, blocked, or no-change
  • files created or modified
  • detection algorithm summary
  • exact test command(s) and observed results
  • type-check or runtime evidence
  • known limitations
  • recommendation for the next review step