"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
    if (value !== null && value !== void 0) {
        if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
        var dispose, inner;
        if (async) {
            if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
            dispose = value[Symbol.asyncDispose];
        }
        if (dispose === void 0) {
            if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
            dispose = value[Symbol.dispose];
            if (async) inner = dispose;
        }
        if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
        if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
        env.stack.push({ value: value, dispose: dispose, async: async });
    }
    else if (async) {
        env.stack.push({ async: true });
    }
    return value;
};
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
    return function (env) {
        function fail(e) {
            env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
            env.hasError = true;
        }
        var r, s = 0;
        function next() {
            while (r = env.stack.pop()) {
                try {
                    if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
                    if (r.dispose) {
                        var result = r.dispose.call(r.value);
                        if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
                    }
                    else s |= 1;
                }
                catch (e) {
                    fail(e);
                }
            }
            if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
            if (env.hasError) throw env.error;
        }
        return next();
    };
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
    var e = new Error(message);
    return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
});
Object.defineProperty(exports, "__esModule", { value: true });
exports.StreamManager = void 0;
const events_1 = require("events");
const path = __importStar(require("path"));
const ai_1 = require("ai");
const result_1 = require("../types/result");
const log_1 = require("./log");
const asyncMutex_1 = require("../utils/concurrency/asyncMutex");
const StreamingTokenTracker_1 = require("../utils/main/StreamingTokenTracker");
const helpers_1 = require("../utils/runtime/helpers");
// Stream state enum for exhaustive checking
var StreamState;
(function (StreamState) {
    StreamState["IDLE"] = "idle";
    StreamState["STARTING"] = "starting";
    StreamState["STREAMING"] = "streaming";
    StreamState["STOPPING"] = "stopping";
    StreamState["ERROR"] = "error";
})(StreamState || (StreamState = {}));
/**
 * Strip encryptedContent from web search results to reduce token usage.
 * The encrypted page content can be massive (4000+ chars per result) and isn't
 * needed for model context. Keep URL, title, and pageAge for reference.
 */
function stripEncryptedContent(output) {
    // Check if output is JSON with a value array (web search results)
    if (typeof output === "object" &&
        output !== null &&
        "type" in output &&
        output.type === "json" &&
        "value" in output &&
        Array.isArray(output.value)) {
        // Strip encryptedContent from each search result
        const strippedValue = output.value.map((item) => {
            if (item && typeof item === "object" && "encryptedContent" in item) {
                // Remove encryptedContent but keep other fields
                const { encryptedContent, ...rest } = item;
                return rest;
            }
            return item;
        });
        return {
            ...output,
            value: strippedValue,
        };
    }
    return output;
}
/**
 * StreamManager - Handles all streaming operations with type safety and atomic operations
 *
 * Key invariants:
 * - Only one active stream per workspace at any time
 * - Atomic stream creation/cancellation operations
 * - Guaranteed resource cleanup in all code paths
 */
