diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index f64065491..545cb329a 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -44,6 +44,8 @@ import { tryFallbackRetry } from "./fallback-retry-handler" import { registerManagerForCleanup, unregisterManagerForCleanup } from "./process-cleanup" import { isCompactionAgent, findNearestMessageExcludingCompaction } from "./compaction-aware-message-resolver" import { handleSessionIdleBackgroundEvent } from "./session-idle-event-handler" +import { sendPostCompactionContinuation } from "./post-compaction-continuation" +import { COUNCIL_MEMBER_KEY_PREFIX } from "../../agents/builtin-agents/council-member-agents" import { MESSAGE_STORAGE } from "../hook-message-injector" import { join } from "node:path" import { pruneStaleTasksAndNotifications } from "./task-poller" @@ -766,6 +768,11 @@ export class BackgroundManager { findBySession: (id) => this.findBySession(id), idleDeferralTimers: this.idleDeferralTimers, recentlyCompactedSessions: this.recentlyCompactedSessions, + onPostCompactionIdle: (t, sid) => { + if (t.agent?.startsWith(COUNCIL_MEMBER_KEY_PREFIX)) { + sendPostCompactionContinuation(this.client, t, sid) + } + }, validateSessionHasOutput: (id) => this.validateSessionHasOutput(id), checkSessionTodos: (id) => this.checkSessionTodos(id), tryCompleteTask: (task, source) => this.tryCompleteTask(task, source), @@ -1494,6 +1501,12 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea continue } + // Refresh lastUpdate so the next poll's stale check doesn't kill + // the task while we're awaiting async validation + if (task.progress) { + task.progress.lastUpdate = new Date() + } + // Edge guard: Validate session has actual output before completing const hasValidOutput = await this.validateSessionHasOutput(sessionID) if (!hasValidOutput) { 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") {