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") => {