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

200 lines
5.6 KiB
TypeScript

import type { BackgroundTask, LaunchInput } from "./types"
import type { QueueItem } from "./constants"
import { log } from "../../shared"
import { subagentSessions } from "../claude-code-session-state"
export class TaskStateManager {
readonly tasks: Map<string, BackgroundTask> = new Map()
readonly notifications: Map<string, BackgroundTask[]> = new Map()
readonly pendingByParent: Map<string, Set<string>> = new Map()
readonly queuesByKey: Map<string, QueueItem[]> = new Map()
readonly processingKeys: Set<string> = new Set()
readonly completionTimers: Map<string, ReturnType<typeof setTimeout>> = new Map()
getTask(id: string): BackgroundTask | undefined {
return this.tasks.get(id)
}
findBySession(sessionID: string): BackgroundTask | undefined {
for (const task of this.tasks.values()) {
if (task.sessionID === sessionID) {
return task
}
}
return undefined
}
getTasksByParentSession(sessionID: string): BackgroundTask[] {
const result: BackgroundTask[] = []
for (const task of this.tasks.values()) {
if (task.parentSessionID === sessionID) {
result.push(task)
}
}
return result
}
getAllDescendantTasks(sessionID: string): BackgroundTask[] {
const result: BackgroundTask[] = []
const directChildren = this.getTasksByParentSession(sessionID)
for (const child of directChildren) {
result.push(child)
if (child.sessionID) {
const descendants = this.getAllDescendantTasks(child.sessionID)
result.push(...descendants)
}
}
return result
}
getRunningTasks(): BackgroundTask[] {
return Array.from(this.tasks.values()).filter(t => t.status === "running")
}
getCompletedTasks(): BackgroundTask[] {
return Array.from(this.tasks.values()).filter(t => t.status !== "running")
}
hasRunningTasks(): boolean {
for (const task of this.tasks.values()) {
if (task.status === "running") return true
}
return false
}
getConcurrencyKeyFromInput(input: LaunchInput): string {
if (input.model) {
return `${input.model.providerID}/${input.model.modelID}`
}
return input.agent
}
getConcurrencyKeyFromTask(task: BackgroundTask): string {
if (task.model) {
return `${task.model.providerID}/${task.model.modelID}`
}
return task.agent
}
addTask(task: BackgroundTask): void {
this.tasks.set(task.id, task)
}
removeTask(taskId: string): void {
const task = this.tasks.get(taskId)
if (task?.sessionID) {
subagentSessions.delete(task.sessionID)
}
this.tasks.delete(taskId)
}
trackPendingTask(parentSessionID: string, taskId: string): void {
const pending = this.pendingByParent.get(parentSessionID) ?? new Set()
pending.add(taskId)
this.pendingByParent.set(parentSessionID, pending)
}
cleanupPendingByParent(task: BackgroundTask): void {
if (!task.parentSessionID) return
const pending = this.pendingByParent.get(task.parentSessionID)
if (pending) {
pending.delete(task.id)
if (pending.size === 0) {
this.pendingByParent.delete(task.parentSessionID)
}
}
}
markForNotification(task: BackgroundTask): void {
const queue = this.notifications.get(task.parentSessionID) ?? []
queue.push(task)
this.notifications.set(task.parentSessionID, queue)
}
getPendingNotifications(sessionID: string): BackgroundTask[] {
return this.notifications.get(sessionID) ?? []
}
clearNotifications(sessionID: string): void {
this.notifications.delete(sessionID)
}
clearNotificationsForTask(taskId: string): void {
for (const [sessionID, tasks] of this.notifications.entries()) {
const filtered = tasks.filter((t) => t.id !== taskId)
if (filtered.length === 0) {
this.notifications.delete(sessionID)
} else {
this.notifications.set(sessionID, filtered)
}
}
}
addToQueue(key: string, item: QueueItem): void {
const queue = this.queuesByKey.get(key) ?? []
queue.push(item)
this.queuesByKey.set(key, queue)
}
getQueue(key: string): QueueItem[] | undefined {
return this.queuesByKey.get(key)
}
removeFromQueue(key: string, taskId: string): boolean {
const queue = this.queuesByKey.get(key)
if (!queue) return false
const index = queue.findIndex(item => item.task.id === taskId)
if (index === -1) return false
queue.splice(index, 1)
if (queue.length === 0) {
this.queuesByKey.delete(key)
}
return true
}
setCompletionTimer(taskId: string, timer: ReturnType<typeof setTimeout>): void {
this.completionTimers.set(taskId, timer)
}
clearCompletionTimer(taskId: string): void {
const timer = this.completionTimers.get(taskId)
if (timer) {
clearTimeout(timer)
this.completionTimers.delete(taskId)
}
}
clearAllCompletionTimers(): void {
for (const timer of this.completionTimers.values()) {
clearTimeout(timer)
}
this.completionTimers.clear()
}
clear(): void {
this.clearAllCompletionTimers()
this.tasks.clear()
this.notifications.clear()
this.pendingByParent.clear()
this.queuesByKey.clear()
this.processingKeys.clear()
}
cancelPendingTask(taskId: string): boolean {
const task = this.tasks.get(taskId)
if (!task || task.status !== "pending") {
return false
}
const key = this.getConcurrencyKeyFromTask(task)
this.removeFromQueue(key, taskId)
task.status = "cancelled"
task.completedAt = new Date()
this.cleanupPendingByParent(task)
log("[background-agent] Cancelled pending task:", { taskId, key })
return true
}
}