import { getAgentState } from "./state.js";
import { logger } from "../logger.js";
import { ToolCallWatchdog } from "./tool-call-watchdog.js";
/** Default silence timeout (ms) waiting for LLM response before aborting the stream. */
const DEFAULT_STREAM_SILENCE_TIMEOUT_MS = 180_000;
/** How often (ms) to check for stream silence. */
const STREAM_SILENCE_POLL_MS = 10_000;
/**
 * Typed error thrown when the stream is silent for too long.
 * Callers can use `instanceof` to distinguish from other errors.
 */
export class StreamSilenceTimeoutError extends Error {
    constructor(silentSeconds) {
        super(`Stream silence timeout: no LLM response for ${silentSeconds}s`);
        this.name = "StreamSilenceTimeoutError";
    }
}
export class SdkTerminalError extends Error {
    terminalReason;
    sdkSubtype;
    constructor(message, options) {
        super(message);
        this.name = "SdkTerminalError";
        this.terminalReason = options?.terminalReason;
        this.sdkSubtype = options?.sdkSubtype;
    }
}
function isToolNamed(blockName, canonicalName) {
    return blockName === canonicalName || blockName.endsWith(`__${canonicalName}`);
}
/**
 * The Claude Agent SDK historically called the subagent-spawn tool `Task`,
 * but newer CLI builds surface it as `Agent` (both names are still possible
 * depending on the pinned executable). Keep the match permissive so subagent
 * progress tracking survives version drift.
 */
export function isSubagentSpawnTool(blockName) {
    return isToolNamed(blockName, "Task") || isToolNamed(blockName, "Agent");
}
/**
 * Newer Claude CLI builds replace the snapshot-style `TodoWrite` tool with
 * incremental `TaskCreate` / `TaskUpdate` tools. Unlike `TodoWrite` (which
 * sends the full todo array every call), these mutate one task at a time, and
 * `TaskCreate` only returns its generated id in the tool *result* — so we
 * correlate a create call to its result by tool_use_id, then track
 * subject/status by task id. `project()` renders the same `{ content, status }`
 * shape `activityTracker.recordTodos` already consumes, so operator surfaces
 * (/tasks, /status) stay identical regardless of which tool the CLI emits.
 */
export class TaskTodoAccumulator {
    sessionRegistry;
    byId = new Map();
    pendingCreates = new Map();
    /**
     * Optional session-scoped registry (`taskId → {subject,status}`) shared
     * across turns. When provided, this accumulator mirrors every resolved
     * create / subject-bearing update into it, and consults it to recover the
     * subject for a status-only update whose task was created in a prior turn.
     * Without it the accumulator falls back to #465's behavior (ignore unknown
     * status-only updates), so plain per-stream callers/tests are unaffected.
     */
    constructor(sessionRegistry) {
        this.sessionRegistry = sessionRegistry;
    }
    /** Mirror a task's latest state into the cross-turn session registry. */
    remember(taskId, tracked) {
        this.sessionRegistry?.set(taskId, { ...tracked });
    }
    /** Record a TaskCreate call; its task id is unknown until the result lands. */
    noteCreate(toolUseId, input) {
        const subject = typeof input.subject === "string" ? input.subject : "";
        this.pendingCreates.set(toolUseId, { subject, status: "pending" });
    }
    hasPendingCreate(toolUseId) {
        return this.pendingCreates.has(toolUseId);
    }
    /** Resolve a TaskCreate result into a tracked task keyed by its id. */
    resolveCreate(toolUseId, taskId) {
        const pending = this.pendingCreates.get(toolUseId);
        if (!pending)
            return false;
        this.pendingCreates.delete(toolUseId);
        if (taskId) {
            this.byId.set(taskId, pending);
            this.remember(taskId, pending);
        }
        return true;
    }
    /**
     * Apply a TaskUpdate call. Returns true if this stream's projection changed.
     *
     * The SDK's Task state is session-scoped, but this accumulator is scoped to a
     * single `processAgentStream()` call (one turn). So a TaskUpdate may reference
     * a `taskId` whose `TaskCreate` happened in an earlier turn and is therefore
     * unknown to `byId`. To preserve continuity we consult the session registry
     * (when present): a status-only update for such a task recovers its original
     * subject and materializes a proper todo. Only when the task is unknown to
     * both `byId` and the registry, and the update carries no non-empty subject,
     * do we ignore it — that avoids the blank-titled placeholder #465 guarded
     * against clobbering the operator's /tasks view.
     */
    noteUpdate(input) {
        const taskId = typeof input.taskId === "string" ? input.taskId : undefined;
        if (!taskId)
            return false;
        if (input.status === "deleted") {
            const had = this.byId.delete(taskId);
            // When the session registry is enabled the projection is built from it,
            // so a delete that only hits the registry (the task was created in an
            // earlier turn) must still report a change to refresh /tasks.
            const hadInRegistry = this.sessionRegistry?.delete(taskId) ?? false;
            return had || hadInRegistry;
        }
        const hasSubject = typeof input.subject === "string" && input.subject.length > 0;
        // Recover a prior-turn task's subject from the session registry so a
        // status-only update can still render meaningfully.
        const existing = this.byId.get(taskId) ?? (!hasSubject ? this.sessionRegistry?.get(taskId) : undefined);
        if (!existing && !hasSubject) {
            // Unknown to this stream AND the session (or no registry) with no
            // meaningful title: ignore rather than show a blank placeholder.
            return false;
        }
        const tracked = existing
            ? { subject: existing.subject, status: existing.status }
            : { subject: input.subject, status: "pending" };
        if (hasSubject)
            tracked.subject = input.subject;
        if (input.status === "pending" || input.status === "in_progress" || input.status === "completed") {
            tracked.status = input.status;
        }
        this.byId.set(taskId, tracked);
        this.remember(taskId, tracked);
        return true;
    }
    /**
     * Project tracked tasks into the activity tracker's todo list shape.
     *
     * When a session registry is present we project from it (not just this
     * stream's `byId`) so /tasks reflects the full set of tasks seen across the
     * session — a status-only update to one prior-turn task must not drop the
     * sibling tasks created alongside it in earlier turns. The registry is an
     * insertion-ordered `Map`, so todos stay in stable creation order. Without a
     * registry we fall back to the per-stream `byId` (unchanged #465 behavior).
     */
    project() {
        const source = this.sessionRegistry ?? this.byId;
        return Array.from(source.values()).map((t) => ({ content: t.subject, status: t.status }));
    }
}
/**
 * Pull the generated task id out of a `TaskCreate` tool result. The result
 * content may be a JSON string, a `{ task: { id } }` object, or an array of
 * text content blocks — tolerate all three.
 */
