/**
 * Process-local registry of task subagents, keyed by parent session scope.
 *
 * Each running/finished task is keyed by `(scopeId, agentId)` where scopeId
 * is the parent session file. Subagent sessions resolve to their parent's
 * scope so a nested subagent's task list still surfaces to the top-level UI.
 *
 * The manager owns three concerns:
 *  - lifecycle state transitions (running -> completed/failed/killed)
 *  - message routing to live subagents (with queueing if the runner has not
 *    yet wired its sender, and resume-from-transcript for terminal subagents)
 *  - rendering helpers used by both the parent agent loop (XML notifications)
 *    and the TUI (single-line display strings)
 */
import { writeFileSync } from 'node:fs';
import { TASK_DISPLAY_PREVIEW_MAX_CHARS, TASK_OUTPUT_LINE_LIMIT, TASK_PARENT_NOTIFICATION_MAX_CHARS, TASK_RECENT_TOOL_CALL_LIMIT } from './types.js';
/** Resolve the scope id used to key tasks — falls through subagent sessions to the top-level parent. */
export function getTaskScopeId(session) {
    const header = toSessionHeader(session?.getHeader?.());
    if (header?.isSubagent && header.parentSession) {
        return header.parentSession;
    }
    return session?.getSessionFile?.() ?? session?.getSessionId?.();
}
/** Narrow an unknown header value to the subset needed for scope-id resolution. */
function toSessionHeader(value) {
    return value && typeof value === 'object'
        ? value
        : undefined;
}
export class TaskManager {
    tasks = new Map();
    names = new Map();
    listeners = new Set();
    /** Register a new running task and return the populated record. */
    register(input) {
        const task = {
            ...input,
            status: 'running',
            isBackgrounded: input.isBackgrounded === true,
            runId: 1,
            startedAt: Date.now(),
            toolUses: 0,
            activeToolUses: 0,
            recentToolCalls: [],
            outputLines: [],
            pendingMessages: [],
            notified: false
        };
        this.tasksForScope(task.scopeId).set(task.agentId, task);
        const names = this.namesForScope(task.scopeId);
        const ids = names.get(task.name) ?? new Set();
        ids.add(task.agentId);
        names.set(task.name, ids);
        this.emitChange();
        return task;
    }
    /** Return all tasks registered under a scope, ordered by start time. */
    list(scopeId) {
        return [...(this.tasks.get(scopeId)?.values() ?? [])].sort((a, b) => a.startedAt - b.startedAt);
    }
    /** Count tasks in the running state for a scope. */
    countRunning(scopeId) {
        return this.list(scopeId).filter((task) => task.status === 'running').length;
    }
    /** Subscribe to registry mutations; returns an unsubscribe function. */
    subscribe(listener) {
        this.listeners.add(listener);
        return () => this.listeners.delete(listener);
    }
    /** Look up a task by agent id or by task name; returns 'ambiguous' if a name resolves to multiple tasks. */
    get(scopeId, agentIdOrName) {
        const tasks = this.tasks.get(scopeId);
        if (!tasks)
            return undefined;
        const byId = tasks.get(agentIdOrName);
        if (byId)
            return byId;
        const ids = this.names.get(scopeId)?.get(agentIdOrName);
        if (!ids || ids.size === 0)
            return undefined;
        if (ids.size > 1)
            return 'ambiguous';
        const [agentId] = ids;
        return agentId ? tasks.get(agentId) : undefined;
    }
    /** Apply a progress snapshot to a running task and notify subscribers. */
    updateProgress(scopeId, agentId, progress, transcriptFile) {
        const task = this.tasks.get(scopeId)?.get(agentId);
        if (!task || task.status !== 'running')
            return;
        const next = typeof progress === 'number' ? { toolUses: progress, transcriptFile } : progress;
        task.toolUses = Math.max(task.toolUses, next.toolUses);
        task.activeToolUses = next.activeToolUses ?? task.activeToolUses;
        if (next.usage)
            task.usage = next.usage;
        if (next.resultText !== undefined)
            task.resultText = next.resultText;
        if (next.toolCalls)
            task.recentToolCalls = tail(next.toolCalls, TASK_RECENT_TOOL_CALL_LIMIT);
        if (next.outputLines)
            task.outputLines = tail(next.outputLines, TASK_OUTPUT_LINE_LIMIT);
        if (next.transcriptFile)
            task.transcriptFile = next.transcriptFile;
        this.flushPendingMessages(task);
        this.emitChange();
    }
    /** Mark a task terminal, persist its output file, and return the updated record. */
    finish(scopeId, agentId, result, runId) {
        const task = this.tasks.get(scopeId)?.get(agentId);
        if (!task)
            return undefined;
        // Resumed background tasks bump runId; ignore late completions from a
        // prior run so a slow first run cannot clobber a resumed second run.
        if (runId !== undefined && task.runId !== runId)
            return undefined;
        task.completedAt = Date.now();
        task.usage = result.usage;
        task.resultText = result.text;
        task.errorMessage = result.errorMessage;
        task.transcriptFile = result.sessionFile ?? task.transcriptFile;
        task.toolUses = Math.max(task.toolUses, result.toolCalls.length);
        task.activeToolUses = 0;
        task.recentToolCalls = tail(result.toolCalls, TASK_RECENT_TOOL_CALL_LIMIT);
        task.outputLines = tail(result.outputLines ?? textToLines(result.errorMessage ?? result.text), TASK_OUTPUT_LINE_LIMIT);
        task.status = result.aborted ? 'killed' : result.exitCode === 0 ? 'completed' : 'failed';
        const outputStats = writeTaskOutput(task);
        task.outputFileChars = outputStats.chars;
        task.outputFileLines = outputStats.lines;
        this.emitChange();
        this.resumePendingMessagesAfterTerminalFinish(task);
        return task;
    }
    /** Atomically mark a task as having delivered its parent notification (returns false on duplicates). */
    markNotified(scopeId, agentId) {
        const task = this.tasks.get(scopeId)?.get(agentId);
        if (!task || task.notified)
            return false;
        task.notified = true;
        this.emitChange();
        return true;
    }
    /** Wire the runner's send-message callback to a registered task and flush queued messages. */
    attachSender(scopeId, agentId, sendMessage) {
        const task = this.tasks.get(scopeId)?.get(agentId);
        if (!task)
            return;
        task.sendMessage = sendMessage;
        this.flushPendingMessages(task);
        this.emitChange();
    }
    /** Promote a foreground task to backgrounded so it survives the parent tool call. */
    background(scopeId, agentId) {
        const task = this.tasks.get(scopeId)?.get(agentId);
        if (!task || task.isBackgrounded)
            return false;
        task.isBackgrounded = true;
        this.emitChange();
        return true;
    }
    /** Abort a running task via its AbortController. */
    abort(scopeId, agentId) {
        const task = this.tasks.get(scopeId)?.get(agentId);
        if (!task || task.status !== 'running')
            return false;
        task.abortController.abort();
        this.emitChange();
        return true;
    }
    // Foreground tasks are removed from the registry when the parent tool call
    // resolves; backgrounded tasks stay so the user can inspect/resume them.
    /** Remove a foreground task from the registry once the parent tool call resolves. */
    unregisterIfForeground(scopeId, agentId) {
        const task = this.tasks.get(scopeId)?.get(agentId);
        if (!task || task.isBackgrounded)
            return;
        this.tasks.get(scopeId)?.delete(agentId);
        const ids = this.names.get(scopeId)?.get(task.name);
        ids?.delete(agentId);
        if (ids?.size === 0)
            this.names.get(scopeId)?.delete(task.name);
        this.emitChange();
    }
    /** Reset a terminal task to running state for a transcript-resume run. */
    restart(scopeId, agentId, abortController) {
        const task = this.tasks.get(scopeId)?.get(agentId);
        if (!task)
            return undefined;
        task.status = 'running';
        task.isBackgrounded = true;
        task.runId++;
        task.startedAt = Date.now();
        task.completedAt = undefined;
        task.abortController = abortController;
        task.sendMessage = undefined;
        task.pendingMessages = [];
        task.activeToolUses = 0;
        task.recentToolCalls = [];
        task.outputLines = [];
        task.usage = undefined;
        task.resultText = undefined;
        task.errorMessage = undefined;
        task.notified = false;
        this.emitChange();
        return task;
    }
    /** Clear all registered tasks (used by tests and session resets). */
    reset() {
        this.tasks.clear();
        this.names.clear();
        this.emitChange();
    }
    /** Deliver a follow-up message to a running task, queue if not yet wired, or restart from transcript. */
    routeMessage(scopeId, agentIdOrName, message) {
        const task = this.get(scopeId, agentIdOrName);
        if (task === 'ambiguous') {
            return { ok: false, reason: `Task name '${agentIdOrName}' is ambiguous in this session.` };
        }
        if (!task) {
            return { ok: false, reason: `No task found for '${agentIdOrName}' in this session.` };
        }
        if (task.status !== 'running') {
            // Terminal task with a transcript: restart from sidechain with the new
            // message as the next user turn (set up by lifecycle.startDetached).
            if (task.resumeMessage) {
                task.resumeMessage(message);
                return { ok: true, agentId: task.agentId };
            }
            return {
                ok: false,
                reason: `Task '${task.agentId}' is ${task.status}; terminal resume is not available yet.`
            };
        }
        if (!task.sendMessage) {
            task.pendingMessages.push(message);
            this.emitChange();
            return { ok: true, agentId: task.agentId };
        }
        if (!task.sendMessage(message)) {
            task.pendingMessages.push(message);
            this.emitChange();
            return { ok: true, agentId: task.agentId };
        }
        return { ok: true, agentId: task.agentId };
    }
    /** Try to deliver any messages that arrived before the runner attached its sender. */
    flushPendingMessages(task) {
        if (!task.sendMessage || task.pendingMessages.length === 0 || task.status !== 'running')
            return;
        const remaining = [];
        for (const message of task.pendingMessages) {
            if (!task.sendMessage(message)) {
                remaining.push(message);
            }
        }
        task.pendingMessages = remaining;
    }
    /** After a terminal finish, replay queued messages by triggering a transcript resume. */
    resumePendingMessagesAfterTerminalFinish(task) {
        if (task.status === 'running' || task.pendingMessages.length === 0 || !task.resumeMessage) {
            return;
        }
        const pendingMessage = formatPendingMessagesForResume(task.pendingMessages);
        task.pendingMessages = [];
        queueMicrotask(() => {
            if (task.status === 'running')
                return;
            task.resumeMessage?.(pendingMessage);
        });
    }
    /** Notify all subscribed listeners that the registry mutated. */
    emitChange() {
        for (const listener of this.listeners)
            listener();
    }
    /** Lazily create the task map for a scope. */
    tasksForScope(scopeId) {
        let tasks = this.tasks.get(scopeId);
        if (!tasks) {
            tasks = new Map();
            this.tasks.set(scopeId, tasks);
        }
        return tasks;
    }
    /** Lazily create the name-to-id index for a scope. */
    namesForScope(scopeId) {
        let names = this.names.get(scopeId);
        if (!names) {
            names = new Map();
            this.names.set(scopeId, names);
        }
        return names;
    }
}
/** Return the last `max` items of a list. */
function tail(items, max) {
    return items.slice(Math.max(0, items.length - max));
}
/** Split text into trimmed non-empty lines. */
function textToLines(value) {
    return value
        .split(/\r?\n/)
        .map((line) => line.trimEnd())
        .filter((line) => line.length > 0);
}
/** Combine queued follow-up messages into a single resume prompt. */
function formatPendingMessagesForResume(messages) {
    if (messages.length === 1)
        return messages[0] ?? '';
    return messages.map((message, index) => `Follow-up ${index + 1}:\n${message}`).join('\n\n');
}
// Shared by the single interactive CLI session in this process. Tests and
// multi-session hosts should inject their own TaskManager via createTaskTool().
export const taskManager = new TaskManager();
export const TASK_NOTIFICATION_CUSTOM_TYPE = 'taskNotification';
/** Persist the task's terminal output to disk and return its size stats. */
function writeTaskOutput(task) {
    const header = [
        `# Task ${task.name}`,
        '',
        `- id: ${task.agentId}`,
        `- subagent: ${task.subagentType}`,
        `- status: ${task.status}`,
        task.transcriptFile ? `- transcript: ${task.transcriptFile}` : undefined,
        '',
        '## Result',
        '',
        task.errorMessage ? `Error: ${task.errorMessage}` : task.resultText || '(no output)'
    ]
        .filter((line) => line !== undefined)
        .join('\n');
    const content = `${header}\n`;
    writeFileSync(task.outputFile, content, 'utf-8');
    return getTextStats(content);
}
/** Render the XML notification block consumed by the parent agent loop. */
export function formatTaskNotification(task) {
    const durationMs = task.completedAt ? Math.max(0, task.completedAt - task.startedAt) : 0;
    const totalTokens = (task.usage?.input ?? 0) + (task.usage?.output ?? 0);
    const outputPreview = truncateForParent(task.errorMessage ?? task.resultText ?? '(no output)');
    const lines = [
        `<sa status="${escapeXmlAttribute(task.status)}" type="${escapeXmlAttribute(task.subagentType)}" name="${escapeXmlAttribute(task.name || task.agentId)}" id="${escapeXmlAttribute(task.agentId)}">`,
        `  <prompt>${escapeXmlText(task.prompt)}</prompt>`,
        `  <output>${escapeXmlText(outputPreview)}</output>`,
        `  <files output="${escapeXmlAttribute(task.outputFile)}" output_chars="${task.outputFileChars ?? 0}" output_lines="${task.outputFileLines ?? 0}"${task.transcriptFile ? ` transcript="${escapeXmlAttribute(task.transcriptFile)}"` : ''}/>`,
        `  <usage tokens="${totalTokens}" tools="${task.toolUses}" ms="${durationMs}"/>`,
        '</sa>'
    ];
    return lines.join('\n');
}
/** Render a compact two-line task summary suitable for the TUI display. */
export function formatTaskNotificationDisplay(task) {
    const durationMs = task.completedAt ? Math.max(0, task.completedAt - task.startedAt) : 0;
    const totalTokens = (task.usage?.input ?? 0) + (task.usage?.output ?? 0);
    const outputPreview = truncateOneLine(task.errorMessage ?? task.resultText ?? '(no output)');
    return [
        `Subagent ${task.status}: ${task.name || task.agentId}`,
        `${outputPreview} · final md: ${formatOutputStats(task.outputFileLines, task.outputFileChars)} · usage: ${formatCompactTokenCount(totalTokens)}, ${task.toolUses} ${task.toolUses === 1 ? 'tool' : 'tools'}, ${durationMs} ms`
    ].join('\n');
}
/** Compute character and line counts for a block of text. */
function getTextStats(text) {
    const normalized = text.replace(/\r?\n$/, '');
    return {
        chars: text.length,
        lines: normalized.length === 0 ? 0 : normalized.split(/\r?\n/).length
    };
}
/** Format file size as "N lines, K chars" for display. */
function formatOutputStats(lines, chars) {
    const safeLines = lines ?? 0;
    const safeChars = chars ?? 0;
    return `${safeLines} ${safeLines === 1 ? 'line' : 'lines'}, ${formatCompactNumber(safeChars)} chars`;
}
/** Format a token count with a "tokens" suffix using compact units. */
function formatCompactTokenCount(count) {
    return `${formatCompactNumber(count)} tokens`;
}
/** Format a number using compact k/M units. */
function formatCompactNumber(count) {
    if (count < 1_000)
        return count.toString();
    if (count < 9_950)
        return `${(count / 1_000).toFixed(1)}k`;
    if (count < 1_000_000)
        return `${Math.round(count / 1_000)}k`;
    return `${(count / 1_000_000).toFixed(1)}M`;
}
/** Truncate text to the parent-notification limit, appending a marker if cut. */
function truncateForParent(value) {
    const max = TASK_PARENT_NOTIFICATION_MAX_CHARS;
    return value.length > max ? `${value.slice(0, max)}\n[truncated; see output file]` : value;
}
/** Collapse whitespace and truncate to fit on a single TUI line. */
function truncateOneLine(value) {
    const max = TASK_DISPLAY_PREVIEW_MAX_CHARS;
    const normalized = value.replace(/\s+/g, ' ').trim();
    if (normalized.length <= max)
        return normalized;
    return `${normalized.slice(0, max - 1)}...`;
}
/** Escape `&`, `<`, and `>` for inclusion in XML element text. */
function escapeXmlText(value) {
    return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
/** Escape XML attribute values (extends `escapeXmlText` with quote escaping). */
function escapeXmlAttribute(value) {
    return escapeXmlText(value).replace(/"/g, '&quot;').replace(/'/g, '&apos;');
}
//# sourceMappingURL=manager.js.map