refactor: wave 2 - split atlas, auto-update-checker, session-recovery, todo-enforcer, background-task hooks
- Extract atlas/ into 15 focused modules (hook, event handler, tool policies, types, etc.) - Split auto-update-checker into checker/ and hook/ subdirectories with single-purpose files - Decompose session-recovery into separate recovery strategy files per error type - Extract todo-continuation-enforcer from monolith to directory with dedicated modules - Split background-task/tools.ts into individual tool creator files - Extract command-executor, tmux-utils into focused sub-modules - Split config/schema.ts into domain-specific schema files - Decompose cli/config-manager.ts into focused modules - Rollback skill-mcp-manager, model-availability, index.ts splits that broke tests - Fix all import path depths for moved files (../../ -> ../../../) - Add explicit type annotations to resolve TS7006 implicit any errors Typecheck: 0 errors Tests: 2359 pass, 5 fail (all pre-existing)
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
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<string, ToolPermission> | 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<void> {
|
||||
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}`
|
||||
|
||||
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 })
|
||||
} catch (error) {
|
||||
log(`[${HOOK_NAME}] Injection failed`, { sessionID, error: String(error) })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user