export function extractCreatedTaskId(content) {
    let obj = content;
    if (typeof content === "string") {
        try {
            obj = JSON.parse(content);
        }
        catch {
            return undefined;
        }
    }
    if (Array.isArray(obj)) {
        for (const part of obj) {
            const text = part?.text;
            if (typeof text === "string") {
                const id = extractCreatedTaskId(text);
                if (id)
                    return id;
            }
        }
        return undefined;
    }
    const task = obj?.task;
    return typeof task?.id === "string" ? task.id : undefined;
}
/**
 * Route a CLI `system/memory_recall` event into the ActivityTracker. Skips
 * malformed payloads defensively — the CLI is evolving this shape and we
 * never want a schema drift in an observability path to take the agent
 * loop down.
 */
export function handleMemoryRecallEvent(event, activityTracker) {
    const raw = event.memories;
    if (!Array.isArray(raw) || raw.length === 0)
        return;
    const memories = raw
        .filter((m) => typeof m.path === "string")
        .map((m) => ({ path: m.path, scope: m.scope }));
    if (memories.length === 0)
        return;
    activityTracker.recordMemoryRecall({
        mode: event.mode,
        memories,
    });
    // One concise log line per recall — full schema stays in the verbose
    // system-message debug log below. Operators typically only care that
    // memory *happened* plus which files were touched. We know memories is
    // non-empty past the guard above, so indexing [0] is safe.
    const first = memories[0];
    const firstScope = first.scope ?? "?";
    const firstPath = first.path;
    const extras = memories.length > 1 ? ` (+${memories.length - 1} more)` : "";
    logger.system(`[memory] recall mode=${event.mode ?? "?"} scope=${firstScope} path=${firstPath}${extras}`);
}
export function handleAsyncTaskEvent(event, subagentByTaskToolUseId, activityTracker) {
    const resolveToolUseId = () => {
        if (event.tool_use_id)
            return event.tool_use_id;
        if (event.task_id) {
            return activityTracker.findSubagentByTaskId(event.task_id)?.toolUseId;
        }
        return undefined;
    };
    switch (event.subtype) {
        case "task_started": {
            const toolUseId = event.tool_use_id;
            if (!toolUseId)
                return;
            // Most of the time we've already registered the subagent from the
            // parent Agent tool_use. Only fall back to registering here if we
            // somehow missed it (e.g. events arrived out of order or the spawn
            // tool_use was filtered out upstream).
            const existing = subagentByTaskToolUseId.get(toolUseId);
            if (!existing) {
                subagentByTaskToolUseId.set(toolUseId, {});
            }
            activityTracker.recordSubagentStarted(toolUseId, {
                description: event.description,
            });
            // Stash the task_id two places:
            //   - subagentByTaskToolUseId (per-call map, used for tool_result
            //     correlation during the same processAgentStream invocation)
            //   - activityTracker.subagents entry (long-lived, survives across
            //     wake-cycle continuation iterations — this is the one that
            //     fixes the `task_updated` dropping bug)
            if (event.task_id) {
                const stored = subagentByTaskToolUseId.get(toolUseId);
                if (stored && !stored.agentId) {
                    subagentByTaskToolUseId.set(toolUseId, { ...stored, agentId: event.task_id });
                }
                activityTracker.recordSubagentTaskId(toolUseId, event.task_id);
            }
            break;
        }
        case "task_progress": {
            const toolUseId = resolveToolUseId();
            if (!toolUseId)
                return;
            activityTracker.recordSubagentProgress(toolUseId, {
                toolUses: event.usage?.tool_uses,
                lastToolName: event.last_tool_name,
                description: event.description,
            });
            break;
        }
        case "task_updated": {
            const status = event.patch?.status;
            if (status !== "completed" && status !== "failed")
                return;
            const toolUseId = resolveToolUseId();
            if (!toolUseId)
                return;
            activityTracker.recordSubagentCompleted(toolUseId, {
                ok: status === "completed",
            });
            break;
        }
        case "task_notification": {
            if (event.status !== "completed" && event.status !== "failed")
                return;
            const toolUseId = resolveToolUseId();
            if (!toolUseId)
                return;
            activityTracker.recordSubagentCompleted(toolUseId, {
                ok: event.status === "completed",
                summary: event.summary,
            });
            break;
        }
    }
}
function getResultErrorSummary(msg) {
    const { errors } = msg;
    if (!Array.isArray(errors)) {
        return "";
    }
    return errors
        .map((error) => {
        if (typeof error === "string") {
            return error;
        }
        try {
            return JSON.stringify(error);
        }
        catch {
            return String(error);
        }
    })
        .filter((error) => error.length > 0)
        .join("; ");
}
function logResult(msg) {
    const cost = msg.total_cost_usd.toFixed(4);
    logger.result(msg.num_turns, msg.usage.input_tokens, msg.usage.output_tokens, cost);
    if (msg.is_error) {
        const errors = getResultErrorSummary(msg);
        // `msg.subtype` describes how the turn ended (e.g. "success" when the
        // agent called the finish tool); it's orthogonal to `is_error`. Putting
        // it in parens next to "error" reads as "(success)" which is nonsense.
        // Include the subtype only when it actually names the failure mode.
        const subtypeHint = msg.subtype && msg.subtype !== "success" ? ` [${msg.subtype}]` : "";
        const terminalReasonHint = msg.terminal_reason ? ` [terminal_reason=${msg.terminal_reason}]` : "";
        logger.err(`Agent ended with error${subtypeHint}${terminalReasonHint}${errors ? `: ${errors}` : ""}`);
    }
}
function captureUsageSnapshotFromResult(session, msg) {
    try {
        logger.debug("Capturing model usage snapshot from result message...");
        const modelUsage = msg.modelUsage ??
            msg.model_usage;
        if (!modelUsage || typeof modelUsage !== "object") {
            logger.debug("[modelUsage] missing on result message");
            return;
        }
        const byModel = modelUsage;
        const primary = typeof msg.model === "string"
            ? (msg.model)
            : undefined;
        const usageObjUnknown = (primary && byModel[primary] !== undefined ? byModel[primary] : undefined) ??
            Object.values(byModel)[0];
        if (!usageObjUnknown || typeof usageObjUnknown !== "object")
            return;
        const u = usageObjUnknown;
        const contextWindow = typeof u.contextWindow === "number" ? u.contextWindow : undefined;
        if (!contextWindow || contextWindow <= 0) {
            logger.debug("[modelUsage] missing/invalid contextWindow", { contextWindow });
            return;
        }
        const inputTokens = typeof u.inputTokens === "number" ? u.inputTokens : 0;
        const cacheReadInputTokens = typeof u.cacheReadInputTokens === "number" ? u.cacheReadInputTokens : 0;
        const cacheCreationInputTokens = typeof u.cacheCreationInputTokens === "number" ? u.cacheCreationInputTokens : 0;
        const usedInputTokens = inputTokens + cacheReadInputTokens + cacheCreationInputTokens;
        if (msg.is_error && usedInputTokens === 0) {
            logger.debug("[modelUsage] skipping zero-token error result snapshot");
            return;
        }
        const usedPct = usedInputTokens / contextWindow;
        if (!Number.isFinite(usedPct) || usedPct < 0) {
            logger.debug("[modelUsage] computed invalid usedPct", {
                usedPct,
                usedInputTokens,
                contextWindow,
            });
            return;
        }
        session.captureUsageSnapshot({
            usedInputTokens,
            contextWindow,
            usedPct,
        });
        logger.debug(`[modelUsage] snapshot: usedPct=${(usedPct * 100).toFixed(1)}% used=${usedInputTokens} window=${contextWindow} (in=${inputTokens} cache_read=${cacheReadInputTokens} cache_create=${cacheCreationInputTokens})`);
    }
    catch {
        // ignore
    }
}
/**
 * Process the agent SDK response stream for a single wake cycle.
 * Handles logging, activity tracking, subagent correlation, and
 * detecting the finish tool call.
 *
 * Returns timeout info if the query was aborted due to a tool call timeout,
 * or `null` if the query completed normally.
 */
