/**
 * @fileoverview Background Task Tracker for Claude Code sessions
 *
 * This module tracks background tasks (subagents) spawned by Claude Code
 * during session execution. It parses both JSON messages and terminal output
 * to detect when tasks are created, updated, and completed.
 *
 * ## Task Hierarchy
 *
 * Tasks can be nested (parent-child relationships) when Claude spawns
 * a subagent from within another subagent. The tracker maintains a stack
 * to track nesting and a tree structure for visualization.
 *
 * ## Detection Methods
 *
 * 1. **JSON Messages**: Parses `tool_use` blocks for Task tool invocations
 *    and `tool_result` blocks for completion
 * 2. **Terminal Output**: Fallback pattern matching for launch/complete messages
 *
 * @module task-tracker
 */
import { EventEmitter } from 'node:events';
import { assertNever } from './utils/index.js';
import { STALE_DATA_MAX_AGE_MS } from './config/server-timing.js';
// ========== Configuration Constants ==========
/**
 * Maximum number of completed tasks to keep in memory.
 * Oldest completed tasks are removed when this limit is exceeded.
 */
const MAX_COMPLETED_TASKS = 100;
/**
 * Maximum age for pending tool uses (in milliseconds).
 * Entries older than this are cleaned up to prevent unbounded growth.
 * Default: 1 hour
 */
const PENDING_TOOL_USE_MAX_AGE_MS = STALE_DATA_MAX_AGE_MS;
/**
 * Maximum number of pending tool uses to allow.
 * Prevents unbounded growth if tool_results never arrive.
 */
const MAX_PENDING_TOOL_USES = 100;
// ========== Pre-compiled Regex Patterns ==========
/**
 * Patterns that indicate a new task/agent is being launched.
 * Used as fallback when JSON parsing doesn't capture the launch.
 * Capture group 1: Agent/task type name
 */
const LAUNCH_PATTERNS = [/Launching\s+(\w+)\s+agent/i, /Starting\s+(\w+)\s+task/i, /Spawning\s+(\w+)\s+agent/i];
/**
 * Patterns that indicate a task has completed.
 * Used as fallback when JSON parsing doesn't capture the result.
 */
const COMPLETE_PATTERNS = [/Task\s+completed/i, /Agent\s+finished/i, /Background\s+task\s+done/i];
/**
 * Events emitted by TaskTracker.
 *
 * @event taskCreated - New task detected and added
 * @event taskUpdated - Task state changed (rarely used)
 * @event taskCompleted - Task finished successfully
 * @event taskFailed - Task finished with error
 */
/**
 * TaskTracker - Detects and tracks background tasks in Claude Code sessions.
 *
 * ## How It Works
 *
 * Claude Code outputs JSON messages when executing. When it spawns a subagent
 * via the Task tool, we see:
 *
 * 1. `tool_use` block with `name: "Task"` and input parameters
 * 2. ... task execution output ...
 * 3. `tool_result` block with the result or error
 *
 * The tracker maintains:
 * - A map of all tasks by ID
 * - A stack of currently running tasks (for nesting)
 * - Parent-child relationships between tasks
 *
 * ## Usage
 *
 * ```typescript
 * const tracker = new TaskTracker();
 *
 * tracker.on('taskCreated', (task) => {
 *   console.log(`New task: ${task.description}`);
 * });
 *
 * tracker.on('taskCompleted', (task) => {
 *   console.log(`Task done: ${task.id}`);
 * });
 *
 * // Feed in Claude messages
 * tracker.processMessage(claudeJsonMessage);
 *
 * // Or terminal output as fallback
 * tracker.processTerminalOutput(ptyData);
 * ```
 *
 * @extends EventEmitter
 */
