Iterative Debugging: Fixing Hallucinations in 9B Local Agents

Jul 23, 2026 min read

You hit enter. OpenCode spins up a 50-line component, the terminal goes quiet for a second, and then your dev server throws it back at you in red: ReferenceError: window is not defined. The agent just wrote perfectly confident, perfectly broken code — it reached for window.innerWidth like it was writing a client-side script, with no idea it was about to run on your Next.js server first.

This is the tax you pay for running Qwen3.5 9B instead of a frontier cloud model. In exchange for inference that runs entirely on your own 8GB card, you get a model that hallucinates more often and reasons less deeply about the code it just wrote. It’s not that 9B models are bad at coding — it’s that the “notice my own mistake” step is exactly where a smaller model’s reasoning gives out first. If you don’t know how to work that step yourself, you end up spending more time untangling AI-generated bugs than you would have spent writing the function from scratch.

The fix isn’t better prompts, it’s a better loop. Feed the agent the exact failure, tell it precisely what changed between what it assumed and what’s actually true, and stop it before it spins on the same broken fix a fourth time. Do that consistently and debugging with a local model becomes closer to pairing with a fast, slightly overconfident junior than fighting a black box.

Understanding the Agentic Loop

How OpenCode Thinks

Every agentic coding tool runs some version of the same cycle: Reason, Act, Observe, Evaluate, and — when something breaks — Debug, then repeat. The agent plans a sub-task, writes code, watches the result (a passing build, a stack trace, a lint error), and decides what to do next based on what it observed.

The Evaluate step is where 9B models struggle. Handed a stack trace, a frontier model reasons backward from the error to a root cause with real depth. A 9B model, working with far less headroom, often pattern-matches to the nearest plausible-looking fix instead — and if that fix happens to invoke a method that doesn’t exist, it commits to it with the same confidence it had for the original code. There’s also a subtler failure mode worth naming: as a debugging session drags on and the context fills with failed attempts, the model can lose track of the original goal entirely, chasing whatever error is directly in front of it instead of the feature it was actually building. Call it intent debt — the more failed context accumulates, the more of the original ask gets crowded out.

Agentic Loop Diagram

The Agentic Loop Visualization illustrates the core cognitive cycle of an AI coding agent, highlighting the critical “Evaluate” phase where 9B local models frequently experience intent debt and pattern-matching failures during debugging.

Agentic Loop Diagram

ReasonActDebugObserveEvaluaEtreror

Visual Notes:

  • The primary loop moves left-to-right, but the critical path often drops into the Debug phase upon encountering an error.
  • The breakdown for 9B models specifically occurs between Evaluate and Debug, where they rely on superficial pattern-matching rather than deep root-cause analysis.

Spotting Hallucinations Early

Two failure types show up constantly with 9B models, and telling them apart determines how you respond.

Ghost APIs — the model assumes a library has a method it doesn’t. A classic example: calling fs.writeJSONSync() in a standard Node project, a method that only exists if you’ve installed fs-extra, which the model never confirmed was in your package.json. The tell is a TypeError: fs.writeJSONSync is not a function pointing at a method name that sounds entirely plausible but was never actually imported.

Environment blindness — the model writes browser-only code without accounting for where it actually runs. This is the window/document problem, and it’s the single most common bug local agents produce in a Next.js project, because the model is pattern-matching against years of client-side React tutorials without tracking that App Router components render on the server first.

Either way, the earliest sign the agent is trapped rather than genuinely fixing something: it applies a change, the error stays effectively the same, and its next “fix” is a variation on the last one rather than a new hypothesis. That’s your signal to stop letting it iterate freely.

The Iterative Fix

Guiding, Not Coding

The instinct when something breaks is to just fix it yourself — but the actual skill here is writing a correction prompt precise enough that the agent fixes it correctly, without you touching the file. That means feeding back the literal error, not a paraphrase of it:

“The previous execution failed with ReferenceError: window is not defined at line 42. This component renders on the Next.js server first. Wrap the window logic in a useEffect hook so it only runs on the client, after hydration.”

Compare that to “it’s broken, fix it.” A 9B model doesn’t have the reasoning depth to infer why something failed from a vague prompt — it needs the exact string, the line number if you have it, and ideally a sentence explaining the constraint it violated. You’re not doing its job for it; you’re giving it the one piece of information it can’t reliably infer on its own.

When to Take the Wheel

Set a hard limit before you start: three failed attempts at the same fix, and you stop letting the loop run. Past that point, the failed attempts are sitting in context, weighting the model’s next prediction toward the same broken pattern rather than a fresh one — the agent isn’t converging on a solution, it’s converging on its own mistake.