export async function processAgentStream(queryStream, session, activityTracker, sessionManager, config, silenceTimeoutMs = DEFAULT_STREAM_SILENCE_TIMEOUT_MS) {
    const toolNameById = new Map();
    // Tracks incremental TaskCreate/TaskUpdate tool calls (newer CLI builds) and
    // projects them into the legacy todo-list shape for the activity tracker.
    // Shares the session-scoped task registry so a TaskUpdate referencing a task
    // created in an earlier turn can recover its subject instead of being dropped.
    const taskTodos = new TaskTodoAccumulator(activityTracker.getTaskRegistry());
    const subagentByTaskToolUseId = new Map();
    const watchdog = new ToolCallWatchdog(() => { session.requestStop(); }, config, activityTracker);
    let finishCalled = false;
    let postFinishContinuationLogged = false;
    let pendingAssistantError = null;
    let liveContextUsageProbed = false;
    // --- Stream silence detection ---
    const activeToolIds = new Set();
    let lastEventTime = Date.now();
    const silenceState = { timedOut: false };
    const silenceTimer = setInterval(() => {
        if (silenceState.timedOut)
            return;
        if (activeToolIds.size > 0) {
            // Tools are running — ToolCallWatchdog handles those timeouts.
            lastEventTime = Date.now();
            return;
        }
        const silentMs = Date.now() - lastEventTime;
        if (silentMs > silenceTimeoutMs) {
            silenceState.timedOut = true;
            logger.err(`Stream silence timeout: no events for ${Math.round(silentMs / 1000)}s while waiting for LLM — aborting`);
            session.requestStop();
        }
    }, STREAM_SILENCE_POLL_MS);
    try {
        for await (const msg of queryStream) {
            lastEventTime = Date.now();
            if ("session_id" in msg && msg.session_id) {
                session.captureSessionId(msg.session_id);
            }
            if (finishCalled && !postFinishContinuationLogged && msg.type === "assistant") {
                postFinishContinuationLogged = true;
                logger.system("Post-finish continuation: SDK is processing an injected message that was already in the pipeline");
            }
            // Probe live context usage once per turn while the query is still
            // streaming (control channel will be closed by the time `result`
            // arrives). Fires on the first assistant message so totals are
            // already initialized.
            if (!liveContextUsageProbed && msg.type === "assistant" && session.getLiveContextUsage) {
                liveContextUsageProbed = true;
                session.getLiveContextUsage().catch((err) => {
                    logger.debug(`[getLiveContextUsage] probe failed: ${err instanceof Error ? err.message : String(err)}`);
                });
            }
            switch (msg.type) {
                case "user": {
                    const um = msg;
                    const parentToolUseId = typeof um.parent_tool_use_id === "string" && um.parent_tool_use_id.length > 0
                        ? um.parent_tool_use_id
                        : undefined;
                    const subagentMeta = parentToolUseId
                        ? subagentByTaskToolUseId.get(parentToolUseId)
                        : undefined;
                    const blocks = um.message?.content;
                    if (Array.isArray(blocks)) {
                        for (const b of blocks) {
                            const block = b;
                            if (block.type !== "tool_result" || !block.tool_use_id)
                                continue;
                            watchdog.trackEnd(block.tool_use_id);
                            activeToolIds.delete(block.tool_use_id);
                            const maybeTask = subagentByTaskToolUseId.get(block.tool_use_id);
                            // `asyncLaunch` means this tool_result is the synchronous launch
                            // ACK for an async Agent task, not its final output. Don't mark
                            // the subagent completed — completion arrives later via
                            // `task_updated` / `task_notification` system messages. We still
                            // harvest the agentId for correlation.
                            let asyncLaunch = false;
                            if (maybeTask) {
                                const contentStr = block.content === undefined
                                    ? ""
                                    : typeof block.content === "string"
                                        ? block.content
                                        : JSON.stringify(block.content);
                                const re = /agentId:\s*([a-f0-9-]+)/i;
                                const match = re.exec(contentStr);
                                if (match?.[1]) {
                                    subagentByTaskToolUseId.set(block.tool_use_id, {
                                        ...maybeTask,
                                        agentId: match[1],
                                    });
                                    asyncLaunch = true;
                                }
                                else if (/Async agent launched/i.test(contentStr)) {
                                    asyncLaunch = true;
                                }
                            }
                            const out = block.content;
                            const summary = out === undefined
                                ? ""
                                : typeof out === "string"
                                    ? out
                                    : JSON.stringify(out);
                            const resolvedName = toolNameById.get(block.tool_use_id) ?? "tool";
                            logger.toolResult(resolvedName, summary, {
                                tool_use_id: block.tool_use_id,
                                is_error: block.is_error,
                                source: "sdk_user_message_block",
                                ...(parentToolUseId ? { parent_tool_use_id: parentToolUseId } : {}),
                                ...(subagentMeta?.subagent_type
                                    ? { subagent_type: subagentMeta.subagent_type }
                                    : {}),
                                ...(subagentMeta?.agentId ? { subagent_agent_id: subagentMeta.agentId } : {}),
                            });
                            // Finalize a pending TaskCreate now that its generated id is
                            // available in the result, then refresh the projected todo list.
                            if (isToolNamed(resolvedName, "TaskCreate") &&
                                taskTodos.hasPendingCreate(block.tool_use_id)) {
                                const taskId = extractCreatedTaskId(out);
                                if (taskTodos.resolveCreate(block.tool_use_id, taskId)) {
                                    activityTracker.recordTodos(taskTodos.project());
                                }
                            }
                            if (!parentToolUseId) {
                                activityTracker.recordToolResult(resolvedName, summary);
                            }
                            else {
                                // Nested tool result from an active subagent — surface the
                                // last step into the per-subagent rolling progress so /tasks
                                // and /status can show what it's actually doing. We key by
                                // the parent Task tool_use_id, not the nested tool_use_id.
                                activityTracker.recordSubagentActivity(parentToolUseId, `← ${resolvedName}`);
                            }
                            // The tool result for the original Task block marks end-of-run
                            // only for the legacy synchronous Task protocol. For the current
                            // async Agent protocol this tool_result is just a launch ACK;
                            // the real completion arrives via `task_updated` /
                            // `task_notification`. Skip completion in the async case.
                            if (maybeTask && !asyncLaunch) {
                                activityTracker.recordSubagentCompleted(block.tool_use_id, {
                                    ok: !block.is_error,
                                    summary,
                                });
                            }
                        }
                    }
                    break;
                }
                case "assistant": {
                    const assistantMsg = msg;
                    const content = assistantMsg.message.content;
                    const parentToolUseIdRaw = assistantMsg
                        .parent_tool_use_id;
                    const parentToolUseId = typeof parentToolUseIdRaw === "string" && parentToolUseIdRaw.length > 0
                        ? parentToolUseIdRaw
                        : undefined;
                    const subagentMeta = parentToolUseId
                        ? subagentByTaskToolUseId.get(parentToolUseId)
                        : undefined;
                    for (const block of content) {
                        switch (block.type) {
                            case "text":
                                if (block.text) {
                                    if (parentToolUseId) {
                                        const tag = subagentMeta?.subagent_type
                                            ? `subagent:${subagentMeta.subagent_type}`
                                            : "subagent";
                                        logger.assistant(`[${tag}] ${block.text}`);
                                        // Feed a short excerpt into the subagent's progress
                                        // rolling summary. Keep it short — full text stays in
                                        // the logs; the tracker only needs the latest signal.
                                        const excerpt = block.text.length > 200
                                            ? block.text.substring(0, 200) + "…"
                                            : block.text;
                                        activityTracker.recordSubagentActivity(parentToolUseId, excerpt);
                                    }
                                    else {
                                        logger.assistant(block.text);
                                        activityTracker.recordAssistantText(block.text);
                                    }
                                }
                                break;
                            case "thinking":
                                logger.debug(`[thinking] ${block.thinking.substring(0, 200)}${block.thinking.length > 200 ? "..." : ""}`);
                                break;
                            case "tool_use":
                                {
                                    const toolUseId = block.id ??
                                        block.tool_use_id;
                                    if (toolUseId) {
                                        toolNameById.set(toolUseId, block.name);
                                        activeToolIds.add(toolUseId);
                                        const inputStr = block.input ? JSON.stringify(block.input) : "";
                                        watchdog.trackStart(toolUseId, block.name, inputStr);
                                    }
                                    if (isSubagentSpawnTool(block.name) && toolUseId) {
                                        const input = block.input;
                                        const subagent_type = typeof input.subagent_type === "string"
                                            ? (input.subagent_type)
                                            : undefined;
                                        subagentByTaskToolUseId.set(toolUseId, { subagent_type });
                                        // Register the subagent with the activity tracker so
                                        // operator surfaces (/tasks, /status) can show its
                                        // progress. Prefer `description` from Task input, fall
                                        // back to a truncated prompt. We tolerate missing fields
                                        // — description is cosmetic only.
                                        const descRaw = input.description;
                                        const promptRaw = input.prompt;
                                        const description = typeof descRaw === "string" && descRaw.length > 0
                                            ? descRaw
                                            : typeof promptRaw === "string"
                                                ? promptRaw
                                                : undefined;
                                        activityTracker.recordSubagentStarted(toolUseId, {
                                            subagentType: subagent_type,
                                            description,
                                        });
                                    }
                                    logger.toolCall(block.name, block.input, {
                                        tool_use_id: toolUseId,
                                        ...(parentToolUseId ? { parent_tool_use_id: parentToolUseId } : {}),
                                        ...(subagentMeta?.subagent_type
                                            ? { subagent_type: subagentMeta.subagent_type }
                                            : {}),
                                        ...(subagentMeta?.agentId ? { subagent_agent_id: subagentMeta.agentId } : {}),
                                    });
                                    if (!parentToolUseId) {
                                        const inputStr = block.input ? JSON.stringify(block.input) : "";
                                        activityTracker.recordToolCall(block.name, inputStr);
                                        // Record notify_user messages as assistant text so the fast
                                        // responder knows what we recently told the user.
                                        if (isToolNamed(block.name, "notify_user")) {
                                            const msg = block.input?.message;
                                            if (msg)
                                                activityTracker.recordAssistantText(msg);
                                        }
                                    }
                                    else {
                                        // Nested tool call inside an active subagent. Surface
                                        // the call name (not the full input) so the per-subagent
                                        // rolling progress stays compact — full detail lives in
                                        // the logs.
                                        activityTracker.recordSubagentActivity(parentToolUseId, `→ ${block.name}`);
                                    }
                                }
                                if (!finishCalled && isToolNamed(block.name, "finish")) {
                                    finishCalled = true;
                                    session.closeInput();
                                }
                                if (!finishCalled &&
                                    sessionManager &&
                                    isToolNamed(block.name, "switch_session")) {
                                    const input = block.input;
                                    if (input.target === "general" || input.target === "coding") {
                                        if (input.target === session.mode) {
                                            logger.warn(`Ignoring self-switch: ${session.mode} → ${input.target}`);
                                        }
                                        else {
                                            sessionManager.setSwitchRequest({
                                                target: input.target,
                                                memo: input.memo ?? "",
                                                reason: input.reason === "complete" ? "complete" : "handoff",
                                                forwardMessage: input.forward_message ?? "",
                                                cwd: input.cwd,
                                            });
                                            finishCalled = true;
                                            session.closeInput();
                                        }
                                    }
                                }
                                if (isToolNamed(block.name, "TodoWrite")) {
                                    const input = block.input;
                                    if (Array.isArray(input.todos)) {
                                        activityTracker.recordTodos(input.todos);
                                    }
                                }
                                else if (isToolNamed(block.name, "TaskCreate")) {
                                    // Created task id arrives in the tool result; record the
                                    // create now and finalize it when the result lands.
                                    const createToolUseId = block.id ??
                                        block.tool_use_id;
                                    if (createToolUseId) {
                                        taskTodos.noteCreate(createToolUseId, block.input);
                                    }
                                }
                                else if (isToolNamed(block.name, "TaskUpdate")) {
                                    if (taskTodos.noteUpdate(block.input)) {
                                        activityTracker.recordTodos(taskTodos.project());
                                    }
                                }
                                break;
                            default:
                                logger.debug(`Assistant block: ${block.type}`);
                                break;
                        }
                    }
                    {
                        // Prefer the SDK's explicit `error` field (used by Claude). Some
                        // Pi engines surface API failures by emitting a synthetic assistant
                        // message whose sole content is a text block beginning with
                        // "[Error] ...". Scope that bracketed-text heuristic to Pi-style
                        // sessions only so normal Claude sessions cannot be misclassified
                        // just because the model wrote a literal "[Error]" line.
                        const contentBlocks = assistantMsg.message.content;
                        const onlyTextBlocks = contentBlocks.every((b) => b.type === "text");
                        const isPiStyleTranscript = session.getTranscriptInfo().kind === "pi-coding-agent";
                        const errText = contentBlocks
                            .filter((b) => b.type === "text" && !!b.text)
                            .map((b) => b.text)
                            .join(" ");
                        const bracketedErrorMatch = isPiStyleTranscript && onlyTextBlocks
                            ? /^\s*\[Error\]\s*(.+)/is.exec(errText)
                            : null;
                        const bracketedError = bracketedErrorMatch?.[1]?.trim();
                        const assistantError = assistantMsg.error ?? bracketedError;
                        if (assistantError) {
                            pendingAssistantError = {
                                message: errText && assistantMsg.error
                                    ? `${errText} (${assistantMsg.error})`
                                    : assistantError,
                                skipUsageSnapshot: !assistantMsg.error && !!bracketedError,
                            };
                            logger.err(`Assistant error: ${assistantError}`);
                        }
                        else if (pendingAssistantError?.skipUsageSnapshot && contentBlocks.length > 0) {
                            // A later assistant turn (text, tool_use, thinking, etc.)
                            // completed successfully — the prior Pi-style "[Error] ..."
                            // was a transient/recoverable signal, not a terminal one. Clear
                            // it so the upcoming `result` is not misclassified as fatal.
                            pendingAssistantError = null;
                        }
                    }
                    break;
                }
                case "result": {
                    logResult(msg);
                    if (!pendingAssistantError?.skipUsageSnapshot) {
                        captureUsageSnapshotFromResult(session, msg);
                    }
                    // We throw on:
                    //   - any SDK-reported error (`msg.is_error`, unchanged from before), or
                    //   - a Pi-style synthetic "[Error] ..." assistant message that the
                    //     engine surfaces without setting `is_error`. We intentionally do
                    //     NOT promote a Claude explicit `assistantMsg.error` to a throw
                    //     here unless the SDK itself flags the result as errored — that
                    //     preserves the pre-existing Claude semantics.
                    const promoteSyntheticPiError = pendingAssistantError?.skipUsageSnapshot === true;
                    if (msg.is_error || promoteSyntheticPiError) {
                        const sdkErrors = getResultErrorSummary(msg);
                        const errorDetail = pendingAssistantError?.message ?? (sdkErrors || msg.subtype || "unknown SDK error");
                        throw new SdkTerminalError(`API Error: ${errorDetail}`, {
                            terminalReason: msg.terminal_reason,
                            sdkSubtype: msg.subtype,
                        });
                    }
                    session.closeInput();
                    return { toolTimeout: watchdog.getResult(), finishCalled };
                }
                case "system":
                    if (msg.subtype === "compact_boundary") {
                        const meta = msg.compact_metadata;
                        logger.system(`SDK auto-compacted mid-query (trigger=${meta?.trigger ?? "unknown"}, pre_tokens=${meta?.pre_tokens ?? "?"})`);
                        if ("session_id" in msg && msg.session_id) {
                            session.captureSessionId(msg.session_id);
                        }
                        if (typeof meta?.pre_tokens === "number") {
                            session.capturePostCompactionSnapshot(meta.pre_tokens);
                        }
                    }
                    else if (msg.subtype === "init") {
                        const init = msg;
                        if (Array.isArray(init.mcp_servers)) {
                            try {
                                getAgentState().mcpServers = init.mcp_servers;
                            }
                            catch {
                                // Agent state not yet initialized
                            }
                        }
                        // CLI auto-memory is an opt-in feature on the CLI binary side.
                        // When enabled, every `system/init` carries `memory_paths`
                        // (currently just the `"auto"` scope, e.g.
                        // `~/.claude/projects/<sanitized-cwd>/memory/`). We cache the
                        // mapping in the ActivityTracker so operator surfaces can show
                        // where the CLI is writing/reading memories. Absence is fine —
                        // some CLI builds or --disable-auto-memory configurations
                        // simply won't populate this field.
                        if (init.memory_paths && typeof init.memory_paths === "object") {
                            activityTracker.recordMemoryPaths(init.memory_paths);
                        }
                    }
                    else if (msg.subtype === "status") {
                        const status = msg.status;
                        logger.debug(`SDK status: ${typeof status === "string" ? status : "unknown"}`, { subtype: msg.subtype });
                    }
                    else if (msg.subtype === "memory_recall") {
                        // CLI auto-memory recall event: the background recall supervisor
                        // surfaced one or more memory files for the current turn. We
                        // record each recall for operator observability (/obs, /tasks)
                        // but NEVER forward content into our own transcript — the CLI
                        // has already fed everything into the model's context window.
                        handleMemoryRecallEvent(msg, activityTracker);
                    }
                    else if (msg.subtype === "task_started" ||
                        msg.subtype === "task_progress" ||
                        msg.subtype === "task_updated" ||
                        msg.subtype === "task_notification") {
                        handleAsyncTaskEvent(msg, subagentByTaskToolUseId, activityTracker);
                    }
                    if (msg.subtype !== "status") {
                        // `init` carries the per-turn session_id/mcp_servers metadata that
                        // `data-processing` (WakeCycle.systemInitEntries) uses to map log
                        // cycles to SDK transcripts. Emit it at info level so turn boundaries
                        // are visible in the default console output; other subtypes stay at debug.
                        if (msg.subtype === "init") {
                            logger.info(`System message: ${JSON.stringify(msg)}`, {
                                subtype: msg.subtype,
                            });
                        }
                        else {
                            logger.debug(`System message: ${JSON.stringify(msg)}`, {
                                subtype: msg.subtype,
                            });
                        }
                    }
                    break;
                case "stream_event": {
                    // Partial streaming tokens reset lastEventTime (done at top of loop).
                    // Additionally, extract content signals for the activity tracker so
                    // the fast responder can report what the agent is currently generating.
                    const evt = msg.event;
                    if (evt && typeof evt === "object") {
                        const evtType = evt.type;
                        if (evtType === "content_block_start") {
                            const cb = evt.content_block;
                            const label = cb?.type === "tool_use" ? (cb.name ?? "tool_use") : (cb?.type ?? "content");
                            activityTracker.startStreamingBlock(label);
                        }
                        else if (evtType === "content_block_delta") {
                            const delta = evt.delta;
                            const chars = (delta?.text ?? delta?.partial_json ?? "").length;
                            if (chars > 0)
                                activityTracker.recordStreamingDelta(chars);
                        }
                        else if (evtType === "content_block_stop") {
                            activityTracker.stopStreamingBlock();
                        }
                    }
                    break;
                }
                default: {
                    const rawType = msg.type;
                    if (rawType === "tool_progress") {
                        const tp = msg;
                        if (tp.tool_use_id && typeof tp.elapsed_time_seconds === "number") {
                            const name = tp.tool_name ?? toolNameById.get(tp.tool_use_id);
                            watchdog.handleProgress(tp.tool_use_id, name, tp.elapsed_time_seconds);
                        }
                    }
                    else if (rawType === "tool_result") {
                        const tr = msg;
                        if (tr.tool_use_id) {
                            watchdog.trackEnd(tr.tool_use_id);
                            activeToolIds.delete(tr.tool_use_id);
                        }
                        const name = tr.name ?? "tool";
                        const summary = tr.content === undefined
                            ? ""
                            : typeof tr.content === "string"
                                ? tr.content
                                : JSON.stringify(tr.content);
                        logger.toolResult(name, summary, {
                            tool_use_id: tr.tool_use_id,
                            is_error: tr.is_error,
                        });
                    }
                    break;
                }
            }
        }
    }
    catch (err) {
        if (watchdog.triggered) {
            // Timeout was triggered — the error is expected (AbortError or similar).
            // If it's an unexpected error shape, log it but still treat as timeout.
            const isAbort = err instanceof Error && err.name === "AbortError";
            if (!isAbort) {
                logger.warn(`Timeout active but caught non-AbortError: ${err instanceof Error ? `${err.name}: ${err.message}` : String(err)}`);
            }
            session.closeInput();
            return { toolTimeout: watchdog.getResult(), finishCalled };
        }
        if (silenceState.timedOut) {
            session.closeInput();
            throw new StreamSilenceTimeoutError(Math.round(silenceTimeoutMs / 1000));
        }
        throw err;
    }
    finally {
        clearInterval(silenceTimer);
        watchdog.dispose();
    }
    session.closeInput();
    return { toolTimeout: watchdog.getResult(), finishCalled };
}
//# sourceMappingURL=stream-handler.js.map