import type { PluginInput } from "@opencode-ai/plugin" import type { BackgroundManager } from "../../features/background-agent" import { findNearestMessageWithFields, type ToolPermission, } from "../../features/hook-message-injector" import { log } from "../../shared/logger" import { CONTINUATION_PROMPT, DEFAULT_SKIP_AGENTS, HOOK_NAME, } from "./constants" import { getMessageDir } from "./message-directory" import { getIncompleteCount } from "./todo" import type { ResolvedMessageInfo, Todo } from "./types" import type { SessionStateStore } from "./session-state" function hasWritePermission(tools: Record | undefined): boolean { const editPermission = tools?.edit const writePermission = tools?.write return ( !tools || (editPermission !== false && editPermission !== "deny" && writePermission !== false && writePermission !== "deny") ) } export async function injectContinuation(args: { ctx: PluginInput sessionID: string backgroundManager?: BackgroundManager skipAgents?: string[] resolvedInfo?: ResolvedMessageInfo sessionStateStore: SessionStateStore }): Promise { const { ctx, sessionID, backgroundManager, skipAgents = DEFAULT_SKIP_AGENTS, resolvedInfo, sessionStateStore, } = args const state = sessionStateStore.getExistingState(sessionID) if (state?.isRecovering) { log(`[${HOOK_NAME}] Skipped injection: in recovery`, { sessionID }) return } const hasRunningBgTasks = backgroundManager ? backgroundManager.getTasksByParentSession(sessionID).some((task: { status: string }) => task.status === "running") : false if (hasRunningBgTasks) { log(`[${HOOK_NAME}] Skipped injection: background tasks running`, { sessionID }) return } let todos: Todo[] = [] try { const response = await ctx.client.session.todo({ path: { id: sessionID } }) todos = (response.data ?? response) as Todo[] } catch (error) { log(`[${HOOK_NAME}] Failed to fetch todos`, { sessionID, error: String(error) }) return } const freshIncompleteCount = getIncompleteCount(todos) if (freshIncompleteCount === 0) { log(`[${HOOK_NAME}] Skipped injection: no incomplete todos`, { sessionID }) return } let agentName = resolvedInfo?.agent let model = resolvedInfo?.model let tools = resolvedInfo?.tools if (!agentName || !model) { const messageDir = getMessageDir(sessionID) const previousMessage = messageDir ? findNearestMessageWithFields(messageDir) : null agentName = agentName ?? previousMessage?.agent model = model ?? (previousMessage?.model?.providerID && previousMessage?.model?.modelID ? { providerID: previousMessage.model.providerID, modelID: previousMessage.model.modelID, ...(previousMessage.model.variant ? { variant: previousMessage.model.variant } : {}), } : undefined) tools = tools ?? previousMessage?.tools } if (agentName && skipAgents.includes(agentName)) { log(`[${HOOK_NAME}] Skipped: agent in skipAgents list`, { sessionID, agent: agentName }) return } if (!hasWritePermission(tools)) { log(`[${HOOK_NAME}] Skipped: agent lacks write permission`, { sessionID, agent: agentName }) return } const incompleteTodos = todos.filter((todo) => todo.status !== "completed" && todo.status !== "cancelled") const todoList = incompleteTodos.map((todo) => `- [${todo.status}] ${todo.content}`).join("\n") const prompt = `${CONTINUATION_PROMPT} [Status: ${todos.length - freshIncompleteCount}/${todos.length} completed, ${freshIncompleteCount} remaining] Remaining tasks: ${todoList}` const injectionState = sessionStateStore.getExistingState(sessionID) if (injectionState) { injectionState.inFlight = true } try { log(`[${HOOK_NAME}] Injecting continuation`, { sessionID, agent: agentName, model, incompleteCount: freshIncompleteCount, }) await ctx.client.session.promptAsync({ path: { id: sessionID }, body: { agent: agentName, ...(model !== undefined ? { model } : {}), parts: [{ type: "text", text: prompt }], }, query: { directory: ctx.directory }, }) log(`[${HOOK_NAME}] Injection successful`, { sessionID }) if (injectionState) { injectionState.inFlight = false injectionState.lastInjectedAt = Date.now() injectionState.consecutiveFailures = 0 } } catch (error) { log(`[${HOOK_NAME}] Injection failed`, { sessionID, error: String(error) }) if (injectionState) { injectionState.inFlight = false injectionState.lastInjectedAt = Date.now() injectionState.consecutiveFailures += 1 } } }