From dc04733bee2405e1437349de3c5aeb22a6a9f342 Mon Sep 17 00:00:00 2001 From: ismeth Date: Tue, 24 Feb 2026 12:14:06 +0100 Subject: [PATCH] fix(background-agent): add post-compaction continuation + fix stale/idle race MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract sendPostCompactionContinuation to dedicated file — council members now resume after compaction instead of silently failing. Refresh lastUpdate before async validation in both idle handler and polling path to prevent stale timeout from racing with completion detection. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- .../post-compaction-continuation.ts | 55 +++++++++++++++++++ .../session-idle-event-handler.ts | 10 ++++ 2 files changed, 65 insertions(+) create mode 100644 src/features/background-agent/post-compaction-continuation.ts diff --git a/src/features/background-agent/post-compaction-continuation.ts b/src/features/background-agent/post-compaction-continuation.ts new file mode 100644 index 000000000..934fd23a5 --- /dev/null +++ b/src/features/background-agent/post-compaction-continuation.ts @@ -0,0 +1,55 @@ +import type { PluginInput } from "@opencode-ai/plugin" +import type { BackgroundTask } from "./types" +import { + log, + getAgentToolRestrictions, + createInternalAgentTextPart, +} from "../../shared" +import { setSessionTools } from "../../shared/session-tools-store" + +type OpencodeClient = PluginInput["client"] + +const CONTINUATION_PROMPT = + "Your session was compacted (context summarized). Continue your analysis from where you left off. Report your findings when done." + +export function sendPostCompactionContinuation( + client: OpencodeClient, + task: BackgroundTask, + sessionID: string, +): void { + if (task.status !== "running") return + + const resumeModel = task.model + ? { providerID: task.model.providerID, modelID: task.model.modelID } + : undefined + const resumeVariant = task.model?.variant + + client.session.promptAsync({ + path: { id: sessionID }, + body: { + agent: task.agent, + ...(resumeModel ? { model: resumeModel } : {}), + ...(resumeVariant ? { variant: resumeVariant } : {}), + tools: (() => { + const tools = { + task: false, + call_omo_agent: true, + question: false, + ...getAgentToolRestrictions(task.agent), + } + setSessionTools(sessionID, tools) + return tools + })(), + parts: [createInternalAgentTextPart(CONTINUATION_PROMPT)], + }, + }).catch((error) => { + log("[background-agent] Post-compaction continuation error:", { + taskId: task.id, + error: String(error), + }) + }) + + if (task.progress) { + task.progress.lastUpdate = new Date() + } +} diff --git a/src/features/background-agent/session-idle-event-handler.ts b/src/features/background-agent/session-idle-event-handler.ts index a5b1c087a..70c0e6530 100644 --- a/src/features/background-agent/session-idle-event-handler.ts +++ b/src/features/background-agent/session-idle-event-handler.ts @@ -12,6 +12,7 @@ export function handleSessionIdleBackgroundEvent(args: { findBySession: (sessionID: string) => BackgroundTask | undefined idleDeferralTimers: Map> recentlyCompactedSessions?: Set + onPostCompactionIdle?: (task: BackgroundTask, sessionID: string) => void validateSessionHasOutput: (sessionID: string) => Promise checkSessionTodos: (sessionID: string) => Promise tryCompleteTask: (task: BackgroundTask, source: string) => Promise @@ -22,6 +23,7 @@ export function handleSessionIdleBackgroundEvent(args: { findBySession, idleDeferralTimers, recentlyCompactedSessions, + onPostCompactionIdle, validateSessionHasOutput, checkSessionTodos, tryCompleteTask, @@ -37,6 +39,7 @@ export function handleSessionIdleBackgroundEvent(args: { if (recentlyCompactedSessions?.has(sessionID)) { recentlyCompactedSessions.delete(sessionID) log("[background-agent] Skipping post-compaction session.idle:", { taskId: task.id, sessionID }) + onPostCompactionIdle?.(task, sessionID) return } @@ -63,6 +66,13 @@ export function handleSessionIdleBackgroundEvent(args: { return } + // Refresh lastUpdate to prevent stale timeout from racing with this async validation. + // Without this, checkAndInterruptStaleTasks can kill the task synchronously + // while validateSessionHasOutput is still awaiting an API response. + if (task.progress) { + task.progress.lastUpdate = new Date() + } + validateSessionHasOutput(sessionID) .then(async (hasValidOutput) => { if (task.status !== "running") {