When you hit that limit, don’t just say “try again.” Clear the conversation, paste in the current (still broken) code, and say explicitly: “Discard your previous approach. Do not use useState for this — start fresh with a useEffect-based fix.” That instruction matters more than it looks like it should. Without it, a fresh context window doesn’t guarantee a fresh approach — the model can arrive back at the same pattern purely because it’s the statistically obvious one for the problem as described.

Practical Example: Resolving a React Hydration Error

Here’s the scenario end to end. OpenCode generated a WindowWidth component meant to conditionally render a mobile layout:

Before — the broken generation:

// components/WindowWidth.tsx
// Assume MobileLayout and DesktopLayout are imported

export default function WindowWidth() {
  const width = window.innerWidth; // crashes: no `window` on the server

  return (
    <div>
      {width < 768 ? <MobileLayout /> : <DesktopLayout />}
    </div>
  );
}

Step 1 — copy the exact error. Next.js prerenders this component on the server, where window doesn’t exist, and the terminal throws:

ReferenceError: window is not defined
    at WindowWidth (components/WindowWidth.tsx:4:17)

Step 2 — formulate the correction prompt. Paste the stack trace verbatim and explain the constraint the model missed:

“This failed with ReferenceError: window is not defined at WindowWidth.tsx:4. Next.js renders this component on the server first, where window doesn’t exist. Move the window.innerWidth read into a useEffect hook so it only runs client-side after mount, and use a state variable to store the value.”

Step 3 — let the agent rewrite it. The corrected version:

// components/WindowWidth.tsx
"use client";

import { useState, useEffect } from "react";

// Assume MobileLayout and DesktopLayout are imported

export default function WindowWidth() {
  // Start with a known server-safe default; real value fills in after mount
  const [width, setWidth] = useState<number | null>(null);

  useEffect(() => {
    setWidth(window.innerWidth);

    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  // Avoid rendering a layout guess before the client has reported a real width
  // Note: Using a skeleton loader here instead of null would be a great UX improvement
  if (width === null) return null;

  return (
    <div>
      {width < 768 ? <MobileLayout /> : <DesktopLayout />}
    </div>
  );
}

The useEffect only executes after the component mounts in the browser, so window is guaranteed to exist by the time it’s read. The width === null guard matters just as much as the useEffect — without it, the server renders one layout, the client’s first paint briefly shows nothing, then flips to the correct layout, which is a hydration mismatch by another name. If OpenCode’s first correction skips that guard, that’s worth a second, equally specific prompt: “This still causes a hydration mismatch because the server and initial client render produce different output. Add a loading state that renders null until width is set.”

Best Practices and Tips

Do:

  • Paste the exact error message, not a summary of it. “It says window is undefined” gives the model less to work with than the actual stack trace.
  • Name the line number when you have it. It narrows the model’s search space from “somewhere in this file” to “this line.”

Don’t:

  • Say “it’s broken, fix it” and expect a 9B model to infer the cause. It doesn’t have the reasoning depth for that leap — be the one who does the diagnosis, and let it do the rewrite.
  • Let the agent rewrite an entire file to fix one broken line. Full-file rewrites burn through context unnecessarily — even with a 262,144-token context window available in Qwen3.5, every unrelated line it rewrites is a new opportunity to introduce a bug you didn’t have before.

Troubleshooting Common Issues

The agent keeps making the same mistake, over and over. The failed approach is sitting in the context history, and it’s weighting every subsequent prediction back toward itself — the model isn’t stubborn, it’s anchored. Clear the chat history entirely, feed it the current broken code fresh, and say explicitly: “Discard your previous approach. Do not use [the specific library or method that kept failing]. Start over.” Naming the thing to avoid is what actually breaks the anchor — a plain “try again” usually just reproduces the same fix.

Conclusion

Debugging AI-generated code isn’t a one-shot exchange, it’s a collaborative loop, and your job in that loop is diagnosis: read the actual error, hand the agent the exact string and the constraint it violated, and know when three failed attempts means it’s time to reset the context instead of asking a fourth time. Do that, and a 9B model running on your own hardware fixes its own mistakes often enough that hand-writing the fix becomes the exception, not the rule.

Next steps: the next time OpenCode throws an error, practice feeding it the raw terminal output instead of paraphrasing. The final article in this series covers wiring in external data sources through MCP, once your agent’s foundation — architecture, security, and now debugging — is solid.

Related Articles in This Series:

Sources