Skip to content

bug(session): infinite compaction loop when compression fails to reduce context #27924

Closed as not planned

Description

@ranxianglei

Bug Description

The session loop in prompt.ts can enter an infinite compaction loop:

overflow detected → compact → still overflow → compact → ... (infinite)

This happens when compaction fails to reduce the context below the token limit — for example, when compression state is lost, messages are inherently uncompressable, or the compression plugin returns an error.

The loop spins indefinitely consuming API credits without making progress.

Steps to Reproduce

  1. Have a session with large context that triggers overflow
  2. If compaction fails or doesn't reduce tokens enough, the loop retries indefinitely
  3. Observe continuous API calls with no progress

Root Cause

The runLoop function has two paths that trigger compaction and continue without any guard:

  1. Overflow check: after each step, if compaction.isOverflow() returns true, it calls compaction.create() and continues — but the next iteration sees the same overflow and repeats.

  2. Compact result: when the provider returns "compact" as the result, it calls compaction.create() and continues — same infinite loop if compaction doesn't actually reduce context.

Neither path has a retry counter or break condition.

Expected Behavior

After a reasonable number of compaction attempts (e.g., 2), the loop should break and return an error to the user, rather than spinning indefinitely.

Activity

  1. github-actions commented on May 16, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    If your issue is distinct from these (e.g., it specifically covers the case where compaction runs but doesn't reduce tokens below the limit, with no retry guard), please clarify the difference so maintainers can track it separately.

  2. tommy29tmar commented on May 17, 2026

    @tommy29tmar

    I’d make this a terminal state rather than another retry path. A useful regression test would force overflow, make compaction return the same-or-larger token count (or an error), then verify the runner:

    1. attempts compaction at most N times,
    2. records before/after token counts and the compaction reason,
    3. stops further model calls with a state like compaction_failed / blocked_context_overflow,
    4. preserves the last successful turn plus a recovery handoff, and
    5. tells the user the exact next recovery action: export/clear/restart or remove the uncompressible payload.

    Without the before/after token receipt, an infinite compact loop can look like progress while only spending credits.

  3. dawidbednarczyk commented on May 23, 2026

    @dawidbednarczyk

    Draft comment for #27924

    Status: DRAFT — gated as comms-in-user-name. Do NOT post until user (dbednarc) reviews and approves.
    Target: #27924
    Author identity if posted: dbednarc (Cisco CX EMEA)
    Date drafted: 2026-05-23


    Suggested comment body

    Confirming this on OpenCode 1.14.50 with Claude Opus 4.7 via GitHub Copilot, reasoning=medium. Reproduces deterministically as a compound of the runLoop defect you describe + a Copilot-provider context-budget mismatch.

    Reproduction

    1. Fresh session, cwd is a repo with a large AGENTS.md (~30K tokens) plus tool schemas (MCP servers loaded: tech-playbook, second-brain, salesforce-dx, quicker, delphi) — combined first-message overhead ~100K tokens.
    2. Run an unremarkable orchestration task (in our case: a documentation-drift port across sister repos that completed cleanly at commit 52aedfd).
    3. After agent reports "task complete", send "continue" or any short follow-up.
    4. Spinner enters ▣ Compaction · Claude Opus 4.7 and never exits. Status bar shows "9.5K / 5% used" — i.e. the post-compaction count — yet compaction triggers again immediately on the next loop iteration.
    5. Each cycle burns one Opus 4.7 request at Copilot's 7.5× premium rate, indefinitely, until the user kills the process.

    Root-cause analysis (two compounding defects)

    Defect 1 (this issue) — runLoop has no retry guard. As you noted, both the overflow path (compaction.isOverflow() → compaction.create() → continue) and the provider-returned "compact" result path call compaction.create() without any retry counter, break condition, or "did compaction actually reduce context below threshold?" check. If compaction's output token count is still ≥ the heuristic limit, the very next iteration sees the same overflow and re-fires. Pure infinite loop.

    Defect 2 — Copilot provider context heuristic is too conservative for Claude Opus on Copilot. The GitHub Copilot provider appears to use a heuristic-backed default context limit instead of the actual 200K cap Copilot grants claude-opus-4.7. With our ~100K of system/tool/AGENTS.md overhead, the session looks already-over-budget against the heuristic before the user has typed anything — so Defect 1's loop fires on the first turn after any idle interval.

    Defect 2 is workaroundable client-side (explicit provider.github-copilot.models.claude-opus-4.7.limit.context = 200000 in opencode.json) — but Defect 1 still bites whenever compaction genuinely fails to reduce context below threshold (e.g. when the in-context AGENTS.md + tool schemas alone exceed the post-compaction target). The loop should break after N consecutive non-reducing compactions, surface an error to the user, and let the user choose between "/clear", switch model, or manual context prune.

    Suggested guard (sketch)

    // pseudo-code in runLoop compaction paths
    let compactionAttempts = 0
    const MAX_COMPACTION_ATTEMPTS = 2
    const MIN_REDUCTION_RATIO = 0.1  // require ≥10% token reduction per pass
    
    const beforeTokens = currentContextTokens()
    await compaction.create()
    const afterTokens = currentContextTokens()
    
    if ((beforeTokens - afterTokens) / beforeTokens < MIN_REDUCTION_RATIO) {
      compactionAttempts++
      if (compactionAttempts >= MAX_COMPACTION_ATTEMPTS) {
        throw new CompactionStalledError({
          beforeTokens, afterTokens, attempts: compactionAttempts,
          hint: "AGENTS.md + tool schemas likely exceed post-compaction target. " +
                "Try `/clear`, switch model, or reduce AGENTS.md/MCP footprint."
        })
      }
    }

    Happy to test a fix against this repro if a PR lands.

    Cross-references

    (All of the above describe loop conditions that this issue's runLoop guard would also cover.)

    Workaround we are running

    ~/.config/opencode/opencode.json:

    "compaction": { "auto": false, "prune": true, "reserved": 10000 },
    "provider": {
      "github-copilot": {
        "models": {
          "claude-opus-4.7":  { "limit": { "context": 200000, "output": 64000 } },
          "claude-opus-4.6":  { "limit": { "context": 200000, "output": 64000 } },
          "claude-sonnet-4.5":{ "limit": { "context": 200000, "output": 32000 } },
          "claude-haiku-4.5": { "limit": { "context": 200000, "output": 32000 } }
        }
      }
    }

    auto: false trades the loop risk for occasional hard-stops at context limit, which we'd rather handle manually than burn premium requests in a spinner.


    Pre-post checklist (for user before posting)

    • Confirm OpenCode version still matches (opencode --version == 1.14.50)
    • Re-confirm Copilot Opus 4.7 still pinned to medium reasoning + 200K cap (no upstream tier change)
    • Sanity-check tone (factual, no Cisco-internal references, no customer names)
    • Decide whether to mention CX/Cisco context (recommendation: no — keep generic, posted as personal GitHub identity)
    • Optional: attach a redacted session log if reviewers ask (currently we do NOT have one — stuck instance was killed before transcript export)
    • Post via gh issue comment 27924 --repo sst/opencode --body-file ISSUE_COMMENT_DRAFT.md (strip the front-matter and the checklist before posting — only the "Suggested comment body" section above goes on the issue)
  4. selfagency commented on May 25, 2026

    @selfagency

    I can attest it's happening with more than just Copilot. It happens across various models on OpenCode Go and Ollama Cloud as well. I think it's a bigger issue than just one provider. But this right here "compaction": { "auto": false, "prune": true, "reserved": 10000 }, just helped dramatically.

  5. cwj2001 commented on Jun 26, 2026

    @cwj2001

    I reproduced this on OpenCode 1.17.11 with a long-running session. The DB tail showed automatic overflow compaction ({"type":"compaction","auto":true,"overflow":true}) followed by a compaction assistant failure (MessageAbortedError), and resuming the same session re-entered agent=compaction mode=primary instead of stopping on the original overflow.

    I opened a small PR focused on that recovery gap: if overflow recovery summarization aborts/dies with a non-interrupt failure, treat compaction as failed and persist the original provider overflow as the assistant error. Real user/session interrupts are still preserved.

  6. a692570 commented on Jul 18, 2026

    @a692570

    Reproduced on 1.18.2 and dug into where the loop actually lives. One data point that may help scope the fix: the two open PRs (#34029, #34261) patch the core V2 runner (packages/core/src/session/runner/llm.ts and packages/core/src/session/compaction.ts), but the loop I captured runs through the session prompt path in packages/opencode/src/session/prompt.ts, which is what the server prompt handler wires up. Unless I am misreading the wiring, those PRs will not cover it.

    Evidence from my local DB: one long-running opencode run session recorded 35 consecutive assistant messages with mode: "compaction", one every 30 to 90 seconds for over an hour. Every one of them finished stop with no error. So this is not only the summarizer-failure mode: each summary succeeded but did not shrink the request enough (we had compaction.prune: false, so giant old tool outputs stayed), the next turn overflowed, and compaction.create fired again with nothing bounding the cycle.

    In SessionPrompt the mid-turn overflow path (result === "compact" -> compaction.create) has no attempt limit and no check that compaction reduced tokens, which is the same gap #34261 closes for the core runner.

    What fixed it for us, in case it helps others:

    1. compaction.prune: true so compaction can actually drop old tool outputs.
    2. Pinning the built-in compaction agent to a large-context model so the summary prompt itself cannot overflow.
    3. A small guard in the SessionPrompt loop: count consecutive overflow -> compaction cycles with no successful turn in between, and after 3, fail the message with ContextOverflowError instead of compacting again. Running this as a source patch since yesterday; the loop is gone and legitimate multi-compaction sessions still work because any productive turn resets the counter.

    Happy to open a PR for the guard on the SessionPrompt path if maintainers want it, either standalone or rebased on top of #34261.

  7. a692570 commented on Jul 18, 2026

    @a692570

    Opened #37584 with the SessionPrompt guard described above.

  8. lazebird commented on Aug 12, 2026

    @lazebird
    Image

    like this?

  9. YASoftwareDev commented on Aug 19, 2026

    @YASoftwareDev

    Deterministic fresh-session reproduction of this loop (OpenCode 1.18.18, OpenAI-compatible local vLLM endpoint):

    Setup: a model entry with limit: {context: 65536, output: 32768} plus one MCP server exposing a large tool catalog (GitLab MCP, 135 tools). No conversation history — a brand-new session.

    Repro: opencode run "print exactly: PROBE-OK" in an empty directory.

    Result: the session immediately enters the compaction agent and streams compaction rounds forever (13 agent=compaction stream events in 240 s with --print-logs; single message=created, so this is not a continued session). The task never answers; the process never exits. Control: the identical config with the MCP server disabled completes in 7 s with zero compaction events.

    Why it can never make progress: the compaction trigger sits at context − max(output, buffer) = 65536 − 32768 = 32768 tokens, and the fixed prompt overhead (135 tool definitions + system prompt + instruction files) appears to exceed that on its own — and compaction cannot compact away fixed overhead, so every round re-triggers. (In longer agentic runs on the same config the model's first turns sometimes do complete and write files before the hang, so the trigger arithmetic may not be the whole mechanism — but the MCP-catalog dependency is fully reproducible either way.)

    This is a sharper trigger than the long-session framing above: no history needed, fails from message zero, bisectable by toggling one MCP server. Happy to provide the full probe logs. A guard like the one in #37584 (bounded consecutive compaction cycles → explicit ContextOverflowError) would have turned this hang into an actionable error — that PR was closed by the stale-bot with green checks; this repro may be a reason to revive it.

    Related: #22792, #27631 (large MCP tool sets + compaction, closed as duplicate).

  10. thdxr commented on Oct 11, 2026

    @thdxr
    Member

    Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.

    — from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions