oh-my-opencode/src/features/background-agent/notify-parent-session.ts
YeonGyu-Kim e3bd43ff64 refactor(background-agent): split manager.ts into focused modules
Extract 30+ single-responsibility modules from manager.ts (1556 LOC):
- task lifecycle: task-starter, task-completer, task-canceller, task-resumer
- task queries: task-queries, task-poller, task-queue-processor
- notifications: notification-builder, notification-tracker, parent-session-notifier
- session handling: session-validator, session-output-validator, session-todo-checker
- spawner: spawner/ directory with focused spawn modules
- utilities: duration-formatter, error-classifier, message-storage-locator
- result handling: result-handler-context, background-task-completer
- shutdown: background-manager-shutdown, process-signal
2026-02-08 16:20:52 +09:00

193 lines
5.7 KiB
TypeScript

import { log } from "../../shared"
import { findNearestMessageWithFields } from "../hook-message-injector"
import { getTaskToastManager } from "../task-toast-manager"
import { TASK_CLEANUP_DELAY_MS } from "./constants"
import { formatDuration } from "./format-duration"
import { isAbortedSessionError } from "./error-classifier"
import { getMessageDir } from "./message-dir"
import { buildBackgroundTaskNotificationText } from "./notification-builder"
import type { BackgroundTask } from "./types"
import type { OpencodeClient } from "./opencode-client"
type AgentModel = { providerID: string; modelID: string }
type MessageInfo = {
agent?: string
model?: AgentModel
providerID?: string
modelID?: string
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null
}
function extractMessageInfo(message: unknown): MessageInfo {
if (!isRecord(message)) return {}
const info = message["info"]
if (!isRecord(info)) return {}
const agent = typeof info["agent"] === "string" ? info["agent"] : undefined
const modelObj = info["model"]
if (isRecord(modelObj)) {
const providerID = modelObj["providerID"]
const modelID = modelObj["modelID"]
if (typeof providerID === "string" && typeof modelID === "string") {
return { agent, model: { providerID, modelID } }
}
}
const providerID = info["providerID"]
const modelID = info["modelID"]
if (typeof providerID === "string" && typeof modelID === "string") {
return { agent, model: { providerID, modelID } }
}
return { agent }
}
export async function notifyParentSession(args: {
task: BackgroundTask
tasks: Map<string, BackgroundTask>
pendingByParent: Map<string, Set<string>>
completionTimers: Map<string, ReturnType<typeof setTimeout>>
clearNotificationsForTask: (taskId: string) => void
client: OpencodeClient
}): Promise<void> {
const { task, tasks, pendingByParent, completionTimers, clearNotificationsForTask, client } = args
const duration = formatDuration(task.startedAt ?? new Date(), task.completedAt)
log("[background-agent] notifyParentSession called for task:", task.id)
const toastManager = getTaskToastManager()
if (toastManager) {
toastManager.showCompletionToast({
id: task.id,
description: task.description,
duration,
})
}
const pendingSet = pendingByParent.get(task.parentSessionID)
if (pendingSet) {
pendingSet.delete(task.id)
if (pendingSet.size === 0) {
pendingByParent.delete(task.parentSessionID)
}
}
const allComplete = !pendingSet || pendingSet.size === 0
const remainingCount = pendingSet?.size ?? 0
const completedTasks = allComplete
? Array.from(tasks.values()).filter(
(t) =>
t.parentSessionID === task.parentSessionID &&
t.status !== "running" &&
t.status !== "pending"
)
: []
const notification = buildBackgroundTaskNotificationText({
task,
duration,
allComplete,
remainingCount,
completedTasks,
})
let agent: string | undefined = task.parentAgent
let model: AgentModel | undefined
try {
const messagesResp = await client.session.messages({
path: { id: task.parentSessionID },
})
const raw = (messagesResp as { data?: unknown }).data ?? []
const messages = Array.isArray(raw) ? raw : []
for (let i = messages.length - 1; i >= 0; i--) {
const extracted = extractMessageInfo(messages[i])
if (extracted.agent || extracted.model) {
agent = extracted.agent ?? task.parentAgent
model = extracted.model
break
}
}
} catch (error) {
if (isAbortedSessionError(error)) {
log("[background-agent] Parent session aborted, skipping notification:", {
taskId: task.id,
parentSessionID: task.parentSessionID,
})
return
}
const messageDir = getMessageDir(task.parentSessionID)
const currentMessage = messageDir ? findNearestMessageWithFields(messageDir) : null
agent = currentMessage?.agent ?? task.parentAgent
model =
currentMessage?.model?.providerID && currentMessage?.model?.modelID
? { providerID: currentMessage.model.providerID, modelID: currentMessage.model.modelID }
: undefined
}
log("[background-agent] notifyParentSession context:", {
taskId: task.id,
resolvedAgent: agent,
resolvedModel: model,
})
try {
await client.session.promptAsync({
path: { id: task.parentSessionID },
body: {
noReply: !allComplete,
...(agent !== undefined ? { agent } : {}),
...(model !== undefined ? { model } : {}),
parts: [{ type: "text", text: notification }],
},
})
log("[background-agent] Sent notification to parent session:", {
taskId: task.id,
allComplete,
noReply: !allComplete,
})
} catch (error) {
if (isAbortedSessionError(error)) {
log("[background-agent] Parent session aborted, skipping notification:", {
taskId: task.id,
parentSessionID: task.parentSessionID,
})
return
}
log("[background-agent] Failed to send notification:", error)
}
if (!allComplete) return
for (const completedTask of completedTasks) {
const taskId = completedTask.id
const existingTimer = completionTimers.get(taskId)
if (existingTimer) {
clearTimeout(existingTimer)
completionTimers.delete(taskId)
}
const timer = setTimeout(() => {
completionTimers.delete(taskId)
if (tasks.has(taskId)) {
clearNotificationsForTask(taskId)
tasks.delete(taskId)
log("[background-agent] Removed completed task from memory:", taskId)
}
}, TASK_CLEANUP_DELAY_MS)
completionTimers.set(taskId, timer)
}
}