import { logger } from "../logger.js";
const MAX_EXCHANGES = 10;
const EXCHANGE_TEXT_LIMIT = 200;
const SUBAGENT_ACTIVITY_LIMIT = 120;
/** Minimum interval (ms) between streaming activity updates to avoid thrash. */
const STREAM_ACTIVITY_INTERVAL_MS = 5_000;
/** Minimum chars accumulated before we update the streaming activity summary. */
const STREAM_ACTIVITY_CHAR_THRESHOLD = 500;
/**
 * Cap the number of subagent records we retain. Active subagents always
 * fit; once completed they age out FIFO when new ones are added. This
 * prevents unbounded growth over a long task that spawns many subagents.
 */
const MAX_SUBAGENTS = 32;
/**
 * How many recent CLI auto-memory recall events to retain. Each entry is tiny
 * (a tool name + a truncated query + a few paths) but a chatty recall
 * supervisor could still pile up — this caps memory use with a FIFO ring.
 */
const MAX_MEMORY_RECALL_EVENTS = 20;
/**
 * Maintains a rolling summary of the primary agent's current activity.
 * Read by the fast responder to give context-aware quick replies when the
 * agent is busy.
 */
export class ActivityTracker {
    currentTaskDescription = "";
    recentActions = [];
    lastAssistantText = "";
    taskStartedAt = null;
    lastTodos = [];
    recentExchanges = [];
    streamingActivity = null;
    /**
     * Ordered map of in-flight + recently-completed subagent runs keyed by
     * tool_use_id. Insertion order is preserved by `Map`; we prune oldest
     * completed entries when the map exceeds `MAX_SUBAGENTS`.
     */
    subagents = new Map();
    /**
     * CLI auto-memory state observed this session. Paths come from
     * `system/init` (stable for the life of a query); recall events come
     * from `system/memory_recall`. Survives across wake cycles so operator
     * surfaces like /obs/memory-status can display the most recent info.
     */
    memoryPaths = {};
    memoryRecallEvents = [];
    /**
     * Session-scoped registry of tasks seen via the incremental
     * `TaskCreate`/`TaskUpdate` tools, keyed by the SDK's task id. Unlike the
     * per-stream `TaskTodoAccumulator` (one `processAgentStream` call / one
     * turn), this lives for the life of the session, so a cross-turn status-only
     * `TaskUpdate` — whose `TaskCreate` landed in an earlier turn — can recover
     * the original subject instead of being dropped. In-memory only: it does not
     * survive a process restart, which is acceptable since a fresh session
     * re-creates its tasks.
     */
    taskRegistry = new Map();
    maxActions;
    constructor(maxActions = 15) {
        this.maxActions = maxActions;
    }
    /**
     * The session-scoped task registry shared with each turn's
     * `TaskTodoAccumulator` so cross-turn `TaskUpdate`s can restore subjects.
     */
    getTaskRegistry() {
        return this.taskRegistry;
    }
    startTask(description) {
        this.currentTaskDescription = description;
        this.recentActions = [];
        this.lastAssistantText = "";
        this.taskStartedAt = Date.now();
        this.streamingActivity = null;
        this.subagents.clear();
        logger.debug("[activity-tracker] task started");
    }
    recordUserMessage(text) {
        this.pushExchange({ role: "user", text });
    }
    recordAssistantText(text) {
        this.lastAssistantText = text;
        this.pushExchange({ role: "assistant", text });
    }
    recordToolCall(name, inputSummary) {
        const truncated = inputSummary.length > 120
            ? inputSummary.substring(0, 120) + "…"
            : inputSummary;
        this.push(`→ ${name}(${truncated})`);
    }
    recordToolResult(name, outputSummary) {
        const truncated = outputSummary.length > 120
            ? outputSummary.substring(0, 120) + "…"
            : outputSummary;
        this.push(`← ${name}: ${truncated}`);
    }
    recordTodos(todos) {
        this.lastTodos = todos;
    }
    getTodos() {
        return this.lastTodos;
    }
    /**
     * Signal that a new streaming content block has started. Sets up tracking
     * state so subsequent `recordStreamingDelta` calls can accumulate content.
     */
    startStreamingBlock(blockType) {
        this.streamingActivity = {
            blockType,
            charsAccumulated: 0,
            lastUpdatedAt: Date.now(),
            summary: `Generating ${blockType}…`,
        };
        logger.debug(`[activity-tracker] streaming start: ${blockType}`);
    }
    /**
     * Accumulate streaming delta characters. Updates the human-readable summary
     * only when enough time or content has elapsed (debounced).
     */
    recordStreamingDelta(chars) {
        if (!this.streamingActivity)
            return;
        this.streamingActivity.charsAccumulated += chars;
        const now = Date.now();
        const elapsed = now - this.streamingActivity.lastUpdatedAt;
        const accumulated = this.streamingActivity.charsAccumulated;
        if (elapsed >= STREAM_ACTIVITY_INTERVAL_MS || accumulated % STREAM_ACTIVITY_CHAR_THRESHOLD < chars) {
            const kb = (accumulated / 1000).toFixed(1);
            this.streamingActivity.summary =
                `Streaming ${this.streamingActivity.blockType} (${kb}k chars so far)`;
            this.streamingActivity.lastUpdatedAt = now;
            // logger.debug(
            //   `[activity-tracker] streaming delta: ${this.streamingActivity.blockType} (+${chars} chars, total ${accumulated})`,
            // );
        }
    }
    /** Signal that the current streaming block has finished. */
    stopStreamingBlock() {
        if (this.streamingActivity) {
            logger.debug(`[activity-tracker] streaming stop: ${this.streamingActivity.blockType} (total ${this.streamingActivity.charsAccumulated} chars)`);
        }
        this.streamingActivity = null;
    }
    getStreamingActivity() {
        return this.streamingActivity;
    }
    /**
     * Register a newly-spawned subagent. Safe to call multiple times for
     * the same toolUseId — later calls are treated as no-ops so we don't
     * clobber already-captured progress if events arrive out of order.
     */
    recordSubagentStarted(toolUseId, info) {
        if (this.subagents.has(toolUseId))
            return;
        this.subagents.set(toolUseId, {
            toolUseId,
            subagentType: info.subagentType,
            description: info.description,
            startedAt: Date.now(),
            completedAt: null,
            status: "running",
            lastActivity: null,
            actionCount: 0,
        });
        this.pruneCompletedSubagents();
    }
    /**
     * Associate an SDK `task_id` with an already-registered subagent so that
     * subsequent system events lacking `tool_use_id` (notably `task_updated`)
     * can still be routed to the correct entry. No-op if the subagent isn't
     * tracked or already has a taskId — the mapping is stable for the life of
     * the task.
     */
    recordSubagentTaskId(toolUseId, taskId) {
        const entry = this.subagents.get(toolUseId);
        if (!entry)
            return;
        entry.taskId ??= taskId;
    }
    /**
     * Reverse lookup: given an SDK `task_id`, return the tracked subagent.
     * Used by stream-handler to repair events that only carry `task_id`.
     */
    findSubagentByTaskId(taskId) {
        for (const entry of this.subagents.values()) {
            if (entry.taskId === taskId)
                return entry;
        }
        return undefined;
    }
    /**
     * Record a nested event (assistant text or tool call) observed under
     * an active subagent. We keep only the latest activity string plus a
     * counter to avoid per-subagent history growth. No-op if the subagent
     * isn't tracked — stream ordering can produce nested events before we
     * see the parent Task block; callers should always `recordSubagentStarted`
     * first when possible.
     */
    recordSubagentActivity(toolUseId, label) {
        const entry = this.subagents.get(toolUseId);
        if (entry?.status !== "running")
            return;
        const truncated = label.length > SUBAGENT_ACTIVITY_LIMIT
            ? label.substring(0, SUBAGENT_ACTIVITY_LIMIT) + "…"
            : label;
        entry.lastActivity = truncated;
        entry.actionCount += 1;
    }
    /**
     * Record an async subagent progress snapshot from a `task_progress` system
     * event. Unlike {@link recordSubagentActivity}, the SDK supplies a cumulative
     * `toolUses` count, so we *set* `actionCount` instead of incrementing. The
     * `description` and `lastToolName` fields describe the current operation;
     * prefer `lastToolName` when present because descriptions often repeat the
     * subagent title (e.g. "Research Middle East situation") rather than the
     * current step.
     */
    recordSubagentProgress(toolUseId, info) {
        const entry = this.subagents.get(toolUseId);
        if (entry?.status !== "running")
            return;
        if (typeof info.toolUses === "number" && Number.isFinite(info.toolUses)) {
            entry.actionCount = Math.max(entry.actionCount, info.toolUses);
        }
        const label = info.lastToolName ?? info.description;
        if (label && label.length > 0) {
            const truncated = label.length > SUBAGENT_ACTIVITY_LIMIT
                ? label.substring(0, SUBAGENT_ACTIVITY_LIMIT) + "…"
                : label;
            entry.lastActivity = truncated;
        }
    }
    recordSubagentCompleted(toolUseId, info) {
        const entry = this.subagents.get(toolUseId);
        if (!entry)
            return;
        entry.status = info.ok ? "completed" : "failed";
        entry.completedAt = Date.now();
        if (info.summary) {
            const truncated = info.summary.length > SUBAGENT_ACTIVITY_LIMIT
                ? info.summary.substring(0, SUBAGENT_ACTIVITY_LIMIT) + "…"
                : info.summary;
            entry.lastActivity = truncated;
        }
    }
    /**
     * Returns all tracked subagents in insertion order. Callers that only
     * care about the currently-running ones should filter by status.
     */
    getSubagents() {
        return Array.from(this.subagents.values());
    }
    getActiveSubagents() {
        return this.getSubagents().filter((s) => s.status === "running");
    }
    /**
     * Update the known CLI auto-memory paths (typically one, keyed "auto").
     * Safe to call repeatedly — we merge new keys in rather than clobber, so
     * a transient CLI init that omits a known scope won't wipe it.
     *
     * The CLI reports `memory_paths` on every `system/init` event; the payload
     * is stable within a session and only changes if e.g. the operator moved
     * the profile directory. Callers should not filter — we take whatever the
     * CLI gives us.
     */
    recordMemoryPaths(paths) {
        for (const [scope, raw] of Object.entries(paths)) {
            if (typeof raw !== "string" || raw.length === 0)
                continue;
            if (this.memoryPaths[scope] !== raw) {
                this.memoryPaths[scope] = raw;
                logger.debug(`[activity-tracker] memory_paths.${scope} = ${raw}`);
            }
        }
    }
    /**
     * Record a CLI `system/memory_recall` event. We keep a rolling ring of
     * the most recent N so operator surfaces can show what the CLI has been
     * pulling into context. Stores path/scope metadata only — any
     * `synthesize`-mode content is deliberately dropped (it can be large
     * and is already fed to the model by the CLI).
     */
    recordMemoryRecall(event) {
        const entry = {
            observedAt: Date.now(),
            mode: event.mode,
            memories: event.memories.slice(0, 10).map((m) => ({
                path: m.path,
                scope: m.scope,
            })),
        };
        this.memoryRecallEvents.push(entry);
        if (this.memoryRecallEvents.length > MAX_MEMORY_RECALL_EVENTS) {
            this.memoryRecallEvents.shift();
        }
    }
    /**
     * Snapshot of the observed CLI auto-memory state. Returns a shallow copy
     * so callers can't mutate internal state. Safe to call from /obs and
     * /tasks even when no memory activity has been seen yet — `paths` will
     * simply be empty.
     */
    getMemoryStatus() {
        return {
            paths: { ...this.memoryPaths },
            recentRecalls: this.memoryRecallEvents.slice(),
        };
    }
    getTaskDescription() {
        return this.currentTaskDescription;
    }
    getTaskElapsedMs() {
        return this.taskStartedAt !== null ? Date.now() - this.taskStartedAt : null;
    }
    getRecentActions() {
        return this.recentActions;
    }
    /**
     * Produce a concise summary of the agent's current activity for the fast
     * responder's system context.
     */
    getSummary() {
        if (this.taskStartedAt === null)
            return "Agent is idle.";
        const elapsedSec = Math.round((Date.now() - this.taskStartedAt) / 1000);
        const lines = [];
        if (this.currentTaskDescription) {
            lines.push(`Current task: ${this.currentTaskDescription}`);
        }
        lines.push(`Running for: ${elapsedSec}s`);
        if (this.recentExchanges.length > 0) {
            lines.push("", "Recent conversation:");
            for (const e of this.recentExchanges) {
                const preview = e.text.length > EXCHANGE_TEXT_LIMIT
                    ? e.text.substring(0, EXCHANGE_TEXT_LIMIT) + "…"
                    : e.text;
                const label = e.role === "user" ? "User" : "Agent";
                lines.push(`  ${label}: ${preview}`);
            }
        }
        if (this.streamingActivity) {
            lines.push(`Currently streaming: ${this.streamingActivity.summary}`);
        }
        if (this.lastAssistantText) {
            const preview = this.lastAssistantText.length > 300
                ? this.lastAssistantText.substring(0, 300) + "…"
                : this.lastAssistantText;
            lines.push(`Last agent message: ${preview}`);
        }
        if (this.recentActions.length > 0) {
            lines.push("Recent actions:");
            for (const a of this.recentActions)
                lines.push(`  ${a}`);
        }
        const activeSubagents = this.getActiveSubagents();
        if (activeSubagents.length > 0) {
            lines.push("Active subagents:");
            for (const s of activeSubagents) {
                lines.push(`  ${formatSubagentLine(s)}`);
            }
        }
        return lines.join("\n");
    }
    clear() {
        this.currentTaskDescription = "";
        this.recentActions = [];
        this.lastAssistantText = "";
        this.taskStartedAt = null;
        this.lastTodos = [];
        this.recentExchanges = [];
        this.streamingActivity = null;
        this.subagents.clear();
        // Memory paths + recall history are intentionally preserved across
        // clears: they describe session-level state (where the CLI is writing
        // memory, what it has recalled) and are useful for diagnosing subtle
        // issues like "did auto-memory ever fire in this session?" across
        // wake-cycle boundaries. They only reset when the process restarts.
    }
    push(entry) {
        this.recentActions.push(entry);
        if (this.recentActions.length > this.maxActions) {
            this.recentActions.shift();
        }
    }
    pushExchange(entry) {
        this.recentExchanges.push(entry);
        if (this.recentExchanges.length > MAX_EXCHANGES) {
            this.recentExchanges.shift();
        }
    }
    /**
     * Bound the subagent map. Running subagents are never evicted — only
     * already-completed ones age out when we exceed the cap. This protects
     * both against runaway growth and against losing in-flight progress
     * for long-running tasks.
     */
    pruneCompletedSubagents() {
        if (this.subagents.size <= MAX_SUBAGENTS)
            return;
        let overflow = this.subagents.size - MAX_SUBAGENTS;
        for (const [id, entry] of this.subagents) {
            if (overflow <= 0)
                break;
            if (entry.status !== "running") {
                this.subagents.delete(id);
                overflow -= 1;
            }
        }
    }
}
/**
 * Render a one-line summary of a subagent for `/tasks` and status surfaces.
 * Kept at module scope so it can be reused by command handlers that want
 * a consistent presentation (e.g. /tasks).
 */
export function formatSubagentLine(progress) {
    const elapsedSec = Math.round(((progress.completedAt ?? Date.now()) - progress.startedAt) / 1000);
    const label = progress.subagentType ?? "subagent";
    const statusIcon = progress.status === "running"
        ? "🔄"
        : progress.status === "completed"
            ? "✅"
            : "❌";
    const parts = [`${statusIcon} ${label}`];
    parts.push(`(${elapsedSec}s, ${progress.actionCount} step${progress.actionCount === 1 ? "" : "s"})`);
    if (progress.description) {
        const desc = progress.description.length > 80
            ? progress.description.substring(0, 80) + "…"
            : progress.description;
        parts.push(`— ${desc}`);
    }
    if (progress.lastActivity) {
        parts.push(`| last: ${progress.lastActivity}`);
    }
    return parts.join(" ");
}
//# sourceMappingURL=activity-tracker.js.map