fix(background-agent): add post-compaction continuation + fix stale/idle race

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 <clio-agent@sisyphuslabs.ai>
This commit is contained in:
ismeth
2026-02-24 12:14:06 +01:00
committed by YeonGyu-Kim
parent e503697d92
commit 61eb0ee04a
3 changed files with 78 additions and 0 deletions

View File

@@ -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) {

View File

@@ -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()
}
}

View File

@@ -12,6 +12,7 @@ export function handleSessionIdleBackgroundEvent(args: {
findBySession: (sessionID: string) => BackgroundTask | undefined
idleDeferralTimers: Map<string, ReturnType<typeof setTimeout>>
recentlyCompactedSessions?: Set<string>
onPostCompactionIdle?: (task: BackgroundTask, sessionID: string) => void
validateSessionHasOutput: (sessionID: string) => Promise<boolean>
checkSessionTodos: (sessionID: string) => Promise<boolean>
tryCompleteTask: (task: BackgroundTask, source: string) => Promise<boolean>
@@ -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") {