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
200 lines
5.6 KiB
TypeScript
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
|
|
}
|
|
}
|