From d85ec41128d744aca7c91f87bf4c910507cb4fb2 Mon Sep 17 00:00:00 2001 From: ZeusCraft10 Date: Sat, 7 Mar 2026 18:22:41 -0500 Subject: [PATCH] fix(app): abort session on synthetic continue loop detection (#777) When the LLM outputs "Suggested next steps for continuation" at the end of a work summary, OpenCode sends a synthetic "continue" prompt which triggers another summary, creating an infinite loop. The existing diagnostic code detected this (3+ synthetic continues in 60s) but only logged a warning. Now calls session.abort() on first loop detection to break the cycle. Uses a per-session Set to ensure abort fires only once per session. --- packages/app/src/app/app.tsx | 5 +++++ packages/app/src/app/context/session.ts | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/packages/app/src/app/app.tsx b/packages/app/src/app/app.tsx index af20492b6..b7698bc82 100644 --- a/packages/app/src/app/app.tsx +++ b/packages/app/src/app/app.tsx @@ -1154,6 +1154,11 @@ export default function App() { void refreshPlugins(pluginScope()); void refreshMcpServers(); }, + abortSession: async (sessionID: string) => { + const c = client(); + if (!c) return; + await abortSessionTyped(c, sessionID); + }, }); const { diff --git a/packages/app/src/app/context/session.ts b/packages/app/src/app/context/session.ts index ec7fca3c1..ec8b6dc39 100644 --- a/packages/app/src/app/context/session.ts +++ b/packages/app/src/app/context/session.ts @@ -128,6 +128,7 @@ export function createSessionStore(options: { setSseConnected: (connected: boolean) => void; markReloadRequired?: (reason: ReloadReason, trigger?: ReloadTrigger) => void; onHotReloadApplied?: () => void; + abortSession?: (sessionID: string) => Promise; }) { const sessionDebugEnabled = () => options.developerMode(); @@ -177,6 +178,7 @@ export function createSessionStore(options: { const invalidToolDetectionSet = new Set(); const syntheticContinueEventTimesBySession = new Map(); const syntheticContinueLoopLastWarnAtBySession = new Map(); + const syntheticContinueAutoAbortedSessions = new Set(); const skillPathPattern = /[\\/]\.opencode[\\/](skill|skills)[\\/]/i; const skillNamePattern = /[\\/]\.opencode[\\/](?:skill|skills)[\\/]+([^\\/]+)/i; @@ -432,6 +434,14 @@ export function createSessionStore(options: { threshold: COMPACTION_LOOP_WARN_THRESHOLD, windowMs: COMPACTION_DIAGNOSTIC_WINDOW_MS, }); + + // Abort the session to break the infinite loop (issue #777). + // Only abort once per session to avoid abort spam. + if (!syntheticContinueAutoAbortedSessions.has(sessionID) && options.abortSession) { + syntheticContinueAutoAbortedSessions.add(sessionID); + sessionWarn("compaction:synthetic-continue-loop:aborting", { sessionID }); + void options.abortSession(sessionID); + } }; const addError = (error: unknown, fallback = "Unknown error") => {