import { randomBytes } from "node:crypto";
import { resolve } from "node:path";
import { createTauSdkClient } from "../../sdk/client.js";
import { safeParseCoreEventEnvelope } from "../events/parser.js";
import { extractAssistantText } from "../utils/messages.js";
import { cleanupWorkspacePath as cleanupWorkspacePathOnDisk, prepareWorkspace, resolveWorkspacePath, runBootstrapCommands, } from "./workspace.js";
const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
const PUBLIC_SESSION_ID_LENGTH = 8;
const NANOSECONDS_PER_MILLISECOND = 1000000n;
const ACTIVE_STATES = new Set([
    "queued",
    "preparing-workspace",
    "running",
    "waiting-input",
]);
function elapsedMs(startTime) {
    return Number((process.hrtime.bigint() - startTime) / NANOSECONDS_PER_MILLISECOND);
}
export class AsyncSessionManagerError extends Error {
    code;
    constructor(code, message) {
        super(message);
        this.name = "AsyncSessionManagerError";
        this.code = code;
    }
}
class AsyncSessionManagerImpl {
    sessions = new Map();
    listeners = new Set();
    projects;
    workspaceRoot;
    maxSessions;
    systemMessage;
    now;
    createClient;
    prepareWorkspace;
    runBootstrapCommands;
    cleanupWorkspacePath;
    closePromise;
    constructor(options) {
        this.projects = options.projects;
        this.workspaceRoot = resolve(options.workspaceRoot ?? resolve(process.cwd(), ".tau/async-workspaces"));
        this.maxSessions = options.maxSessions;
        this.systemMessage = options.systemMessage?.trim() || undefined;
        this.now = options.now ?? (() => new Date());
        this.createClient = options.createClient ?? createTauSdkClient;
        this.prepareWorkspace = options.prepareWorkspace ?? prepareWorkspace;
        this.runBootstrapCommands = options.runBootstrapCommands ?? runBootstrapCommands;
        this.cleanupWorkspacePath = options.cleanupWorkspacePath ?? cleanupWorkspacePathOnDisk;
    }
    async createSession(input) {
        const project = this.projects[input.projectId];
        if (!project) {
            throw new AsyncSessionManagerError("invalid_project", `unknown async project '${input.projectId}'`);
        }
        if (this.maxSessions !== undefined && this.countActiveSessions() >= this.maxSessions) {
            throw new AsyncSessionManagerError("max_sessions", "maximum session count reached");
        }
        const id = this.createSessionId();
        const now = this.now().toISOString();
        const entry = {
            record: {
                id,
                projectId: input.projectId,
                ...(input.ownerId ? { ownerId: input.ownerId } : {}),
                state: "queued",
                createdAt: now,
                updatedAt: now,
            },
            logs: [],
            project,
            abortController: new AbortController(),
            cancelRequested: false,
        };
        this.sessions.set(id, entry);
        this.log(entry, "info", "session queued");
        this.emit({
            type: "session-created",
            session: this.toRecord(entry),
        });
        let initializePromise;
        initializePromise = this.initializeSession(entry, input.prompt, input.additionalSystemMessage).finally(() => {
            if (entry.initializePromise === initializePromise) {
                entry.initializePromise = undefined;
            }
        });
        entry.initializePromise = initializePromise;
        return this.toRecord(entry);
    }
    listSessions() {
        return Array.from(this.sessions.values()).map((entry) => this.toRecord(entry));
    }
    getSession(sessionId) {
        const entry = this.getEntryBySessionId(sessionId);
        return entry ? this.toRecord(entry) : undefined;
    }
    getLogs(sessionId) {
        const entry = this.getEntryBySessionId(sessionId);
        if (!entry) {
            return undefined;
        }
        return entry.logs.map((logEntry) => ({ ...logEntry }));
    }
    onEvent(listener) {
        this.listeners.add(listener);
        return () => {
            this.listeners.delete(listener);
        };
    }
    async sendMessage(sessionId, text, options) {
        const entry = this.requireSession(sessionId);
        const trimmed = text.trim();
        if (!trimmed) {
            throw new AsyncSessionManagerError("invalid_state", "message text cannot be empty");
        }
        if (entry.record.state === "failed") {
            throw new AsyncSessionManagerError("invalid_state", `cannot submit messages when session is ${entry.record.state}`);
        }
        if (!entry.client) {
            throw new AsyncSessionManagerError("not_ready", "session is still preparing");
        }
        if (entry.record.state === "running" || entry.activeSubmit) {
            throw new AsyncSessionManagerError("busy", "session is running");
        }
        void this.submitText(entry, trimmed, "user-message", options?.additionalSystemMessage);
        return this.toRecord(entry);
    }
    async interruptSession(sessionId) {
        const entry = this.requireSession(sessionId);
        if (entry.record.state !== "running" || !entry.activeSubmit) {
            return {
                session: this.toRecord(entry),
                interrupted: false,
                isTurnRunning: false,
            };
        }
        if (!entry.client) {
            throw new AsyncSessionManagerError("not_ready", "session is still preparing");
        }
        const result = await entry.client.interrupt();
        this.log(entry, "info", "interrupt requested", {
            interrupted: result.interrupted,
            isTurnRunning: result.isTurnRunning,
        });
        return {
            session: this.toRecord(entry),
            interrupted: result.interrupted,
            isTurnRunning: result.isTurnRunning,
        };
    }
    async closeSession(sessionId) {
        const entry = this.requireSession(sessionId);
        return await this.closeEntry(entry, "close requested");
    }
    async closeInactiveSessions() {
        const entries = Array.from(this.sessions.values()).filter((entry) => this.isCloseableWithCloseAll(entry.record.state));
        const closed = [];
        for (const entry of entries) {
            closed.push(await this.closeEntry(entry, "close requested"));
        }
        return closed;
    }
    async close() {
        if (this.closePromise) {
            await this.closePromise;
            return;
        }
        this.closePromise = this.closeAllSessions();
        await this.closePromise;
    }
    async initializeSession(entry, prompt, additionalSystemMessage) {
        try {
            const sessionPreparationStart = process.hrtime.bigint();
            this.setState(entry, "preparing-workspace");
            this.log(entry, "info", "preparing workspace", {
                workspaceRoot: this.workspaceRoot,
            });
            const workspacePreparationStart = process.hrtime.bigint();
            const workspace = await this.prepareWorkspace({
                sessionId: entry.record.id,
                projectId: entry.record.projectId,
                project: entry.project,
                workspaceRoot: entry.project.workspaceRoot ?? this.workspaceRoot,
                signal: entry.abortController.signal,
                onLog: (workspaceLog) => {
                    this.log(entry, workspaceLog.level === "error" ? "error" : "info", workspaceLog.message, workspaceLog.data);
                },
            });
            const workspacePreparationDurationMs = elapsedMs(workspacePreparationStart);
            if (entry.cancelRequested) {
                return;
            }
            entry.record.workspacePath = workspace.workspacePath;
            this.touch(entry);
            this.log(entry, "info", "workspace ready", {
                workspacePath: workspace.workspacePath,
                sessionCwd: workspace.sessionCwd,
                durationMs: workspacePreparationDurationMs,
            });
            const clientConnectStart = process.hrtime.bigint();
            const client = await this.createClient(this.buildClientOptions(entry, workspace.sessionCwd));
            const clientConnectDurationMs = elapsedMs(clientConnectStart);
            if (entry.cancelRequested) {
                entry.client = client;
                await this.stopClient(entry);
                return;
            }
            entry.client = client;
            entry.record.rpcSessionId = client.ready.sessionId;
            this.touch(entry);
            this.log(entry, "info", "rpc client connected", {
                rpcSessionId: client.ready.sessionId,
                durationMs: clientConnectDurationMs,
            });
            entry.unsubscribeClientEvents = client.onEvent((event) => {
                this.handleClientEvent(entry, event);
            });
            this.log(entry, "info", "session preparation complete", {
                durationMs: elapsedMs(sessionPreparationStart),
            });
            this.startBackgroundBootstrap(entry, workspace.sessionCwd);
            if (prompt?.trim()) {
                await this.submitText(entry, prompt.trim(), "initial-prompt", additionalSystemMessage);
            }
            if (!entry.cancelRequested && entry.record.state !== "failed") {
                this.setState(entry, "waiting-input");
            }
        }
        catch (error) {
            if (entry.cancelRequested) {
                return;
            }
            if (entry.record.state !== "failed") {
                const message = error instanceof Error ? error.message : String(error);
                entry.record.error = message;
                this.setState(entry, "failed");
                this.log(entry, "error", "session failed", { cause: message });
            }
            await this.stopClient(entry);
        }
    }
    startBackgroundBootstrap(entry, sessionCwd) {
        const commands = entry.project.backgroundBootstrapCommands;
        if (!commands || commands.length === 0 || entry.backgroundBootstrapPromise) {
            return;
        }
        let backgroundBootstrapPromise;
        backgroundBootstrapPromise = this.runBootstrapCommands({
            commands,
            cwd: sessionCwd,
            signal: entry.abortController.signal,
            mode: "background",
            onLog: (workspaceLog) => {
                this.log(entry, workspaceLog.level === "error" ? "error" : "info", workspaceLog.message, workspaceLog.data);
            },
        })
            .catch((error) => {
            if (entry.cancelRequested) {
                this.log(entry, "info", "background bootstrap cancelled");
                return;
            }
            this.log(entry, "warn", "background bootstrap failed", {
                cause: error instanceof Error ? error.message : String(error),
            });
        })
            .finally(() => {
            if (entry.backgroundBootstrapPromise === backgroundBootstrapPromise) {
                entry.backgroundBootstrapPromise = undefined;
            }
        });
        entry.backgroundBootstrapPromise = backgroundBootstrapPromise;
    }
    submitText(entry, text, source, additionalSystemMessage) {
        if (!entry.client) {
            throw new AsyncSessionManagerError("not_ready", "session is still preparing");
        }
        if (entry.activeSubmit) {
            throw new AsyncSessionManagerError("busy", "session is running");
        }
        this.setState(entry, "running");
        this.log(entry, "info", "submitting message", { source, text });
        const client = entry.client;
        const payload = this.buildSubmitPayload(text, additionalSystemMessage);
        const submitPromise = (async () => {
            try {
                const result = await client.submit(payload);
                this.log(entry, result.turn.blocked ? "error" : "info", "message finished", {
                    source,
                    aborted: result.turn.aborted,
                    blocked: result.turn.blocked,
                    userHistoryEntryId: result.userHistoryEntryId,
                });
                if (!entry.cancelRequested) {
                    if (result.turn.blocked) {
                        entry.record.error = result.turn.blocked.message;
                        this.setState(entry, "failed");
                    }
                    else {
                        this.setState(entry, "waiting-input");
                    }
                }
            }
            catch (error) {
                if (!entry.cancelRequested) {
                    const message = error instanceof Error ? error.message : String(error);
                    entry.record.error = message;
                    this.setState(entry, "failed");
                    this.log(entry, "error", "submit failed", { source, cause: message });
                    await this.stopClient(entry);
                }
            }
        })();
        entry.activeSubmit = submitPromise;
        void submitPromise.finally(() => {
            if (entry.activeSubmit === submitPromise) {
                entry.activeSubmit = undefined;
            }
        });
        return submitPromise;
    }
    buildClientOptions(entry, cwd) {
        const options = { cwd };
        if (entry.project.persona) {
            options.persona = entry.project.persona;
        }
        if (entry.project.riskLevel) {
            options.riskLevel = entry.project.riskLevel;
        }
        if (entry.project.noAgentContextFiles !== undefined) {
            options.noAgentContextFiles = entry.project.noAgentContextFiles;
        }
        return options;
    }
    async closeAllSessions() {
        const entries = Array.from(this.sessions.values());
        for (const entry of entries) {
            if (this.isActiveState(entry.record.state)) {
                this.requestCancellation(entry, "manager shutdown requested");
            }
        }
        await Promise.allSettled(entries.map(async (entry) => {
            await this.stopClient(entry);
            await this.runWorkspaceCleanup(entry);
        }));
    }
    async closeEntry(entry, message) {
        if (this.isActiveState(entry.record.state)) {
            this.requestCancellation(entry, message);
        }
        else {
            this.log(entry, "info", message);
        }
        await this.stopClient(entry);
        await this.runWorkspaceCleanup(entry);
        const record = this.toRecord(entry);
        this.deleteEntry(entry.record.id);
        return record;
    }
    async runWorkspaceCleanup(entry) {
        if (entry.workspaceCleanupPromise) {
            await entry.workspaceCleanupPromise;
            return;
        }
        let cleanupPromise;
        cleanupPromise = (async () => {
            const pendingWork = [
                entry.activeSubmit,
                entry.initializePromise,
                entry.backgroundBootstrapPromise,
            ].filter((promise) => promise !== undefined);
            if (pendingWork.length > 0) {
                await Promise.allSettled(pendingWork);
            }
            const workspacePath = this.resolveWorkspacePathForCleanup(entry);
            try {
                await this.cleanupWorkspacePath(workspacePath);
                this.log(entry, "info", "workspace cleanup complete", { workspacePath });
            }
            catch (error) {
                this.log(entry, "warn", "workspace cleanup failed", {
                    workspacePath,
                    cause: error instanceof Error ? error.message : String(error),
                });
            }
        })().finally(() => {
            if (entry.workspaceCleanupPromise === cleanupPromise) {
                entry.workspaceCleanupPromise = undefined;
            }
        });
        entry.workspaceCleanupPromise = cleanupPromise;
        await cleanupPromise;
    }
    resolveWorkspacePathForCleanup(entry) {
        if (entry.record.workspacePath) {
            return entry.record.workspacePath;
        }
        return resolveWorkspacePath({
            workspaceRoot: entry.project.workspaceRoot ?? this.workspaceRoot,
            projectId: entry.record.projectId,
            sessionId: entry.record.id,
        });
    }
    requestCancellation(entry, message) {
        entry.cancelRequested = true;
        entry.abortController.abort();
        this.log(entry, "info", message);
    }
    async stopClient(entry) {
        if (entry.unsubscribeClientEvents) {
            entry.unsubscribeClientEvents();
            entry.unsubscribeClientEvents = undefined;
        }
        if (!entry.client) {
            return;
        }
        if (entry.clientClosePromise) {
            await entry.clientClosePromise;
            return;
        }
        const client = entry.client;
        let closePromise;
        closePromise = (async () => {
            try {
                await client.interrupt();
            }
            catch (error) {
                this.log(entry, "warn", "interrupt failed", {
                    cause: error instanceof Error ? error.message : String(error),
                });
            }
            try {
                await client.shutdown();
            }
            catch (error) {
                this.log(entry, "warn", "shutdown failed", {
                    cause: error instanceof Error ? error.message : String(error),
                });
            }
            try {
                await client.close();
            }
            catch (error) {
                this.log(entry, "warn", "close failed", {
                    cause: error instanceof Error ? error.message : String(error),
                });
            }
        })().finally(() => {
            if (entry.clientClosePromise === closePromise) {
                entry.clientClosePromise = undefined;
            }
            if (entry.client === client) {
                entry.client = undefined;
            }
        });
        entry.clientClosePromise = closePromise;
        await closePromise;
    }
    isActiveState(state) {
        return ACTIVE_STATES.has(state);
    }
    isCloseableWithCloseAll(state) {
        return CLOSEABLE_STATES_WITH_CLOSE_ALL.has(state);
    }
    deleteEntry(sessionId) {
        this.sessions.delete(sessionId);
    }
    getEntryBySessionId(sessionId) {
        return this.sessions.get(sessionId);
    }
    requireSession(sessionId) {
        const entry = this.getEntryBySessionId(sessionId);
        if (!entry) {
            throw new AsyncSessionManagerError("not_found", `session '${sessionId}' not found`);
        }
        return entry;
    }
    createSessionId() {
        for (let attempt = 0; attempt < 8; attempt += 1) {
            const random = randomBytes(PUBLIC_SESSION_ID_LENGTH);
            let id = "";
            for (const byte of random) {
                id += BASE58_ALPHABET[byte % BASE58_ALPHABET.length];
            }
            if (!this.sessions.has(id)) {
                return id;
            }
        }
        throw new Error("failed to allocate session id");
    }
    buildSubmitPayload(text, additionalSystemMessage) {
        const messages = [this.systemMessage, additionalSystemMessage?.trim() || undefined].filter((message) => Boolean(message));
        if (messages.length === 0) {
            return text;
        }
        return [`<system>`, messages.join("\n"), `</system>`, text].join("\n");
    }
    countActiveSessions() {
        let count = 0;
        for (const entry of this.sessions.values()) {
            if (this.isActiveState(entry.record.state)) {
                count += 1;
            }
        }
        return count;
    }
    setState(entry, state) {
        const previousState = entry.record.state;
        if (previousState === state) {
            return;
        }
        entry.record.state = state;
        this.touch(entry);
        this.emit({
            type: "session-state-changed",
            sessionId: entry.record.id,
            projectId: entry.record.projectId,
            previousState,
            state,
            updatedAt: entry.record.updatedAt,
        });
    }
    touch(entry) {
        entry.record.updatedAt = this.now().toISOString();
    }
    log(entry, level, message, data) {
        const logEntry = {
            timestamp: this.now().toISOString(),
            level,
            message,
            ...(data === undefined ? {} : { data }),
        };
        entry.logs.push(logEntry);
        this.emit({
            type: "session-log",
            sessionId: entry.record.id,
            projectId: entry.record.projectId,
            state: entry.record.state,
            log: { ...logEntry },
        });
    }
    emit(event) {
        for (const listener of this.listeners) {
            try {
                listener(event);
            }
            catch {
                // ignore listener errors to keep session processing stable
            }
        }
    }
    handleClientEvent(entry, sdkEvent) {
        if (!entry.activeSubmit) {
            return;
        }
        const parsedEnvelope = safeParseCoreEventEnvelope(sdkEvent.event);
        if (!parsedEnvelope.ok) {
            return;
        }
        const { event: coreEvent } = parsedEnvelope.value;
        switch (coreEvent.type) {
            case "tool_ui": {
                switch (coreEvent.uiEvent.type) {
                    case "bash_started":
                        this.emitProgress(entry, {
                            type: "bash-command",
                            command: coreEvent.uiEvent.command,
                        });
                        return;
                    case "edit_success":
                        this.emitProgress(entry, {
                            type: "edited-file",
                            path: coreEvent.uiEvent.path,
                        });
                        return;
                    case "write_success":
                        this.emitProgress(entry, {
                            type: "wrote-file",
                            path: coreEvent.uiEvent.path,
                        });
                        return;
                    default:
                        return;
                }
            }
            case "assistant_final": {
                const text = extractAssistantText(coreEvent.message);
                if (text) {
                    this.emitProgress(entry, {
                        type: "assistant-message",
                        text,
                    });
                }
                return;
            }
            default:
                return;
        }
    }
    emitProgress(entry, progress) {
        this.emit({
            type: "session-progress",
            sessionId: entry.record.id,
            projectId: entry.record.projectId,
            state: entry.record.state,
            timestamp: this.now().toISOString(),
            progress,
        });
    }
    toRecord(entry) {
        return { ...entry.record };
    }
}
const CLOSEABLE_STATES_WITH_CLOSE_ALL = new Set([
    "waiting-input",
    "failed",
]);
class ScopedAsyncSessionManager {
    sessionManager;
    ownerId;
    allowedProjectIds;
    constructor(options) {
        this.sessionManager = options.sessionManager;
        this.ownerId = options.ownerId;
        this.allowedProjectIds = options.allowedProjectIds;
    }
    async createSession(input) {
        if (!this.allowedProjectIds.has(input.projectId)) {
            throw new AsyncSessionManagerError("invalid_project", `unknown async project '${input.projectId}'`);
        }
        return await this.sessionManager.createSession({
            projectId: input.projectId,
            ownerId: this.ownerId,
            ...(input.prompt === undefined ? {} : { prompt: input.prompt }),
            ...(input.additionalSystemMessage === undefined
                ? {}
                : { additionalSystemMessage: input.additionalSystemMessage }),
        });
    }
    listSessions() {
        return this.sessionManager.listSessions().filter((session) => this.isVisibleSession(session));
    }
    getSession(sessionId) {
        const session = this.sessionManager.getSession(sessionId);
        if (!session || !this.isVisibleSession(session)) {
            return undefined;
        }
        return session;
    }
    getLogs(sessionId) {
        const session = this.sessionManager.getSession(sessionId);
        if (!session || !this.isVisibleSession(session)) {
            return undefined;
        }
        return this.sessionManager.getLogs(sessionId);
    }
    async sendMessage(sessionId, text, options) {
        this.requireSession(sessionId);
        return await this.sessionManager.sendMessage(sessionId, text, options);
    }
    async interruptSession(sessionId) {
        this.requireSession(sessionId);
        return await this.sessionManager.interruptSession(sessionId);
    }
    async closeSession(sessionId) {
        this.requireSession(sessionId);
        return await this.sessionManager.closeSession(sessionId);
    }
    async closeInactiveSessions() {
        const closeableSessions = this.listSessions().filter((session) => CLOSEABLE_STATES_WITH_CLOSE_ALL.has(session.state));
        const closed = [];
        for (const session of closeableSessions) {
            closed.push(await this.sessionManager.closeSession(session.id));
        }
        return closed;
    }
    async close() {
        return;
    }
    onEvent(listener) {
        return this.sessionManager.onEvent((event) => {
            if (event.type === "session-created") {
                if (this.isVisibleSession(event.session)) {
                    listener(event);
                }
                return;
            }
            const session = this.sessionManager.getSession(event.sessionId);
            if (session && this.isVisibleSession(session)) {
                listener(event);
            }
        });
    }
    requireSession(sessionId) {
        const session = this.sessionManager.getSession(sessionId);
        if (!session || !this.isVisibleSession(session)) {
            throw new AsyncSessionManagerError("not_found", `session '${sessionId}' not found`);
        }
        return session;
    }
    isVisibleSession(session) {
        return this.allowedProjectIds.has(session.projectId) && session.ownerId === this.ownerId;
    }
}
export function createAsyncSessionManager(options) {
    return new AsyncSessionManagerImpl(options);
}
export function createScopedAsyncSessionManager(options) {
    return new ScopedAsyncSessionManager({
        sessionManager: options.sessionManager,
        ownerId: options.ownerId,
        allowedProjectIds: new Set(options.allowedProjectIds),
    });
}
//# sourceMappingURL=session_manager.js.map