Files
oh-my-opencode/src/features/background-agent/state.ts
T
YeonGyu-Kim f146aeff0f refactor: major codebase cleanup - BDD comments, file splitting, bug fixes (#1350)
* style(tests): normalize BDD comments from '// #given' to '// given'

- Replace 4,668 Python-style BDD comments across 107 test files
- Patterns changed: // #given -> // given, // #when -> // when, // #then -> // then
- Also handles no-space variants: //#given -> // given

* fix(rules-injector): prefer output.metadata.filePath over output.title

- Extract file path resolution to dedicated output-path.ts module
- Prefer metadata.filePath which contains actual file path
- Fall back to output.title only when metadata unavailable
- Fixes issue where rules weren't injected when tool output title was a label

* feat(slashcommand): add optional user_message parameter

- Add user_message optional parameter for command arguments
- Model can now call: command='publish' user_message='patch'
- Improves error messages with clearer format guidance
- Helps LLMs understand correct parameter usage

* feat(hooks): restore compaction-context-injector hook

- Restore hook deleted in cbbc7bd0 for session compaction context
- Injects 7 mandatory sections: User Requests, Final Goal, Work Completed,
  Remaining Tasks, Active Working Context, MUST NOT Do, Agent Verification State
- Re-register in hooks/index.ts and main plugin entry

* refactor(background-agent): split manager.ts into focused modules

- Extract constants.ts for TTL values and internal types (52 lines)
- Extract state.ts for TaskStateManager class (204 lines)
- Extract spawner.ts for task creation logic (244 lines)
- Extract result-handler.ts for completion handling (265 lines)
- Reduce manager.ts from 1377 to 755 lines (45% reduction)
- Maintain backward compatible exports

* refactor(agents): split prometheus-prompt.ts into subdirectory

- Move 1196-line prometheus-prompt.ts to prometheus/ subdirectory
- Organize prompt sections into separate files for maintainability
- Update agents/index.ts exports

* refactor(delegate-task): split tools.ts into focused modules

- Extract categories.ts for category definitions and routing
- Extract executor.ts for task execution logic
- Extract helpers.ts for utility functions
- Extract prompt-builder.ts for prompt construction
- Reduce tools.ts complexity with cleaner separation of concerns

* refactor(builtin-skills): split skills.ts into individual skill files

- Move each skill to dedicated file in skills/ subdirectory
- Create barrel export for backward compatibility
- Improve maintainability with focused skill modules

* chore: update import paths and lockfile

- Update prometheus import path after refactor
- Update bun.lock

* fix(tests): complete BDD comment normalization

- Fix remaining #when/#then patterns missed by initial sed
- Affected: state.test.ts, events.test.ts

---------

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
2026-02-01 16:47:50 +09:00

205 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
}
}