export class TaskTracker extends EventEmitter {
    /** Map of task ID to task object */
    tasks = new Map();
    /** Stack of active task IDs for tracking nesting depth */
    taskStack = [];
    /** Pending tool_use blocks waiting for results (with timestamp for cleanup) */
    pendingToolUses = new Map();
    /**
     * Creates a new TaskTracker instance.
     */
    constructor() {
        super();
    }
    /**
     * Process a Claude JSON message to detect task events.
     *
     * Looks for `tool_use` blocks with `name: "Task"` and `tool_result` blocks
     * to track task lifecycle.
     *
     * @param msg - Parsed Claude JSON message object
     * @fires taskCreated - When a new task is detected
     * @fires taskCompleted - When a task finishes successfully
     * @fires taskFailed - When a task finishes with error
     */
    processMessage(msg) {
        if (!msg || !msg.message?.content)
            return;
        for (const block of msg.message.content) {
            if (block.type === 'tool_use' && block.name === 'Task') {
                this.handleTaskToolUse(block);
            }
            else if (block.type === 'tool_result') {
                this.handleToolResult(block);
            }
        }
    }
    /**
     * Process raw terminal output to detect task patterns.
     *
     * This is a fallback for when JSON parsing doesn't capture everything.
     * Uses pre-compiled patterns to detect launch and completion messages.
     *
     * Note: May create duplicate tasks in some cases; deduplication is
     * handled by checking for existing running tasks of the same type.
     *
     * @param data - Raw terminal output string
     * @fires taskCreated - When a launch pattern is matched
     * @fires taskCompleted - When a complete pattern is matched
     */
    processTerminalOutput(data) {
        // Detect task launch patterns in terminal output
        // Claude Code shows things like "Launching explore agent..." or similar
        for (const pattern of LAUNCH_PATTERNS) {
            const match = data.match(pattern);
            if (match) {
                // This is a heuristic detection - might create duplicate tasks
                // but we dedupe by checking if we already have a running task of this type
                const agentType = match[1].toLowerCase();
                // Optimize: use iterator directly instead of Array.from
                let existingRunning = false;
                for (const task of this.tasks.values()) {
                    if (task.subagentType === agentType && task.status === 'running') {
                        existingRunning = true;
                        break;
                    }
                }
                if (!existingRunning) {
                    this.createTaskFromTerminal(agentType, data);
                }
            }
        }
        // Detect task completion patterns
        for (const pattern of COMPLETE_PATTERNS) {
            if (pattern.test(data)) {
                // Complete the most recent running task
                const runningTask = this.getMostRecentRunningTask();
                if (runningTask) {
                    this.completeTask(runningTask.id);
                }
            }
        }
    }
    /**
     * Handle a tool_use block for the Task tool.
     * Creates a new task and pushes it onto the stack.
     *
     * @param block - The tool_use content block
     * @fires taskCreated
     */
    handleTaskToolUse(block) {
        const toolUseId = block.id;
        const params = block.input || {};
        const description = params.description || params.prompt?.substring(0, 50) || 'Background task';
        const subagentType = params.subagent_type || 'general';
        // Determine parent (current top of stack or null)
        const parentId = this.taskStack.length > 0 ? this.taskStack[this.taskStack.length - 1] : null;
        // Store pending tool use - task starts when we see activity
        this.pendingToolUses.set(toolUseId, {
            description,
            subagentType,
            parentId,
            createdAt: Date.now(),
        });
        // Clean up old pending entries to prevent unbounded growth
        this.cleanupOldPendingToolUses();
        // Create the task immediately
        const task = {
            id: toolUseId,
            parentId,
            description,
            subagentType,
            status: 'running',
            startTime: Date.now(),
            children: [],
        };
        this.tasks.set(toolUseId, task);
        this.taskStack.push(toolUseId);
        // Update parent's children list
        if (parentId) {
            const parent = this.tasks.get(parentId);
            if (parent) {
                parent.children.push(toolUseId);
            }
        }
        this.emit('taskCreated', task);
    }
    /**
     * Handle a tool_result block.
     * Marks the corresponding task as completed or failed.
     *
     * @param block - The tool_result content block
     * @fires taskCompleted - If result is success
     * @fires taskFailed - If result is error
     */
    handleToolResult(block) {
        const toolUseId = block.tool_use_id;
        const task = this.tasks.get(toolUseId);
        if (task) {
            task.status = block.is_error ? 'failed' : 'completed';
            task.endTime = Date.now();
            task.output = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
            // Remove from stack
            const stackIndex = this.taskStack.indexOf(toolUseId);
            if (stackIndex !== -1) {
                this.taskStack.splice(stackIndex, 1);
            }
            if (block.is_error) {
                this.emit('taskFailed', task, task.output || 'Unknown error');
            }
            else {
                this.emit('taskCompleted', task);
            }
            // Clean up old completed tasks to prevent unbounded growth
            this.cleanupCompletedTasks();
        }
        // Clean up pending
        this.pendingToolUses.delete(toolUseId);
    }
    /**
     * Remove old pending tool uses that never received results.
     * Prevents unbounded growth if tool_results never arrive.
     */
    cleanupOldPendingToolUses() {
        const now = Date.now();
        const toDelete = [];
        // Remove entries older than PENDING_TOOL_USE_MAX_AGE_MS
        for (const [id, entry] of this.pendingToolUses) {
            if (now - entry.createdAt > PENDING_TOOL_USE_MAX_AGE_MS) {
                toDelete.push(id);
            }
        }
        // If still over limit after age-based cleanup, remove oldest entries
        if (this.pendingToolUses.size - toDelete.length > MAX_PENDING_TOOL_USES) {
            const entries = Array.from(this.pendingToolUses.entries())
                .filter(([id]) => !toDelete.includes(id))
                .sort((a, b) => a[1].createdAt - b[1].createdAt);
            const removeCount = entries.length - MAX_PENDING_TOOL_USES;
            for (let i = 0; i < removeCount; i++) {
                toDelete.push(entries[i][0]);
            }
        }
        for (const id of toDelete) {
            this.pendingToolUses.delete(id);
        }
    }
    /**
     * Remove old completed/failed tasks when exceeding the limit
     * Keeps running tasks and the most recent completed tasks
     */
    cleanupCompletedTasks() {
        const completedTasks = [];
        // Collect all completed/failed tasks
        for (const task of this.tasks.values()) {
            if (task.status === 'completed' || task.status === 'failed') {
                completedTasks.push(task);
            }
        }
        // If under limit, no cleanup needed
        if (completedTasks.length <= MAX_COMPLETED_TASKS) {
            return;
        }
        // Sort by end time (oldest first)
        completedTasks.sort((a, b) => (a.endTime || 0) - (b.endTime || 0));
        // Remove oldest tasks beyond the limit
        const toRemove = completedTasks.slice(0, completedTasks.length - MAX_COMPLETED_TASKS);
        for (const task of toRemove) {
            // Remove from parent's children list if applicable
            if (task.parentId) {
                const parent = this.tasks.get(task.parentId);
                if (parent) {
                    const childIndex = parent.children.indexOf(task.id);
                    if (childIndex !== -1) {
                        parent.children.splice(childIndex, 1);
                    }
                }
            }
            this.tasks.delete(task.id);
        }
    }
    /**
     * Create a task from terminal pattern detection.
     * Used as fallback when JSON messages aren't available.
     *
     * @param agentType - Type of agent detected
     * @param context - Terminal context for debugging
     * @fires taskCreated
     */
    createTaskFromTerminal(agentType, _context) {
        const taskId = `terminal-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
        const parentId = this.taskStack.length > 0 ? this.taskStack[this.taskStack.length - 1] : null;
        const task = {
            id: taskId,
            parentId,
            description: `${agentType} agent`,
            subagentType: agentType,
            status: 'running',
            startTime: Date.now(),
            children: [],
        };
        this.tasks.set(taskId, task);
        this.taskStack.push(taskId);
        if (parentId) {
            const parent = this.tasks.get(parentId);
            if (parent) {
                parent.children.push(taskId);
            }
        }
        this.emit('taskCreated', task);
    }
    /**
     * Mark a task as completed.
     *
     * @param taskId - ID of task to complete
     * @fires taskCompleted - If task was running
     */
    completeTask(taskId) {
        const task = this.tasks.get(taskId);
        if (task && task.status === 'running') {
            task.status = 'completed';
            task.endTime = Date.now();
            const stackIndex = this.taskStack.indexOf(taskId);
            if (stackIndex !== -1) {
                this.taskStack.splice(stackIndex, 1);
            }
            this.emit('taskCompleted', task);
        }
    }
    /**
     * Get the most recently started running task.
     * Returns the task at the top of the stack (most nested).
     *
     * @returns Most recent running task, or undefined if none
     */
    getMostRecentRunningTask() {
        // Return the task at the top of the stack
        if (this.taskStack.length > 0) {
            const taskId = this.taskStack[this.taskStack.length - 1];
            return this.tasks.get(taskId);
        }
        return undefined;
    }
    /**
     * Get root-level tasks as a list.
     *
     * Child tasks can be accessed via the `children` array on each task.
     * Use this for displaying a task tree in the UI.
     *
     * @returns Array of tasks without parents (root level)
     */
    getTaskTree() {
        const rootTasks = [];
        for (const task of this.tasks.values()) {
            if (!task.parentId) {
                rootTasks.push(task);
            }
        }
        return rootTasks;
    }
    /**
     * Lightweight task tree for SSE broadcasts — strips large `output` strings
     * to avoid serializing 5-10MB of task results every 500ms.
     * Full task details available via getTaskTree().
     *
     * @returns Array of root tasks with output truncated to 200 chars
     */
    getTaskTreeLight() {
        const rootTasks = [];
        for (const task of this.tasks.values()) {
            if (!task.parentId) {
                // Strip output to avoid serializing large strings on every broadcast
                const { output: _output, ...lightTask } = task;
                rootTasks.push(lightTask);
            }
        }
        return rootTasks;
    }
    /**
     * Get all tasks as a flat Map.
     *
     * @returns Copy of the internal tasks map (safe to modify)
     */
    getAllTasks() {
        return new Map(this.tasks);
    }
    /**
     * Get a specific task by its ID.
     *
     * @param taskId - The task ID to look up
     * @returns The task if found, undefined otherwise
     */
    getTask(taskId) {
        return this.tasks.get(taskId);
    }
    /**
     * Get the count of currently running tasks.
     *
     * @returns Number of tasks with status 'running'
     */
    getRunningCount() {
        let count = 0;
        for (const task of this.tasks.values()) {
            if (task.status === 'running')
                count++;
        }
        return count;
    }
    /**
     * Get aggregated statistics about all tracked tasks.
     *
     * @returns Object with counts:
     *   - total: Total number of tracked tasks
     *   - running: Tasks currently executing
     *   - completed: Successfully finished tasks
     *   - failed: Tasks that ended with errors
     */
    getStats() {
        let running = 0, completed = 0, failed = 0;
        for (const task of this.tasks.values()) {
            switch (task.status) {
                case 'running':
                    running++;
                    break;
                case 'completed':
                    completed++;
                    break;
                case 'failed':
                    failed++;
                    break;
                default:
                    assertNever(task.status, `Unhandled BackgroundTask status: ${task.status}`);
            }
        }
        return { total: this.tasks.size, running, completed, failed };
    }
    /**
     * Clear all tracked tasks.
     *
     * Use when the session is cleared or closed.
     * Resets the task map, stack, and pending tool uses.
     */
    clear() {
        this.tasks.clear();
        this.taskStack = [];
        this.pendingToolUses.clear();
    }
    /**
     * Clean up resources and release memory.
     *
     * Call this when the session is being destroyed to prevent memory leaks.
     * Clears all data and removes all event listeners.
     */
    destroy() {
        this.clear();
        this.removeAllListeners();
    }
}
//# sourceMappingURL=task-tracker.js.map