class StreamManager extends events_1.EventEmitter {
    workspaceStreams = new Map();
    streamLocks = new Map();
    PARTIAL_WRITE_THROTTLE_MS = 500;
    historyService;
    partialService;
    // Token tracker for live streaming statistics
    tokenTracker = new StreamingTokenTracker_1.StreamingTokenTracker();
    constructor(historyService, partialService) {
        super();
        this.historyService = historyService;
        this.partialService = partialService;
    }
    /**
     * Write the current partial message to disk (throttled by mtime)
     * Ensures writes happen during rapid streaming (crash-resilient)
     */
    async schedulePartialWrite(workspaceId, streamInfo) {
        const now = Date.now();
        const timeSinceLastWrite = now - streamInfo.lastPartialWriteTime;
        // If enough time has passed, write immediately
        if (timeSinceLastWrite >= this.PARTIAL_WRITE_THROTTLE_MS) {
            await this.flushPartialWrite(workspaceId, streamInfo);
            return;
        }
        // Otherwise, schedule write for remaining time (fire-and-forget for scheduled writes)
        if (streamInfo.partialWriteTimer) {
            clearTimeout(streamInfo.partialWriteTimer);
        }
        const remainingTime = this.PARTIAL_WRITE_THROTTLE_MS - timeSinceLastWrite;
        streamInfo.partialWriteTimer = setTimeout(() => {
            void this.flushPartialWrite(workspaceId, streamInfo);
        }, remainingTime);
    }
    /**
     * Flush any pending partial write and write immediately
     * Serializes writes to prevent races - waits for any in-flight write before starting new one
     */
    async flushPartialWrite(workspaceId, streamInfo) {
        // Wait for any in-flight write to complete first (serialization)
        if (streamInfo.partialWritePromise) {
            await streamInfo.partialWritePromise;
        }
        // Clear throttle timer
        if (streamInfo.partialWriteTimer) {
            clearTimeout(streamInfo.partialWriteTimer);
            streamInfo.partialWriteTimer = undefined;
        }
        // Start new write and track the promise
        streamInfo.partialWritePromise = (async () => {
            try {
                const partialMessage = {
                    id: streamInfo.messageId,
                    role: "assistant",
                    metadata: {
                        historySequence: streamInfo.historySequence,
                        timestamp: streamInfo.startTime,
                        model: streamInfo.model,
                        partial: true, // Always true - this method only writes partial messages
                        ...streamInfo.initialMetadata,
                    },
                    parts: streamInfo.parts, // Parts array includes reasoning, text, and tools
                };
                await this.partialService.writePartial(workspaceId, partialMessage);
                streamInfo.lastPartialWriteTime = Date.now();
            }
            catch (error) {
                log_1.log.error("Failed to write partial message:", error);
            }
            finally {
                // Clear promise when write completes
                streamInfo.partialWritePromise = undefined;
            }
        })();
        // Wait for this write to complete
        await streamInfo.partialWritePromise;
    }
    /**
     * Atomically ensures stream safety by cancelling any existing stream
     * @param workspaceId The workspace to ensure stream safety for
     * @returns A unique stream token for the new stream
     */
    async ensureStreamSafety(workspaceId) {
        const existing = this.workspaceStreams.get(workspaceId);
        if (existing && existing.state !== StreamState.IDLE) {
            await this.cancelStreamSafely(workspaceId, existing);
        }
        // Generate unique token for this stream (8 hex chars for context efficiency)
        return Math.random().toString(16).substring(2, 10);
    }
    /**
     * Generate a unique stream token (8 hex characters)
     * Used by callers that need to prepare resources (like tools) before starting the stream
     * Uses 8 hex chars instead of UUID for context efficiency (shorter paths in agent output)
     */
    generateStreamToken() {
        return Math.random().toString(16).substring(2, 10);
    }
    /**
     * Create a temporary directory for a stream token
     * Use ~/.cmux-tmp instead of system temp directory (e.g., /var/folders/...)
     * because macOS user-scoped temp paths are extremely long, which leads to:
     * - Agent mistakes when copying/manipulating paths
     * - Harder to read in tool outputs
     * - Potential path length issues on some systems
     *
     * Uses the Runtime abstraction so temp directories work for both local and SSH runtimes.
     */
    async createTempDirForStream(streamToken, runtime) {
        // Create directory and get absolute path (works for both local and remote)
        // Use 'cd' + 'pwd' to resolve ~ to absolute path
        const command = `mkdir -p ~/.cmux-tmp/${streamToken} && cd ~/.cmux-tmp/${streamToken} && pwd`;
        const result = await (0, helpers_1.execBuffered)(runtime, command, {
            cwd: "/",
            timeout: 10,
        });
        if (result.exitCode !== 0) {
            throw new Error(`Failed to create temp directory ~/.cmux-tmp/${streamToken}: exit code ${result.exitCode}`);
        }
        // Return absolute path (e.g., "/home/user/.cmux-tmp/abc123")
        return result.stdout.trim();
    }
    /**
     * Extracts usage and duration metadata from stream result.
     *
     * Usage is only available after stream completes naturally.
     * On abort, the usage promise may hang - we use a timeout to return quickly.
     */
    emitToolCallDeltaIfPresent(workspaceId, streamInfo, part) {
        const maybeDelta = part;
        if (maybeDelta?.type !== "tool-call-delta") {
            return false;
        }
        const toolDelta = part;
        const deltaText = String(toolDelta.argsTextDelta ?? "");
        if (deltaText.length === 0) {
            return true;
        }
        const tokens = this.tokenTracker.countTokens(deltaText);
        const timestamp = Date.now();
        this.emit("tool-call-delta", {
            type: "tool-call-delta",
            workspaceId: workspaceId,
            messageId: streamInfo.messageId,
            toolCallId: toolDelta.toolCallId,
            toolName: toolDelta.toolName,
            delta: toolDelta.argsTextDelta,
            tokens,
            timestamp,
        });
        return true;
    }
    async getStreamMetadata(streamInfo, timeoutMs = 1000) {
        let usage = undefined;
        try {
            // Race usage retrieval against timeout to prevent hanging on abort
            usage = await Promise.race([
                streamInfo.streamResult.usage,
                new Promise((resolve) => setTimeout(() => resolve(undefined), timeoutMs)),
            ]);
        }
        catch (error) {
            log_1.log.debug("Could not retrieve usage:", error);
        }
        return {
            usage,
            duration: Date.now() - streamInfo.startTime,
        };
    }
    /**
     * Safely cancels an existing stream with proper cleanup
     *
     * CRITICAL: Waits for the processing promise to complete before cleanup.
     * This ensures the old stream fully exits before a new stream can start,
     * preventing concurrent streams and race conditions.
     */
    /**
     * Convert a part to an event and emit it.
     * Shared between live streaming and replay to ensure consistent event emission.
     * This guarantees replay reconstructs the exact stream using the same tokenization logic.
     *
     * @param workspaceId - Workspace identifier
     * @param messageId - Message identifier
     * @param part - The part to emit (text, reasoning, or tool)
     */
    emitPartAsEvent(workspaceId, messageId, part) {
        const timestamp = part.timestamp ?? Date.now();
        if (part.type === "text") {
            const tokens = this.tokenTracker.countTokens(part.text);
            this.emit("stream-delta", {
                type: "stream-delta",
                workspaceId: workspaceId,
                messageId,
                delta: part.text,
                tokens,
                timestamp,
            });
        }
        else if (part.type === "reasoning") {
            const tokens = this.tokenTracker.countTokens(part.text);
            this.emit("reasoning-delta", {
                type: "reasoning-delta",
                workspaceId: workspaceId,
                messageId,
                delta: part.text,
                tokens,
                timestamp,
            });
        }
        else if (part.type === "dynamic-tool") {
            const inputText = JSON.stringify(part.input);
            const tokens = this.tokenTracker.countTokens(inputText);
            this.emit("tool-call-start", {
                type: "tool-call-start",
                workspaceId: workspaceId,
                messageId,
                toolCallId: part.toolCallId,
                toolName: part.toolName,
                args: part.input,
                tokens,
                timestamp,
            });
            // If tool has output, emit completion
            if (part.state === "output-available") {
                this.emit("tool-call-end", {
                    type: "tool-call-end",
                    workspaceId: workspaceId,
                    messageId,
                    toolCallId: part.toolCallId,
                    toolName: part.toolName,
                    result: part.output,
                });
            }
        }
    }
    async cancelStreamSafely(workspaceId, streamInfo) {
        try {
            streamInfo.state = StreamState.STOPPING;
            // Flush any pending partial write immediately (preserves work on interruption)
            await this.flushPartialWrite(workspaceId, streamInfo);
            streamInfo.abortController.abort();
            // CRITICAL: Wait for processing to fully complete before cleanup
            // This prevents race conditions where the old stream is still running
            // while a new stream starts (e.g., old stream writing to partial.json)
            await streamInfo.processingPromise;
            // Get usage and duration metadata (usage may be undefined if aborted early)
            const { usage, duration } = await this.getStreamMetadata(streamInfo);
            // Emit abort event with usage if available
            this.emit("stream-abort", {
                type: "stream-abort",
                workspaceId: workspaceId,
                messageId: streamInfo.messageId,
                metadata: { usage, duration },
            });
            // Clean up immediately
            this.workspaceStreams.delete(workspaceId);
        }
        catch (error) {
            console.error("Error during stream cancellation:", error);
            // Force cleanup even if cancellation fails
            this.workspaceStreams.delete(workspaceId);
        }
    }
    /**
     * Atomically creates a new stream with all necessary setup
     */
    createStreamAtomically(workspaceId, streamToken, runtimeTempDir, runtime, messages, model, modelString, abortSignal, system, historySequence, tools, initialMetadata, providerOptions, maxOutputTokens, toolPolicy) {
        // Create abort controller for this specific stream
        const abortController = new AbortController();
        // Link external abort signal
        if (abortSignal) {
            abortSignal.addEventListener("abort", () => abortController.abort());
        }
        // Determine toolChoice based on toolPolicy
        // If a tool is required (tools object has exactly one tool after applyToolPolicy),
        // force the model to use it with toolChoice: { type: "required", toolName: "..." }
        let toolChoice;
        if (tools && toolPolicy) {
            // Check if any filter has "require" action
            const hasRequireAction = toolPolicy.some((filter) => filter.action === "require");
            if (hasRequireAction && Object.keys(tools).length === 1) {
                const requiredToolName = Object.keys(tools)[0];
                toolChoice = { type: "required", toolName: requiredToolName };
                log_1.log.debug("Setting toolChoice to required", { toolName: requiredToolName });
            }
        }
        // Start streaming - this can throw immediately if API key is missing
        let streamResult;
        try {
            streamResult = (0, ai_1.streamText)({
                model,
                messages,
                system,
                abortSignal: abortController.signal,
                tools,
                // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
                toolChoice: toolChoice, // Force tool use when required by policy
                // When toolChoice is set (required tool), limit to 1 step to prevent infinite loops
                // Otherwise allow effectively unlimited steps (100k) for autonomous multi-turn workflows.
                // IMPORTANT: Models should be able to run for hours or even days calling tools repeatedly
                // to complete complex tasks. The stopWhen condition allows the model to decide when it's done.
                ...(toolChoice ? { maxSteps: 1 } : { stopWhen: (0, ai_1.stepCountIs)(100000) }),
                // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
                providerOptions: providerOptions, // Pass provider-specific options (thinking/reasoning config)
                // Default to 32000 tokens if not specified (Anthropic defaults to 4096)
                maxOutputTokens: maxOutputTokens ?? 32000,
            });
        }
        catch (error) {
            // Clean up abort controller if stream creation fails
            abortController.abort();
            // Re-throw the error to be caught by startStream
            throw error;
        }
        const messageId = `assistant-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
        const streamInfo = {
            state: StreamState.STARTING,
            streamResult,
            abortController,
            messageId,
            token: streamToken,
            startTime: Date.now(),
            model: modelString,
            initialMetadata,
            historySequence,
            parts: [], // Initialize empty parts array
            lastPartialWriteTime: 0, // Initialize to 0 to allow immediate first write
            partialWritePromise: undefined, // No write in flight initially
            processingPromise: Promise.resolve(), // Placeholder, overwritten in startStream
            runtimeTempDir, // Stream-scoped temp directory for tool outputs
            runtime, // Runtime for temp directory cleanup
        };
        // Atomically register the stream
        this.workspaceStreams.set(workspaceId, streamInfo);
        return streamInfo;
    }
    /**
     * Complete a tool call by updating its part and emitting tool-call-end event
     */
    completeToolCall(workspaceId, streamInfo, toolCalls, toolCallId, toolName, output) {
        // Find and update the existing tool part
        const existingPartIndex = streamInfo.parts.findIndex((p) => p.type === "dynamic-tool" && p.toolCallId === toolCallId);
        if (existingPartIndex !== -1) {
            const existingPart = streamInfo.parts[existingPartIndex];
            if (existingPart.type === "dynamic-tool") {
                streamInfo.parts[existingPartIndex] = {
                    ...existingPart,
                    state: "output-available",
                    output,
                };
            }
        }
        else {
            // Fallback: part not found (shouldn't happen for errors, but can happen for results)
            // This case exists in tool-result but we'll keep it for robustness
            const toolCall = toolCalls.get(toolCallId);
            if (toolCall) {
                streamInfo.parts.push({
                    type: "dynamic-tool",
                    toolCallId,
                    toolName,
                    state: "output-available",
                    input: toolCall.input,
                    output,
                });
            }
        }
        // Emit tool-call-end event
        this.emit("tool-call-end", {
            type: "tool-call-end",
            workspaceId: workspaceId,
            messageId: streamInfo.messageId,
            toolCallId,
            toolName,
            result: output,
        });
        // Schedule partial write
        void this.schedulePartialWrite(workspaceId, streamInfo);
    }
    /**
     * Processes a stream with guaranteed cleanup, regardless of success or failure
     */
    async processStreamWithCleanup(workspaceId, streamInfo, historySequence) {
        try {
            // Update state to streaming
            streamInfo.state = StreamState.STREAMING;
            // Emit stream start event
            this.emit("stream-start", {
                type: "stream-start",
                workspaceId: workspaceId,
                messageId: streamInfo.messageId,
                model: streamInfo.model,
                historySequence,
            });
            // Initialize token tracker for this model
            this.tokenTracker.setModel(streamInfo.model);
            // Use fullStream to capture all events including tool calls
            const toolCalls = new Map();
            for await (const part of streamInfo.streamResult.fullStream) {
                // Check if stream was cancelled BEFORE processing any parts
                // This improves interruption responsiveness by catching aborts earlier
                if (streamInfo.abortController.signal.aborted) {
                    log_1.log.debug("streamManager: Stream aborted, breaking from loop");
                    break;
                }
                // Log all stream parts to debug reasoning (commented out - too spammy)
                // log.debug("streamManager: Stream part", {
                //   type: part.type,
                //   hasText: "text" in part,
                //   preview: "text" in part ? (part as StreamPartWithText).text?.substring(0, 50) : undefined,
                // });
                switch (part.type) {
                    case "text-delta": {
                        // Append each delta as a new part (merging happens at display time)
                        const textPart = {
                            type: "text",
                            text: part.text,
                            timestamp: Date.now(),
                        };
                        streamInfo.parts.push(textPart);
                        // Emit using shared logic (ensures replay consistency)
                        this.emitPartAsEvent(workspaceId, streamInfo.messageId, textPart);
                        // Schedule partial write (throttled, fire-and-forget to not block stream)
                        void this.schedulePartialWrite(workspaceId, streamInfo);
                        break;
                    }
                    default: {
                        if (this.emitToolCallDeltaIfPresent(workspaceId, streamInfo, part)) {
                            break;
                        }
                        break;
                    }
                    case "reasoning-delta": {
                        // Both Anthropic and OpenAI use reasoning-delta for streaming reasoning content
                        const delta = part.text ?? "";
                        // Append each delta as a new part (merging happens at display time)
                        const reasoningPart = {
                            type: "reasoning",
                            text: delta,
                            timestamp: Date.now(),
                        };
                        streamInfo.parts.push(reasoningPart);
                        // Emit using shared logic (ensures replay consistency)
                        this.emitPartAsEvent(workspaceId, streamInfo.messageId, reasoningPart);
                        void this.schedulePartialWrite(workspaceId, streamInfo);
                        break;
                    }
                    case "reasoning-end": {
                        // Reasoning-end is just a signal - no state to update
                        this.emit("reasoning-end", {
                            type: "reasoning-end",
                            workspaceId: workspaceId,
                            messageId: streamInfo.messageId,
                        });
                        break;
                    }
                    case "tool-call": {
                        // Tool call started - store in map for later lookup
                        toolCalls.set(part.toolCallId, {
                            toolCallId: part.toolCallId,
                            toolName: part.toolName,
                            input: part.input,
                        });
                        // Note: Tool availability is handled by the SDK, which emits tool-error events
                        // for unavailable tools. No need to check here.
                        // IMPORTANT: Add tool part to streamInfo.parts immediately (not just on completion)
                        // This ensures in-progress tool calls are saved to partial.json if stream is interrupted
                        const toolPart = {
                            type: "dynamic-tool",
                            toolCallId: part.toolCallId,
                            toolName: part.toolName,
                            state: "input-available",
                            // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
                            input: part.input,
                            timestamp: Date.now(),
                        };
                        streamInfo.parts.push(toolPart);
                        // Emit using shared logic (ensures replay consistency)
                        const inputText = JSON.stringify(part.input);
                        log_1.log.debug(`[StreamManager] tool-call: toolName=${part.toolName}, input length=${inputText.length}`);
                        this.emitPartAsEvent(workspaceId, streamInfo.messageId, toolPart);
                        break;
                    }
                    case "tool-result": {
                        // Tool call completed successfully
                        const toolCall = toolCalls.get(part.toolCallId);
                        if (toolCall) {
                            // Strip encrypted content from web search results before storing
                            const strippedOutput = stripEncryptedContent(part.output);
                            toolCall.output = strippedOutput;
                            // Use shared completion logic
                            this.completeToolCall(workspaceId, streamInfo, toolCalls, part.toolCallId, part.toolName, strippedOutput);
                        }
                        break;
                    }
                    // Handle tool-error parts from the stream (AI SDK 5.0+)
                    // These are emitted when tool execution fails (e.g., tool doesn't exist)
                    case "tool-error": {
                        const toolErrorPart = part;
                        log_1.log.error(`Tool execution error for '${toolErrorPart.toolName}'`, {
                            toolCallId: toolErrorPart.toolCallId,
                            error: toolErrorPart.error,
                        });
                        // Format error output
                        const errorOutput = {
                            error: typeof toolErrorPart.error === "string"
                                ? toolErrorPart.error
                                : toolErrorPart.error instanceof Error
                                    ? toolErrorPart.error.message
                                    : JSON.stringify(toolErrorPart.error),
                        };
                        // Use shared completion logic
                        this.completeToolCall(workspaceId, streamInfo, toolCalls, toolErrorPart.toolCallId, toolErrorPart.toolName, errorOutput);
                        break;
                    }
                    // Handle error parts from the stream (e.g., OpenAI context_length_exceeded)
                    case "error": {
                        // Capture the error and immediately throw to trigger error handling
                        // Error parts are structured errors from the AI SDK
                        const errorPart = part;
                        // Try to extract error message from various possible structures
                        let errorMessage;
                        if (errorPart.error instanceof Error) {
                            throw errorPart.error;
                        }
                        else if (typeof errorPart.error === "object" && errorPart.error !== null) {
                            const errorObj = errorPart.error;
                            // Check for nested error object with message (OpenAI format)
                            if (errorObj.error && typeof errorObj.error === "object" && errorObj.error !== null) {
                                const nestedError = errorObj.error;
                                if (typeof nestedError.message === "string") {
                                    errorMessage = nestedError.message;
                                }
                            }
                            // Fallback to direct message property
                            errorMessage ?? (errorMessage = typeof errorObj.message === "string" ? errorObj.message : undefined);
                            // Last resort: stringify the error
                            errorMessage ?? (errorMessage = JSON.stringify(errorObj));
                            const error = new Error(errorMessage);
                            // Preserve original error as cause for debugging
                            Object.assign(error, { cause: errorObj });
                            throw error;
                        }
                        else {
                            throw new Error(String(errorPart.error));
                        }
                    }
                    // Handle other event types as needed
                    case "start":
                    case "start-step":
                    case "text-start":
                    case "finish":
                    case "finish-step":
                        // These events can be logged or handled if needed
                        break;
                }
            }
            // No need to save remaining text - text-delta handler already maintains parts array
            // (Removed duplicate push that was causing double text parts)
            // Flush final state to partial.json for crash resilience
            // This happens regardless of abort status to ensure the final state is persisted to disk
            // On abort: second flush after cancelStreamSafely, ensures all streamed content is saved
            // On normal completion: provides crash resilience before AIService writes to chat.jsonl
            await this.flushPartialWrite(workspaceId, streamInfo);
            // Check if stream completed successfully
            if (!streamInfo.abortController.signal.aborted) {
                // Get usage, duration, and provider metadata from stream result
                const { usage, duration } = await this.getStreamMetadata(streamInfo);
                const providerMetadata = await streamInfo.streamResult.providerMetadata;
                // Emit stream end event with parts preserved in temporal order
                const streamEndEvent = {
                    type: "stream-end",
                    workspaceId: workspaceId,
                    messageId: streamInfo.messageId,
                    metadata: {
                        ...streamInfo.initialMetadata, // AIService-provided metadata (systemMessageTokens, etc)
                        model: streamInfo.model,
                        usage, // AI SDK normalized usage
                        providerMetadata, // Raw provider metadata
                        duration,
                    },
                    parts: streamInfo.parts, // Parts array with temporal ordering (includes reasoning)
                };
                this.emit("stream-end", streamEndEvent);
                // Update history with final message (only if there are parts)
                if (streamInfo.parts && streamInfo.parts.length > 0) {
                    const finalAssistantMessage = {
                        id: streamInfo.messageId,
                        role: "assistant",
                        metadata: {
                            ...streamEndEvent.metadata,
                            historySequence: streamInfo.historySequence,
                        },
                        parts: streamInfo.parts,
                    };
                    // CRITICAL: Delete partial.json before updating chat.jsonl
                    // On successful completion, partial.json becomes stale and must be removed
                    await this.partialService.deletePartial(workspaceId);
                    // Update the placeholder message in chat.jsonl with final content
                    await this.historyService.updateHistory(workspaceId, finalAssistantMessage);
                }
            }
        }
        catch (error) {
            streamInfo.state = StreamState.ERROR;
            // Log the actual error for debugging
            console.error("Stream processing error:", error);
            // Extract error message (errors thrown from 'error' parts already have the correct message)
            let errorMessage = error instanceof Error ? error.message : String(error);
            let actualError = error;
            // For categorization, use the cause if available (preserves the original error structure)
            if (error instanceof Error && error.cause) {
                actualError = error.cause;
            }
            let errorType = this.categorizeError(actualError);
            // Detect and enhance model-not-found errors
            if (ai_1.APICallError.isInstance(actualError)) {
                const apiError = actualError;
                // Type guard for error data structure
                const hasErrorProperty = (data) => {
                    return (typeof data === "object" &&
                        data !== null &&
                        "error" in data &&
                        typeof data.error === "object" &&
                        data.error !== null);
                };
                // OpenAI: 400 with error.code === 'model_not_found'
                const isOpenAIModelError = apiError.statusCode === 400 &&
                    hasErrorProperty(apiError.data) &&
                    apiError.data.error.code === "model_not_found";
                // Anthropic: 404 with error.type === 'not_found_error'
                const isAnthropicModelError = apiError.statusCode === 404 &&
                    hasErrorProperty(apiError.data) &&
                    apiError.data.error.type === "not_found_error";
                if (isOpenAIModelError || isAnthropicModelError) {
                    errorType = "model_not_found";
                    // Extract model name from model string (e.g., "anthropic:sonnet-1m" -> "sonnet-1m")
                    const [, modelName] = streamInfo.model.split(":");
                    errorMessage = `Model '${modelName || streamInfo.model}' does not exist or is not available. Please check your model selection.`;
                }
            }
            // If we detect API key issues in the error message, override the type
            if (errorMessage.toLowerCase().includes("api key") ||
                errorMessage.toLowerCase().includes("api_key") ||
                errorMessage.toLowerCase().includes("anthropic_api_key")) {
                errorType = "authentication";
            }
            // Write error metadata to partial.json for persistence across reloads
            const errorPartialMessage = {
                id: streamInfo.messageId,
                role: "assistant",
                metadata: {
                    historySequence: streamInfo.historySequence,
                    timestamp: streamInfo.startTime,
                    model: streamInfo.model,
                    partial: true,
                    error: errorMessage,
                    errorType,
                    ...streamInfo.initialMetadata,
                },
                parts: streamInfo.parts,
            };
            // Write error state to disk (fire-and-forget to not block error emission)
            void this.partialService.writePartial(workspaceId, errorPartialMessage);
            // Emit error event
            this.emit("error", {
                type: "error",
                workspaceId: workspaceId,
                messageId: streamInfo.messageId,
                error: errorMessage,
                errorType: errorType,
            });
        }
        finally {
            // Guaranteed cleanup in all code paths
            // Clear any pending timers to prevent keeping process alive
            if (streamInfo.partialWriteTimer) {
                clearTimeout(streamInfo.partialWriteTimer);
                streamInfo.partialWriteTimer = undefined;
            }
            // Clean up stream temp directory using runtime (fire-and-forget)
            // Don't block stream completion waiting for directory deletion
            // This is especially important for SSH where rm -rf can take 500ms-2s
            if (streamInfo.runtimeTempDir) {
                // Use parent directory as cwd for safety - if runtimeTempDir is malformed,
                // we won't accidentally run rm -rf from root
                const tempDirBasename = path.basename(streamInfo.runtimeTempDir);
                const tempDirParent = path.dirname(streamInfo.runtimeTempDir);
                void streamInfo.runtime
                    .exec(`rm -rf "${tempDirBasename}"`, {
                    cwd: tempDirParent,
                    timeout: 10,
                })
                    .then(async (result) => {
                    await result.exitCode;
                    log_1.log.debug(`Cleaned up temp dir: ${streamInfo.runtimeTempDir}`);
                })
                    .catch((error) => {
                    log_1.log.error(`Failed to cleanup temp dir ${streamInfo.runtimeTempDir}:`, error);
                });
            }
            this.workspaceStreams.delete(workspaceId);
        }
    }
    /**
     * Converts errors to strongly-typed SendMessageError
     */
    convertToSendMessageError(error) {
        // Check for specific AI SDK errors using type guards
        if (ai_1.LoadAPIKeyError.isInstance(error)) {
            return {
                type: "api_key_not_found",
                provider: "anthropic", // We can infer this from LoadAPIKeyError context
            };
        }
        // TODO: Add more specific error types as needed
        // if (APICallError.isInstance(error)) {
        //   if (error.statusCode === 401) return { type: "authentication", ... };
        //   if (error.statusCode === 429) return { type: "rate_limit", ... };
        // }
        // if (RetryError.isInstance(error)) {
        //   return { type: "retry_failed", ... };
        // }
        // Fallback for unknown errors
        const message = error instanceof Error ? error.message : String(error);
        return { type: "unknown", raw: message };
    }
    /**
     * Categorizes errors for better error handling (used for event emission)
     */
    categorizeError(error) {
        // Use AI SDK error type guards first
        if (ai_1.LoadAPIKeyError.isInstance(error)) {
            return "authentication";
        }
        if (ai_1.APICallError.isInstance(error)) {
            if (error.statusCode === 401)
                return "authentication";
            if (error.statusCode === 429)
                return "rate_limit";
            if (error.statusCode && error.statusCode >= 500)
                return "server_error";
            // Check for Anthropic context exceeded errors
            if (error.message.includes("prompt is too long:")) {
                return "context_exceeded";
            }
            return "api";
        }
        if (ai_1.RetryError.isInstance(error)) {
            return "retry_failed";
        }
        // Check for OpenAI/Anthropic structured error format (from error.cause)
        // Structure: { error: { code: 'context_length_exceeded', type: '...', message: '...' } }
        if (typeof error === "object" &&
            error !== null &&
            "error" in error &&
            typeof error.error === "object" &&
            error.error !== null) {
            const structuredError = error.error;
            // OpenAI context length errors have code: 'context_length_exceeded'
            if (structuredError.code === "context_length_exceeded") {
                return "context_exceeded";
            }
            // Check for other specific error codes/types
            if (structuredError.code === "rate_limit_exceeded") {
                return "rate_limit";
            }
        }
        // Fall back to string matching for other errors
        if (error instanceof Error) {
            const message = error.message.toLowerCase();
            if (error.name === "AbortError" || message.includes("abort")) {
                return "aborted";
            }
            else if (message.includes("network") || message.includes("fetch")) {
                return "network";
            }
            else if (message.includes("token") ||
                message.includes("context") ||
                message.includes("too long") ||
                message.includes("maximum")) {
                return "context_exceeded";
            }
            else if (message.includes("quota") || message.includes("limit")) {
                return "quota";
            }
            else if (message.includes("auth") || message.includes("key")) {
                return "authentication";
            }
            else {
                return "api";
            }
        }
        return "unknown";
    }
    /**
     * Starts a new stream for a workspace, automatically cancelling any existing stream
     *
     * Uses per-workspace mutex to prevent concurrent streams. The mutex ensures:
     * 1. Only one startStream can execute at a time per workspace
     * 2. Old stream fully exits before new stream starts
     * 3. No race conditions in stream registration or cleanup
     */
    async startStream(workspaceId, messages, model, modelString, historySequence, system, runtime, abortSignal, tools, initialMetadata, providerOptions, maxOutputTokens, toolPolicy, providedStreamToken) {
        const typedWorkspaceId = workspaceId;
        // Get or create mutex for this workspace
        if (!this.streamLocks.has(typedWorkspaceId)) {
            this.streamLocks.set(typedWorkspaceId, new asyncMutex_1.AsyncMutex());
        }
        const mutex = this.streamLocks.get(typedWorkspaceId);
        try {
            const env_1 = { stack: [], error: void 0, hasError: false };
            try {
                // Acquire lock - guarantees only one startStream per workspace
                // Lock is automatically released when scope exits via Symbol.asyncDispose
                const _lock = __addDisposableResource(env_1, await mutex.acquire(), true);
                // DEBUG: Log stream start
                log_1.log.debug(`[STREAM START] workspaceId=${workspaceId} historySequence=${historySequence} model=${modelString}`);
                // Step 1: Cancel any existing stream before proceeding
                // This must happen regardless of whether a token was provided
                await this.ensureStreamSafety(typedWorkspaceId);
                // Step 2: Use provided stream token or generate a new one
                const streamToken = providedStreamToken ?? this.generateStreamToken();
                // Step 3: Create temp directory for this stream using runtime
                // If token was provided, temp dir might already exist - mkdir -p handles this
                const runtimeTempDir = await this.createTempDirForStream(streamToken, runtime);
                // Step 4: Atomic stream creation and registration
                const streamInfo = this.createStreamAtomically(typedWorkspaceId, streamToken, runtimeTempDir, runtime, messages, model, modelString, abortSignal, system, historySequence, tools, initialMetadata, providerOptions, maxOutputTokens, toolPolicy);
                // Step 5: Track the processing promise for guaranteed cleanup
                // This allows cancelStreamSafely to wait for full exit
                streamInfo.processingPromise = this.processStreamWithCleanup(typedWorkspaceId, streamInfo, historySequence).catch((error) => {
                    console.error("Unexpected error in stream processing:", error);
                });
                return (0, result_1.Ok)(streamToken);
            }
            catch (e_1) {
                env_1.error = e_1;
                env_1.hasError = true;
            }
            finally {
                const result_2 = __disposeResources(env_1);
                if (result_2)
                    await result_2;
            }
        }
        catch (error) {
            // Guaranteed cleanup on any failure
            this.workspaceStreams.delete(typedWorkspaceId);
            // Convert to strongly-typed error
            return (0, result_1.Err)(this.convertToSendMessageError(error));
        }
    }
    /**
     * Stops an active stream for a workspace
     */
    async stopStream(workspaceId) {
        const typedWorkspaceId = workspaceId;
        try {
            const streamInfo = this.workspaceStreams.get(typedWorkspaceId);
            if (streamInfo) {
                await this.cancelStreamSafely(typedWorkspaceId, streamInfo);
            }
            return (0, result_1.Ok)(undefined);
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            return (0, result_1.Err)(`Failed to stop stream: ${message}`);
        }
    }
    /**
     * Gets the current stream state for a workspace
     */
    getStreamState(workspaceId) {
        const typedWorkspaceId = workspaceId;
        const streamInfo = this.workspaceStreams.get(typedWorkspaceId);
        return streamInfo?.state ?? StreamState.IDLE;
    }
    /**
     * Checks if a workspace currently has an active stream
     */
    isStreaming(workspaceId) {
        const state = this.getStreamState(workspaceId);
        return state === StreamState.STARTING || state === StreamState.STREAMING;
    }
    /**
     * Gets all active workspace streams (for debugging/monitoring)
     */
    getActiveStreams() {
        return Array.from(this.workspaceStreams.keys()).map((id) => id);
    }
    /**
     * Gets the current stream info for a workspace if actively streaming
     * Returns undefined if no active stream exists
     * Used to re-establish streaming context on frontend reconnection
     */
    getStreamInfo(workspaceId) {
        const typedWorkspaceId = workspaceId;
        const streamInfo = this.workspaceStreams.get(typedWorkspaceId);
        // Only return info if stream is actively running
        if (streamInfo &&
            (streamInfo.state === StreamState.STARTING || streamInfo.state === StreamState.STREAMING)) {
            return {
                messageId: streamInfo.messageId,
                model: streamInfo.model,
                historySequence: streamInfo.historySequence,
                parts: streamInfo.parts,
            };
        }
        return undefined;
    }
    /**
     * Replay stream events
     * Emits the same events (stream-start, stream-delta, etc.) that would be emitted during live streaming
     * This allows replay to flow through the same event path as live streaming (no duplication)
     */
    replayStream(workspaceId) {
        const typedWorkspaceId = workspaceId;
        const streamInfo = this.workspaceStreams.get(typedWorkspaceId);
        // Only replay if stream is actively running
        if (!streamInfo ||
            (streamInfo.state !== StreamState.STARTING && streamInfo.state !== StreamState.STREAMING)) {
            return;
        }
        // Initialize token tracker for this model (required for tokenization)
        this.tokenTracker.setModel(streamInfo.model);
        // Emit stream-start event
        this.emit("stream-start", {
            type: "stream-start",
            workspaceId,
            messageId: streamInfo.messageId,
            model: streamInfo.model,
            historySequence: streamInfo.historySequence,
        });
        // Replay accumulated parts as events using shared emission logic
        // This guarantees replay produces identical events to the original stream
        for (const part of streamInfo.parts) {
            this.emitPartAsEvent(typedWorkspaceId, streamInfo.messageId, part);
        }
    }
    /**
     * DEBUG ONLY: Trigger an artificial stream error for testing
     * This method allows integration tests to simulate stream errors without
     * mocking the AI SDK or network layer. It triggers the same error handling
     * path as genuine stream errors by aborting the stream and manually triggering
     * the error event (since abort alone doesn't throw, it just sets a flag).
     */
    debugTriggerStreamError(workspaceId, errorMessage) {
        const typedWorkspaceId = workspaceId;
        const streamInfo = this.workspaceStreams.get(typedWorkspaceId);
        // Only trigger error if stream is actively running
        if (!streamInfo ||
            (streamInfo.state !== StreamState.STARTING && streamInfo.state !== StreamState.STREAMING)) {
            return false;
        }
        // Abort the stream first
        streamInfo.abortController.abort(new Error(errorMessage));
        // Update streamInfo metadata with error (so subsequent flushes preserve it)
        streamInfo.initialMetadata = {
            ...streamInfo.initialMetadata,
            error: errorMessage,
            errorType: "network",
        };
        // Write error state to partial.json (same as real error handling)
        const errorPartialMessage = {
            id: streamInfo.messageId,
            role: "assistant",
            metadata: {
                historySequence: streamInfo.historySequence,
                timestamp: streamInfo.startTime,
                model: streamInfo.model,
                partial: true,
                error: errorMessage,
                errorType: "network", // Test errors are network-like
                ...streamInfo.initialMetadata,
            },
            parts: streamInfo.parts,
        };
        void this.partialService.writePartial(workspaceId, errorPartialMessage);
        // Emit error event (same as real error handling)
        this.emit("error", {
            type: "error",
            workspaceId,
            messageId: streamInfo.messageId,
            error: errorMessage,
            errorType: "network",
        });
        return true;
    }
}
exports.StreamManager = StreamManager;
//# sourceMappingURL=streamManager.js.map