You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Plan agent loops forever after auto-compaction (opencode run never exits) #62628
With the plan agent, once auto-compaction triggers, every compaction is followed by the "You are in Plan mode…" synthetic reminder, and the model answers again. That triggers another compaction, and so on. opencode run --agent plan never exits.
In 150 s, sessions had 2, 3 and 4 compaction/reminder/answer cycles after the model's first ok. The same prompt with a large context limit (no compaction) ends normally, and build with the small limit ends normally. Long plan sessions that reach compaction are likely affected in the TUI too, though I only tested run.
Plugins
none (minimal config)
OpenCode version
2.2.6
Steps to reproduce
Custom provider model with "limit": { "context": 32768, "output": 2048 } (small enough that the system prompt triggers auto-compaction).
opencode run --agent plan "reply with exactly: ok": hangs (3 of 3); the session shows repeated compaction → synthetic (plan reminder) → assistant entries.
Same with "context": 262144: exits normally (3 of 3).
Thanks for the detailed report! I reproduced this on 2.2.6, and it still happens on the latest dev (c33eaa170).
Setup: a local OpenAI-compatible mock model (no real provider), with the model's context limit set small enough that the system prompt alone triggers auto-compaction.
opencode run --agent plan "reply with exactly: ok" never exits. In 40 seconds the mock got about 3,000 requests, alternating compaction and answer, until timeout killed it.
--agent build with the same small limit exits after one compaction and one answer.
--agent plan with a large context limit exits normally.
The script below reproduces it with a mock model, so no credentials are needed. The likely cause is that the Plan-mode reminder is added back after every compaction, which seems to start another turn that compacts again. That has not been confirmed yet.
Related: #52697 and #48827 (earlier auto-compaction loops, not specific to the plan agent) and #58395 (when the Plan reminder is saved).
Reproduction script (repro.sh)
#!/usr/bin/env bash# Repro for #62628: `opencode run --agent plan` never exits once auto-compaction triggers.# Uses a local OpenAI-compatible mock model (no credentials). Requires bun.# OC="opencode" by default; set OC to another command (e.g. a source checkout) to test it.# For a source checkout set OC to its CLI command and OC_SERVE=1.# CONTEXT (default 3000) is the model's context limit; small enough that the first request auto-compacts.set -u
OC=${OC:-opencode}
AGENT=${AGENT:-plan}
CONTEXT=${CONTEXT:-3000}
PORT=${PORT:-4567}
WORK=$(mktemp -d)
mkdir -p "$WORK/proj""$WORK/home"
cat >"$WORK/mock.ts"<<'EOF'let n = 0const SUMMARY = "## Objective\n- Reply with exactly: ok\n\n## Requirements\n- (none)\n\n## Decisions\n- (none)\n\n## Work State\n### Completed\n- (none)\n\n### Active\n- Reply ok\n\n### Blocked\n- (none)\n\n## Next Move\n1. Reply ok\n\n## Relevant Files\n- (none)\n\n## Important Context\n- (none)\n"Bun.serve({ port: Number(process.env.PORT), async fetch(req) { if (!new URL(req.url).pathname.endsWith("/chat/completions")) return new Response("{}", { status: 404 }) const body: any = await req.json() n++ const size = JSON.stringify(body.messages).length const last = body.messages.at(-1) const lastText = typeof last.content === "string" ? last.content : (last.content ?? []).map((p: any) => p.text ?? "").join("") const title = /first message of the conversation/.test(lastText) const compaction = /^(You MUST summarize|Update the existing checkpoint|The previous response did not fill in the required summary template)/.test(lastText) const kind = title ? "title" : compaction ? "COMPACTION" : "answer" console.log(`request #${n} ${kind}`) const text = compaction ? SUMMARY : title ? "Reply ok" : "ok" const enc = new TextEncoder() const base = { id: "c" + n, object: "chat.completion.chunk", created: 0, model: "m" } const chunk = (o: any) => enc.encode(`data: ${JSON.stringify({ ...base, ...o })}\n\n`) return new Response(new ReadableStream({ start(c) { c.enqueue(chunk({ choices: [{ index: 0, delta: { role: "assistant", content: text } }] })) c.enqueue(chunk({ choices: [{ index: 0, delta: {}, finish_reason: "stop" }] })) c.enqueue(chunk({ choices: [], usage: { prompt_tokens: Math.ceil(size / 4), completion_tokens: 2, total_tokens: Math.ceil(size / 4) + 2 } })) c.enqueue(enc.encode("data: [DONE]\n\n")); c.close() } }), { headers: { "content-type": "text/event-stream" } }) },})EOF
PORT=$PORT bun "$WORK/mock.ts">"$WORK/mock.log"2>&1&
MOCK=$!trap'kill $MOCK 2>/dev/null' EXIT
sleep 1
cat >"$WORK/proj/opencode.json"<<EOF{ "\$schema": "https://opencode.ai/config.json", "model": "mock/m", "provider": { "mock": { "npm": "@ai-sdk/openai-compatible", "options": { "baseURL": "http://127.0.0.1:$PORT/v1", "apiKey": "x" }, "models": { "m": { "name": "m", "limit": { "context": $CONTEXT, "output": 2048 } } } } }}EOFcd"$WORK/proj"&& git init -q
echo"== $($OC --version) agent=$AGENT context=$CONTEXT"if [ -n"${OC_SERVE:-}" ];then# Source checkouts: start the server separately (the --standalone child lacks bun's preload/conditions).
OPENCODE_PASSWORD=repro HOME="$WORK/home"$OC serve --port $((PORT+1))>"$WORK/serve.log"2>&1&
SERVE=$!;trap'kill $MOCK $SERVE 2>/dev/null' EXIT; sleep 8
CONN="--server http://127.0.0.1:$((PORT+1))"else
CONN="--standalone"fi
OPENCODE_PASSWORD=repro HOME="$WORK/home" timeout 40 $OC run $CONN --agent "$AGENT""reply with exactly: ok">"$WORK/run.out"2>&1
code=$?; [ -n"${SHOW_OUT:-}" ] && tail -c 2000 "$WORK/run.out"
[ $code= 124 ] &&echo"RESULT: opencode run did NOT exit within 40s (killed)"||echo"RESULT: opencode run exited with code $code"echo"== model requests ($(grep -c '^request'"$WORK/mock.log") total), first 15:"
head -15 "$WORK/mock.log"
Root cause, confirmed locally with the mock-model repro from @opencodejr[bot]'s comment (latest dev, ~2,000 requests in 40s before the fix): the Plan plugin's context hook persists the re-injected reminder via ctx.session.synthetic, which admits a pending steer item; the next idle boundary promotes it into a fresh turn answering the reminder, and with auto-compaction stripping the reminder every turn the cycle never ends.
The fix records the reminder straight onto the durable log instead (same path as instruction-file injection), so it stays in history and in the current request without spawning answer turns. Repro now exits after 3 requests; plan plugin tests and typecheck pass.
Thanks for the quick fix in #62633. A cheap backstop for this whole class: cap compactions per turn (say 3) and end the turn with an error after that. A turn that compacts more than a few times is not making progress.
Activity
opencodejr commentedon Nov 7, 2026
Thanks for the detailed report! I reproduced this on 2.2.6, and it still happens on the latest
dev(c33eaa170).Setup: a local OpenAI-compatible mock model (no real provider), with the model's context limit set small enough that the system prompt alone triggers auto-compaction.
opencode run --agent plan "reply with exactly: ok"never exits. In 40 seconds the mock got about 3,000 requests, alternating compaction and answer, untiltimeoutkilled it.--agent buildwith the same small limit exits after one compaction and one answer.--agent planwith a large context limit exits normally.The script below reproduces it with a mock model, so no credentials are needed. The likely cause is that the Plan-mode reminder is added back after every compaction, which seems to start another turn that compacts again. That has not been confirmed yet.
Related: #52697 and #48827 (earlier auto-compaction loops, not specific to the plan agent) and #58395 (when the Plan reminder is saved).
Reproduction script (repro.sh)
Run it:
bash repro.shtomwangcn commentedon Nov 7, 2026
I've picked this up and opened #62633.
Root cause, confirmed locally with the mock-model repro from @opencodejr[bot]'s comment (latest dev, ~2,000 requests in 40s before the fix): the Plan plugin's context hook persists the re-injected reminder via
ctx.session.synthetic, which admits a pending steer item; the next idle boundary promotes it into a fresh turn answering the reminder, and with auto-compaction stripping the reminder every turn the cycle never ends.The fix records the reminder straight onto the durable log instead (same path as instruction-file injection), so it stays in history and in the current request without spawning answer turns. Repro now exits after 3 requests; plan plugin tests and typecheck pass.
vijaym2k6 commentedon Nov 8, 2026
Thanks for the quick fix in #62633. A cheap backstop for this whole class: cap compactions per turn (say 3) and end the turn with an error after that. A turn that compacts more than a few times is not making progress.