"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 __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AgentSession = void 0;
const assert_1 = __importDefault(require("../utils/assert"));
const events_1 = require("events");
const path = __importStar(require("path"));
const message_1 = require("../types/message");
const sendMessageError_1 = require("../services/utils/sendMessageError");
const result_1 = require("../types/result");
const policy_1 = require("../utils/thinking/policy");
const tokenizer_1 = require("../utils/main/tokenizer");
const runtimeFactory_1 = require("../runtime/runtimeFactory");
class AgentSession {
    workspaceId;
    config;
    historyService;
    partialService;
    aiService;
    initStateManager;
    emitter = new events_1.EventEmitter();
    aiListeners = [];
    initListeners = [];
    disposed = false;
    constructor(options) {
        (0, assert_1.default)(options, "AgentSession requires options");
        const { workspaceId, config, historyService, partialService, aiService, initStateManager } = options;
        (0, assert_1.default)(typeof workspaceId === "string", "workspaceId must be a string");
        const trimmedWorkspaceId = workspaceId.trim();
        (0, assert_1.default)(trimmedWorkspaceId.length > 0, "workspaceId must not be empty");
        this.workspaceId = trimmedWorkspaceId;
        this.config = config;
        this.historyService = historyService;
        this.partialService = partialService;
        this.aiService = aiService;
        this.initStateManager = initStateManager;
        this.attachAiListeners();
        this.attachInitListeners();
    }
    dispose() {
        if (this.disposed) {
            return;
        }
        this.disposed = true;
        for (const { event, handler } of this.aiListeners) {
            this.aiService.off(event, handler);
        }
        this.aiListeners.length = 0;
        for (const { event, handler } of this.initListeners) {
            this.initStateManager.off(event, handler);
        }
        this.initListeners.length = 0;
        this.emitter.removeAllListeners();
    }
    onChatEvent(listener) {
        (0, assert_1.default)(typeof listener === "function", "listener must be a function");
        this.emitter.on("chat-event", listener);
        return () => {
            this.emitter.off("chat-event", listener);
        };
    }
    onMetadataEvent(listener) {
        (0, assert_1.default)(typeof listener === "function", "listener must be a function");
        this.emitter.on("metadata-event", listener);
        return () => {
            this.emitter.off("metadata-event", listener);
        };
    }
    async subscribeChat(listener) {
        this.assertNotDisposed("subscribeChat");
        (0, assert_1.default)(typeof listener === "function", "listener must be a function");
        const unsubscribe = this.onChatEvent(listener);
        await this.emitHistoricalEvents(listener);
        return unsubscribe;
    }
    async replayHistory(listener) {
        this.assertNotDisposed("replayHistory");
        (0, assert_1.default)(typeof listener === "function", "listener must be a function");
        await this.emitHistoricalEvents(listener);
    }
    emitMetadata(metadata) {
        this.assertNotDisposed("emitMetadata");
        this.emitter.emit("metadata-event", {
            workspaceId: this.workspaceId,
            metadata,
        });
    }
    async emitHistoricalEvents(listener) {
        // Load chat history (persisted messages from chat.jsonl)
        const historyResult = await this.historyService.getHistory(this.workspaceId);
        if (historyResult.success) {
            for (const message of historyResult.data) {
                listener({ workspaceId: this.workspaceId, message });
            }
        }
        // Check for interrupted streams (active streaming state)
        const streamInfo = this.aiService.getStreamInfo(this.workspaceId);
        const partial = await this.partialService.readPartial(this.workspaceId);
        if (streamInfo) {
            this.aiService.replayStream(this.workspaceId);
        }
        else if (partial) {
            listener({ workspaceId: this.workspaceId, message: partial });
        }
        // Replay init state BEFORE caught-up (treat as historical data)
        // This ensures init events are buffered correctly by the frontend,
        // preserving their natural timing characteristics from the hook execution.
        await this.initStateManager.replayInit(this.workspaceId);
        // Send caught-up after ALL historical data (including init events)
        // This signals frontend that replay is complete and future events are real-time
        listener({
            workspaceId: this.workspaceId,
            message: { type: "caught-up" },
        });
    }
    ensureMetadata(args) {
        this.assertNotDisposed("ensureMetadata");
        (0, assert_1.default)(args, "ensureMetadata requires arguments");
        const { workspacePath, projectName } = args;
        (0, assert_1.default)(typeof workspacePath === "string", "workspacePath must be a string");
        const trimmedWorkspacePath = workspacePath.trim();
        (0, assert_1.default)(trimmedWorkspacePath.length > 0, "workspacePath must not be empty");
        const normalizedWorkspacePath = path.resolve(trimmedWorkspacePath);
        const existing = this.aiService.getWorkspaceMetadata(this.workspaceId);
        if (existing.success) {
            // Metadata already exists, verify workspace path matches
            const metadata = existing.data;
            // For in-place workspaces (projectPath === name), use path directly
            // Otherwise reconstruct using runtime's worktree pattern
            const isInPlace = metadata.projectPath === metadata.name;
            const expectedPath = isInPlace
                ? metadata.projectPath
                : (() => {
                    const runtime = (0, runtimeFactory_1.createRuntime)(metadata.runtimeConfig ?? { type: "local", srcBaseDir: this.config.srcDir });
                    return runtime.getWorkspacePath(metadata.projectPath, metadata.name);
                })();
            (0, assert_1.default)(expectedPath === normalizedWorkspacePath, `Existing metadata workspace path mismatch for ${this.workspaceId}: expected ${expectedPath}, got ${normalizedWorkspacePath}`);
            return;
        }
        // Detect in-place workspace: if workspacePath is not under srcBaseDir,
        // it's a direct workspace (e.g., for CLI/benchmarks) rather than a worktree
        const srcBaseDir = this.config.srcDir;
        const normalizedSrcBaseDir = path.resolve(srcBaseDir);
        const isUnderSrcBaseDir = normalizedWorkspacePath.startsWith(normalizedSrcBaseDir + path.sep);
        let derivedProjectPath;
        let workspaceName;
        let derivedProjectName;
        if (isUnderSrcBaseDir) {
            // Standard worktree mode: workspace is under ~/.cmux/src/project/branch
            derivedProjectPath = path.dirname(normalizedWorkspacePath);
            workspaceName = path.basename(normalizedWorkspacePath);
            derivedProjectName =
                projectName && projectName.trim().length > 0
                    ? projectName.trim()
                    : path.basename(derivedProjectPath) || "unknown";
        }
        else {
            // In-place mode: workspace is a standalone directory
            // Store the workspace path directly by setting projectPath === name
            derivedProjectPath = normalizedWorkspacePath;
            workspaceName = normalizedWorkspacePath;
            derivedProjectName =
                projectName && projectName.trim().length > 0
                    ? projectName.trim()
                    : path.basename(normalizedWorkspacePath) || "unknown";
        }
        const metadata = {
            id: this.workspaceId,
            name: workspaceName,
            projectName: derivedProjectName,
            projectPath: derivedProjectPath,
        };
        // Write metadata directly to config.json (single source of truth)
        this.config.addWorkspace(derivedProjectPath, metadata);
        this.emitMetadata(metadata);
    }
    async sendMessage(message, options) {
        this.assertNotDisposed("sendMessage");
        (0, assert_1.default)(typeof message === "string", "sendMessage requires a string message");
        const trimmedMessage = message.trim();
        const imageParts = options?.imageParts;
        if (trimmedMessage.length === 0 && (!imageParts || imageParts.length === 0)) {
            return (0, result_1.Err)((0, sendMessageError_1.createUnknownSendMessageError)("Empty message not allowed. Use interruptStream() to interrupt active streams."));
        }
        if (options?.editMessageId) {
            const truncateResult = await this.historyService.truncateAfterMessage(this.workspaceId, options.editMessageId);
            if (!truncateResult.success) {
                return (0, result_1.Err)((0, sendMessageError_1.createUnknownSendMessageError)(truncateResult.error));
            }
        }
        const messageId = `user-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
        const additionalParts = imageParts && imageParts.length > 0
            ? imageParts.map((img, index) => {
                (0, assert_1.default)(typeof img.url === "string", `image part [${index}] must include url string content (got ${typeof img.url}): ${JSON.stringify(img).slice(0, 200)}`);
                (0, assert_1.default)(img.url.startsWith("data:"), `image part [${index}] url must be a data URL (got: ${img.url.slice(0, 50)}...)`);
                (0, assert_1.default)(typeof img.mediaType === "string" && img.mediaType.trim().length > 0, `image part [${index}] must include a mediaType (got ${typeof img.mediaType}): ${JSON.stringify(img).slice(0, 200)}`);
                return {
                    type: "file",
                    url: img.url,
                    mediaType: img.mediaType,
                };
            })
            : undefined;
        const userMessage = (0, message_1.createCmuxMessage)(messageId, "user", message, {
            timestamp: Date.now(),
            toolPolicy: options?.toolPolicy,
            cmuxMetadata: options?.cmuxMetadata, // Pass through frontend metadata as black-box
        }, additionalParts);
        const appendResult = await this.historyService.appendToHistory(this.workspaceId, userMessage);
        if (!appendResult.success) {
            return (0, result_1.Err)((0, sendMessageError_1.createUnknownSendMessageError)(appendResult.error));
        }
        this.emitChatEvent(userMessage);
        if (!options?.model || options.model.trim().length === 0) {
            return (0, result_1.Err)((0, sendMessageError_1.createUnknownSendMessageError)("No model specified. Please select a model using /model."));
        }
        return this.streamWithHistory(options.model, options);
    }
    async resumeStream(options) {
        this.assertNotDisposed("resumeStream");
        (0, assert_1.default)(options, "resumeStream requires options");
        const { model } = options;
        (0, assert_1.default)(typeof model === "string" && model.trim().length > 0, "resumeStream requires a model");
        if (this.aiService.isStreaming(this.workspaceId)) {
            return (0, result_1.Ok)(undefined);
        }
        return this.streamWithHistory(model, options);
    }
    async interruptStream() {
        this.assertNotDisposed("interruptStream");
        if (!this.aiService.isStreaming(this.workspaceId)) {
            return (0, result_1.Ok)(undefined);
        }
        const stopResult = await this.aiService.stopStream(this.workspaceId);
        if (!stopResult.success) {
            return (0, result_1.Err)(stopResult.error);
        }
        return (0, result_1.Ok)(undefined);
    }
    async streamWithHistory(modelString, options) {
        try {
            (0, assert_1.default)(typeof modelString === "string" && modelString.trim().length > 0, "modelString must be a non-empty string");
            await (0, tokenizer_1.loadTokenizerForModel)(modelString);
        }
        catch (error) {
            const reason = error instanceof Error ? error.message : String(error);
            return (0, result_1.Err)((0, sendMessageError_1.createUnknownSendMessageError)(`Failed to preload tokenizer for ${modelString}: ${reason}`));
        }
        const commitResult = await this.partialService.commitToHistory(this.workspaceId);
        if (!commitResult.success) {
            return (0, result_1.Err)((0, sendMessageError_1.createUnknownSendMessageError)(commitResult.error));
        }
        const historyResult = await this.historyService.getHistory(this.workspaceId);
        if (!historyResult.success) {
            return (0, result_1.Err)((0, sendMessageError_1.createUnknownSendMessageError)(historyResult.error));
        }
        // Enforce thinking policy for the specified model (single source of truth)
        // This ensures model-specific requirements are met regardless of where the request originates
        const effectiveThinkingLevel = options?.thinkingLevel
            ? (0, policy_1.enforceThinkingPolicy)(modelString, options.thinkingLevel)
            : undefined;
        return this.aiService.streamMessage(historyResult.data, this.workspaceId, modelString, effectiveThinkingLevel, options?.toolPolicy, undefined, options?.additionalSystemInstructions, options?.maxOutputTokens, options?.providerOptions, options?.mode);
    }
    attachAiListeners() {
        const forward = (event, handler) => {
            const wrapped = (...args) => {
                const [payload] = args;
                if (typeof payload === "object" &&
                    payload !== null &&
                    "workspaceId" in payload &&
                    payload.workspaceId !== this.workspaceId) {
                    return;
                }
                handler(payload);
            };
            this.aiListeners.push({ event, handler: wrapped });
            this.aiService.on(event, wrapped);
        };
        forward("stream-start", (payload) => this.emitChatEvent(payload));
        forward("stream-delta", (payload) => this.emitChatEvent(payload));
        forward("stream-end", (payload) => this.emitChatEvent(payload));
        forward("tool-call-start", (payload) => this.emitChatEvent(payload));
        forward("tool-call-delta", (payload) => this.emitChatEvent(payload));
        forward("tool-call-end", (payload) => this.emitChatEvent(payload));
        forward("reasoning-delta", (payload) => this.emitChatEvent(payload));
        forward("reasoning-end", (payload) => this.emitChatEvent(payload));
        forward("stream-abort", (payload) => this.emitChatEvent(payload));
        const errorHandler = (...args) => {
            const [raw] = args;
            if (typeof raw !== "object" ||
                raw === null ||
                !("workspaceId" in raw) ||
                raw.workspaceId !== this.workspaceId) {
                return;
            }
            const data = raw;
            const streamError = {
                type: "stream-error",
                messageId: data.messageId,
                error: data.error,
                errorType: (data.errorType ?? "unknown"),
            };
            this.emitChatEvent(streamError);
        };
        this.aiListeners.push({ event: "error", handler: errorHandler });
        this.aiService.on("error", errorHandler);
    }
    attachInitListeners() {
        const forward = (event, handler) => {
            const wrapped = (...args) => {
                const [payload] = args;
                if (typeof payload === "object" &&
                    payload !== null &&
                    "workspaceId" in payload &&
                    payload.workspaceId !== this.workspaceId) {
                    return;
                }
                // Strip workspaceId from payload before forwarding (WorkspaceInitEvent doesn't include it)
                const { workspaceId: _, ...message } = payload;
                handler(message);
            };
            this.initListeners.push({ event, handler: wrapped });
            this.initStateManager.on(event, wrapped);
        };
        forward("init-start", (payload) => this.emitChatEvent(payload));
        forward("init-output", (payload) => this.emitChatEvent(payload));
        forward("init-end", (payload) => this.emitChatEvent(payload));
    }
    // Public method to emit chat events (used by init hooks and other workspace events)
    emitChatEvent(message) {
        this.assertNotDisposed("emitChatEvent");
        this.emitter.emit("chat-event", {
            workspaceId: this.workspaceId,
            message,
        });
    }
    assertNotDisposed(operation) {
        (0, assert_1.default)(!this.disposed, `AgentSession.${operation} called after dispose`);
    }
}
exports.AgentSession = AgentSession;
//# sourceMappingURL=agentSession.js.map