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
This commit is contained in:
YeonGyu-Kim
2026-02-08 16:20:52 +09:00
parent 3ec0a4f5a4
commit fdcee08460
43 changed files with 2753 additions and 2071 deletions
@@ -0,0 +1,45 @@
import type { OpencodeClient } from "../constants"
import type { ConcurrencyManager } from "../concurrency"
import type { LaunchInput } from "../types"
import { log } from "../../../shared"
export async function createBackgroundSession(options: {
client: OpencodeClient
input: LaunchInput
parentDirectory: string
concurrencyManager: ConcurrencyManager
concurrencyKey: string
}): Promise<string> {
const { client, input, parentDirectory, concurrencyManager, concurrencyKey } = options
const body = {
parentID: input.parentSessionID,
title: `Background: ${input.description}`,
}
const createResult = await client.session
.create({
body,
query: {
directory: parentDirectory,
},
})
.catch((error: unknown) => {
concurrencyManager.release(concurrencyKey)
throw error
})
if (createResult.error) {
concurrencyManager.release(concurrencyKey)
throw new Error(`Failed to create background session: ${createResult.error}`)
}
if (!createResult.data?.id) {
concurrencyManager.release(concurrencyKey)
throw new Error("Failed to create background session: API returned no session ID")
}
const sessionID = createResult.data.id
log("[background-agent] Background session created", { sessionID })
return sessionID
}
@@ -0,0 +1,7 @@
import type { LaunchInput } from "../types"
export function getConcurrencyKeyFromLaunchInput(input: LaunchInput): string {
return input.model
? `${input.model.providerID}/${input.model.modelID}`
: input.agent
}
@@ -0,0 +1,21 @@
import type { OpencodeClient } from "../constants"
import { log } from "../../../shared"
export async function resolveParentDirectory(options: {
client: OpencodeClient
parentSessionID: string
defaultDirectory: string
}): Promise<string> {
const { client, parentSessionID, defaultDirectory } = options
const parentSession = await client.session
.get({ path: { id: parentSessionID } })
.catch((error: unknown) => {
log(`[background-agent] Failed to get parent session: ${error}`)
return null
})
const parentDirectory = parentSession?.data?.directory ?? defaultDirectory
log(`[background-agent] Parent dir: ${parentSession?.data?.directory}, using: ${parentDirectory}`)
return parentDirectory
}
@@ -0,0 +1,12 @@
import type { BackgroundTask } from "../types"
import type { ConcurrencyManager } from "../concurrency"
import type { OpencodeClient, OnSubagentSessionCreated } from "../constants"
export interface SpawnerContext {
client: OpencodeClient
directory: string
concurrencyManager: ConcurrencyManager
tmuxEnabled: boolean
onSubagentSessionCreated?: OnSubagentSessionCreated
onTaskError: (task: BackgroundTask, error: Error) => void
}
@@ -0,0 +1,18 @@
import { randomUUID } from "crypto"
import type { BackgroundTask, LaunchInput } from "../types"
export function createTask(input: LaunchInput): BackgroundTask {
return {
id: `bg_${randomUUID().slice(0, 8)}`,
status: "pending",
queuedAt: new Date(),
description: input.description,
prompt: input.prompt,
agent: input.agent,
parentSessionID: input.parentSessionID,
parentMessageID: input.parentMessageID,
parentModel: input.parentModel,
parentAgent: input.parentAgent,
model: input.model,
}
}
@@ -0,0 +1,91 @@
import type { BackgroundTask, ResumeInput } from "../types"
import { log, getAgentToolRestrictions } from "../../../shared"
import type { SpawnerContext } from "./spawner-context"
import { subagentSessions } from "../../claude-code-session-state"
import { getTaskToastManager } from "../../task-toast-manager"
export async function resumeTask(
task: BackgroundTask,
input: ResumeInput,
ctx: Pick<SpawnerContext, "client" | "concurrencyManager" | "onTaskError">
): Promise<void> {
const { client, concurrencyManager, onTaskError } = ctx
if (!task.sessionID) {
throw new Error(`Task has no sessionID: ${task.id}`)
}
if (task.status === "running") {
log("[background-agent] Resume skipped - task already running:", {
taskId: task.id,
sessionID: task.sessionID,
})
return
}
const concurrencyKey = task.concurrencyGroup ?? task.agent
await concurrencyManager.acquire(concurrencyKey)
task.concurrencyKey = concurrencyKey
task.concurrencyGroup = concurrencyKey
task.status = "running"
task.completedAt = undefined
task.error = undefined
task.parentSessionID = input.parentSessionID
task.parentMessageID = input.parentMessageID
task.parentModel = input.parentModel
task.parentAgent = input.parentAgent
task.startedAt = new Date()
task.progress = {
toolCalls: task.progress?.toolCalls ?? 0,
lastUpdate: new Date(),
}
subagentSessions.add(task.sessionID)
const toastManager = getTaskToastManager()
if (toastManager) {
toastManager.addTask({
id: task.id,
description: task.description,
agent: task.agent,
isBackground: true,
})
}
log("[background-agent] Resuming task:", { taskId: task.id, sessionID: task.sessionID })
log("[background-agent] Resuming task - calling prompt (fire-and-forget) with:", {
sessionID: task.sessionID,
agent: task.agent,
model: task.model,
promptLength: input.prompt.length,
})
const resumeModel = task.model
? { providerID: task.model.providerID, modelID: task.model.modelID }
: undefined
const resumeVariant = task.model?.variant
client.session
.promptAsync({
path: { id: task.sessionID },
body: {
agent: task.agent,
...(resumeModel ? { model: resumeModel } : {}),
...(resumeVariant ? { variant: resumeVariant } : {}),
tools: {
...getAgentToolRestrictions(task.agent),
task: false,
call_omo_agent: true,
question: false,
},
parts: [{ type: "text", text: input.prompt }],
},
})
.catch((error: unknown) => {
log("[background-agent] resume prompt error:", error)
onTaskError(task, error instanceof Error ? error : new Error(String(error)))
})
}
@@ -0,0 +1,94 @@
import type { QueueItem } from "../constants"
import { log, getAgentToolRestrictions, promptWithModelSuggestionRetry } from "../../../shared"
import { subagentSessions } from "../../claude-code-session-state"
import { getTaskToastManager } from "../../task-toast-manager"
import { createBackgroundSession } from "./background-session-creator"
import { getConcurrencyKeyFromLaunchInput } from "./concurrency-key-from-launch-input"
import { resolveParentDirectory } from "./parent-directory-resolver"
import type { SpawnerContext } from "./spawner-context"
import { maybeInvokeTmuxCallback } from "./tmux-callback-invoker"
export async function startTask(item: QueueItem, ctx: SpawnerContext): Promise<void> {
const { task, input } = item
const { client, directory, concurrencyManager, tmuxEnabled, onSubagentSessionCreated, onTaskError } = ctx
log("[background-agent] Starting task:", {
taskId: task.id,
agent: input.agent,
model: input.model,
})
const concurrencyKey = getConcurrencyKeyFromLaunchInput(input)
const parentDirectory = await resolveParentDirectory({
client,
parentSessionID: input.parentSessionID,
defaultDirectory: directory,
})
const sessionID = await createBackgroundSession({
client,
input,
parentDirectory,
concurrencyManager,
concurrencyKey,
})
subagentSessions.add(sessionID)
await maybeInvokeTmuxCallback({
onSubagentSessionCreated,
tmuxEnabled,
sessionID,
parentID: input.parentSessionID,
title: input.description,
})
task.status = "running"
task.startedAt = new Date()
task.sessionID = sessionID
task.progress = {
toolCalls: 0,
lastUpdate: new Date(),
}
task.concurrencyKey = concurrencyKey
task.concurrencyGroup = concurrencyKey
log("[background-agent] Launching task:", { taskId: task.id, sessionID, agent: input.agent })
const toastManager = getTaskToastManager()
if (toastManager) {
toastManager.updateTask(task.id, "running")
}
log("[background-agent] Calling prompt (fire-and-forget) for launch with:", {
sessionID,
agent: input.agent,
model: input.model,
hasSkillContent: !!input.skillContent,
promptLength: input.prompt.length,
})
const launchModel = input.model
? { providerID: input.model.providerID, modelID: input.model.modelID }
: undefined
const launchVariant = input.model?.variant
promptWithModelSuggestionRetry(client, {
path: { id: sessionID },
body: {
agent: input.agent,
...(launchModel ? { model: launchModel } : {}),
...(launchVariant ? { variant: launchVariant } : {}),
system: input.skillContent,
tools: {
...getAgentToolRestrictions(input.agent),
task: false,
call_omo_agent: true,
question: false,
},
parts: [{ type: "text", text: input.prompt }],
},
}).catch((error: unknown) => {
log("[background-agent] promptAsync error:", error)
onTaskError(task, error instanceof Error ? error : new Error(String(error)))
})
}
@@ -0,0 +1,40 @@
import { setTimeout } from "timers/promises"
import type { OnSubagentSessionCreated } from "../constants"
import { TMUX_CALLBACK_DELAY_MS } from "../constants"
import { log } from "../../../shared"
import { isInsideTmux } from "../../../shared/tmux"
export async function maybeInvokeTmuxCallback(options: {
onSubagentSessionCreated?: OnSubagentSessionCreated
tmuxEnabled: boolean
sessionID: string
parentID: string
title: string
}): Promise<void> {
const { onSubagentSessionCreated, tmuxEnabled, sessionID, parentID, title } = options
log("[background-agent] tmux callback check", {
hasCallback: !!onSubagentSessionCreated,
tmuxEnabled,
isInsideTmux: isInsideTmux(),
sessionID,
parentID,
})
if (!onSubagentSessionCreated || !tmuxEnabled || !isInsideTmux()) {
log("[background-agent] SKIP tmux callback - conditions not met")
return
}
log("[background-agent] Invoking tmux callback NOW", { sessionID })
await onSubagentSessionCreated({
sessionID,
parentID,
title,
}).catch((error: unknown) => {
log("[background-agent] Failed to spawn tmux pane:", error)
})
log("[background-agent] tmux callback completed, waiting")
await setTimeout(TMUX_CALLBACK_DELAY_MS)
}