import { randomUUID } from "node:crypto";
import { resolve } from "node:path";
import { stat } from "node:fs/promises";
import { AGENT_LIFECYCLE_STATUSES, } from "../../shared/agent-lifecycle.js";
import { z } from "zod";
import { AGENT_PROVIDER_IDS, getAgentProviderDefinition } from "./provider-manifest.js";
export { AGENT_LIFECYCLE_STATUSES };
const SYSTEM_ERROR_PREFIX = "[System Error]";
function attachPersistenceCwd(handle, cwd) {
    if (!handle) {
        return null;
    }
    return {
        ...handle,
        metadata: {
            ...(handle.metadata ?? {}),
            cwd,
        },
    };
}
const DEFAULT_TIMELINE_FETCH_LIMIT = 200;
const BUSY_STATUSES = ["initializing", "running"];
const AgentIdSchema = z.string().uuid();
function isAgentBusy(status) {
    return BUSY_STATUSES.includes(status);
}
function isTurnTerminalEvent(event) {
    return (event.type === "turn_completed" ||
        event.type === "turn_failed" ||
        event.type === "turn_canceled");
}
function createAbortError(signal, fallbackMessage) {
    const reason = signal?.reason;
    const message = typeof reason === "string"
        ? reason
        : reason instanceof Error
            ? reason.message
            : fallbackMessage;
    return Object.assign(new Error(message), { name: "AbortError" });
}
function validateAgentId(agentId, source) {
    const result = AgentIdSchema.safeParse(agentId);
    if (!result.success) {
        throw new Error(`${source}: agentId must be a UUID`);
    }
    return result.data;
}
function normalizeMessageId(messageId) {
    if (typeof messageId !== "string") {
        return undefined;
    }
    const trimmed = messageId.trim();
    return trimmed.length > 0 ? trimmed : undefined;
}
export class AgentManager {
    constructor(options) {
        this.clients = new Map();
        this.agents = new Map();
        this.pendingForegroundRuns = new Map();
        this.subscribers = new Set();
        this.previousStatuses = new Map();
        this.backgroundTasks = new Set();
        const maxTimelineItems = options?.maxTimelineItems;
        this.maxTimelineItems =
            typeof maxTimelineItems === "number" &&
                Number.isFinite(maxTimelineItems) &&
                maxTimelineItems >= 0
                ? Math.floor(maxTimelineItems)
                : null;
        this.idFactory = options?.idFactory ?? (() => randomUUID());
        this.registry = options?.registry;
        this.onAgentAttention = options?.onAgentAttention;
        this.mcpBaseUrl = options?.mcpBaseUrl ?? null;
        this.logger = options.logger.child({ module: "agent", component: "agent-manager" });
        if (options?.clients) {
            for (const [provider, client] of Object.entries(options.clients)) {
                if (client) {
                    this.registerClient(provider, client);
                }
            }
        }
    }
    registerClient(provider, client) {
        this.clients.set(provider, client);
    }
    setAgentAttentionCallback(callback) {
        this.onAgentAttention = callback;
    }
    setMcpBaseUrl(url) {
        this.mcpBaseUrl = url;
    }
    getMetricsSnapshot() {
        const byLifecycle = {};
        let withActiveForegroundTurn = 0;
        let totalItems = 0;
        let maxItemsPerAgent = 0;
        for (const agent of this.agents.values()) {
            byLifecycle[agent.lifecycle] = (byLifecycle[agent.lifecycle] ?? 0) + 1;
            if (agent.activeForegroundTurnId !== null) {
                withActiveForegroundTurn++;
            }
            const len = agent.timeline.length;
            totalItems += len;
            if (len > maxItemsPerAgent) {
                maxItemsPerAgent = len;
            }
        }
        return {
            total: this.agents.size,
            byLifecycle,
            withActiveForegroundTurn,
            timelineStats: {
                totalItems,
                maxItemsPerAgent,
            },
        };
    }
    touchUpdatedAt(agent) {
        const nowMs = Date.now();
        const previousMs = agent.updatedAt.getTime();
        const nextMs = nowMs > previousMs ? nowMs : previousMs + 1;
        const next = new Date(nextMs);
        agent.updatedAt = next;
        return next;
    }
    hasInFlightRun(agentId) {
        const agent = this.agents.get(agentId);
        if (!agent) {
            return false;
        }
        return (agent.lifecycle === "running" ||
            Boolean(agent.activeForegroundTurnId) ||
            this.hasPendingForegroundRun(agentId));
    }
    subscribe(callback, options) {
        const targetAgentId = options?.agentId == null ? null : validateAgentId(options.agentId, "subscribe");
        const record = {
            callback,
            agentId: targetAgentId,
        };
        this.subscribers.add(record);
        if (options?.replayState !== false) {
            if (record.agentId) {
                const agent = this.agents.get(record.agentId);
                if (agent) {
                    callback({
                        type: "agent_state",
                        agent: { ...agent },
                    });
                }
            }
            else {
                // For global subscribers, skip internal agents during replay
                for (const agent of this.agents.values()) {
                    if (agent.internal) {
                        continue;
                    }
                    callback({
                        type: "agent_state",
                        agent: { ...agent },
                    });
                }
            }
        }
        return () => {
            this.subscribers.delete(record);
        };
    }
    listAgents() {
        return Array.from(this.agents.values())
            .filter((agent) => !agent.internal)
            .map((agent) => ({
            ...agent,
        }));
    }
    async listPersistedAgents(options) {
        if (options?.provider) {
            const client = this.requireClient(options.provider);
            if (!client.listPersistedAgents) {
                return [];
            }
            return client.listPersistedAgents({ limit: options.limit });
        }
        const descriptors = [];
        for (const [provider, client] of this.clients.entries()) {
            if (!client.listPersistedAgents) {
                continue;
            }
            try {
                const entries = await client.listPersistedAgents({
                    limit: options?.limit,
                });
                descriptors.push(...entries);
            }
            catch (error) {
                this.logger.warn({ err: error, provider }, "Failed to list persisted agents for provider");
            }
        }
        const limit = options?.limit ?? 20;
        return descriptors
            .sort((a, b) => b.lastActivityAt.getTime() - a.lastActivityAt.getTime())
            .slice(0, limit);
    }
    async listProviderAvailability() {
        const checks = AGENT_PROVIDER_IDS.map(async (providerId) => {
            const provider = providerId;
            const client = this.clients.get(provider);
            if (!client) {
                return {
                    provider,
                    available: false,
                    error: `No client registered for provider '${provider}'`,
                };
            }
            try {
                const available = await client.isAvailable();
                return {
                    provider,
                    available,
                    error: null,
                };
            }
            catch (error) {
                const message = error instanceof Error ? error.message : String(error);
                this.logger.warn({ err: error, provider }, "Failed to check provider availability");
                return {
                    provider,
                    available: false,
                    error: message,
                };
            }
        });
        return Promise.all(checks);
    }
    async listDraftCommands(config) {
        const normalizedConfig = await this.normalizeConfig(config);
        const client = this.requireClient(normalizedConfig.provider);
        const available = await client.isAvailable();
        if (!available) {
            throw new Error(`Provider '${normalizedConfig.provider}' is not available. Please ensure the CLI is installed.`);
        }
        const session = await client.createSession(normalizedConfig);
        try {
            if (!session.listCommands) {
                throw new Error(`Provider '${normalizedConfig.provider}' does not support listing commands`);
            }
            return await session.listCommands();
        }
        finally {
            try {
                await session.close();
            }
            catch (error) {
                this.logger.warn({ err: error, provider: normalizedConfig.provider }, "Failed to close draft command listing session");
            }
        }
    }
    async listDraftFeatures(config) {
        const normalizedConfig = await this.normalizeConfig(config);
        const client = this.requireClient(normalizedConfig.provider);
        const available = await client.isAvailable();
        if (!available) {
            throw new Error(`Provider '${normalizedConfig.provider}' is not available. Please ensure the CLI is installed.`);
        }
        const session = await client.createSession(normalizedConfig);
        try {
            return session.features ?? [];
        }
        finally {
            try {
                await session.close();
            }
            catch (error) {
                this.logger.warn({ err: error, provider: normalizedConfig.provider }, "Failed to close draft feature listing session");
            }
        }
    }
    getAgent(id) {
        const agent = this.agents.get(id);
        return agent ? { ...agent } : null;
    }
    getTimeline(id) {
        const agent = this.requireAgent(id);
        return [...agent.timeline];
    }
    getTimelineRows(id) {
        const agent = this.requireAgent(id);
        const { rows } = this.ensureTimelineState(agent);
        return rows.map((row) => ({ ...row }));
    }
    fetchTimeline(id, options) {
        const agent = this.requireAgent(id);
        const { rows, epoch, nextSeq, minSeq, maxSeq } = this.ensureTimelineState(agent);
        const direction = options?.direction ?? "tail";
        const requestedLimit = options?.limit;
        const limit = requestedLimit === undefined
            ? DEFAULT_TIMELINE_FETCH_LIMIT
            : Math.max(0, Math.floor(requestedLimit));
        const cursor = options?.cursor;
        const window = { minSeq, maxSeq, nextSeq };
        if (cursor && cursor.epoch !== epoch) {
            return {
                epoch,
                direction,
                reset: true,
                staleCursor: true,
                gap: false,
                window,
                hasOlder: false,
                hasNewer: false,
                rows: rows.map((row) => ({ ...row })),
            };
        }
        const selectAll = limit === 0;
        const cloneRows = (items) => items.map((row) => ({ ...row }));
        if (direction === "after" && cursor && rows.length > 0 && cursor.seq < minSeq - 1) {
            return {
                epoch,
                direction,
                reset: true,
                staleCursor: false,
                gap: true,
                window,
                hasOlder: false,
                hasNewer: false,
                rows: cloneRows(rows),
            };
        }
        if (rows.length === 0) {
            return {
                epoch,
                direction,
                reset: false,
                staleCursor: false,
                gap: false,
                window,
                hasOlder: false,
                hasNewer: false,
                rows: [],
            };
        }
        if (direction === "tail") {
            const selected = selectAll || limit >= rows.length ? rows : rows.slice(rows.length - limit);
            const hasOlder = selected.length > 0 && selected[0].seq > minSeq;
            return {
                epoch,
                direction,
                reset: false,
                staleCursor: false,
                gap: false,
                window,
                hasOlder,
                hasNewer: false,
                rows: cloneRows(selected),
            };
        }
        if (direction === "after") {
            const baseSeq = cursor?.seq ?? 0;
            const startIdx = rows.findIndex((row) => row.seq > baseSeq);
            if (startIdx < 0) {
                return {
                    epoch,
                    direction,
                    reset: false,
                    staleCursor: false,
                    gap: false,
                    window,
                    hasOlder: baseSeq >= minSeq,
                    hasNewer: false,
                    rows: [],
                };
            }
            const selected = selectAll ? rows.slice(startIdx) : rows.slice(startIdx, startIdx + limit);
            const lastSelected = selected[selected.length - 1];
            return {
                epoch,
                direction,
                reset: false,
                staleCursor: false,
                gap: false,
                window,
                hasOlder: selected[0].seq > minSeq,
                hasNewer: Boolean(lastSelected && lastSelected.seq < maxSeq),
                rows: cloneRows(selected),
            };
        }
        // direction === "before"
        const beforeSeq = cursor?.seq ?? nextSeq;
        const endExclusive = rows.findIndex((row) => row.seq >= beforeSeq);
        const boundedRows = endExclusive < 0 ? rows : rows.slice(0, endExclusive);
        const selected = selectAll || limit >= boundedRows.length
            ? boundedRows
            : boundedRows.slice(boundedRows.length - limit);
        const hasOlder = selected.length > 0 && selected[0].seq > minSeq;
        const hasNewer = endExclusive >= 0;
        return {
            epoch,
            direction,
            reset: false,
            staleCursor: false,
            gap: false,
            window,
            hasOlder,
            hasNewer,
            rows: cloneRows(selected),
        };
    }
    async createAgent(config, agentId, options) {
        const resolvedAgentId = validateAgentId(agentId ?? this.idFactory(), "createAgent");
        const injectedConfig = this.mcpBaseUrl == null
            ? config
            : {
                ...config,
                mcpServers: {
                    seawork: {
                        type: "http",
                        url: `${this.mcpBaseUrl}?callerAgentId=${resolvedAgentId}`,
                    },
                    ...(config.mcpServers ?? {}),
                },
            };
        const normalizedConfig = await this.normalizeConfig(injectedConfig);
        const launchContext = this.buildLaunchContext(resolvedAgentId);
        const client = this.requireClient(normalizedConfig.provider);
        const available = await client.isAvailable();
        if (!available) {
            throw new Error(`Provider '${normalizedConfig.provider}' is not available. Please ensure the CLI is installed.`);
        }
        const session = await client.createSession(normalizedConfig, launchContext);
        return this.registerSession(session, normalizedConfig, resolvedAgentId, {
            labels: options?.labels,
        });
    }
    // Reconstruct an agent from provider persistence. Callers should explicitly
    // hydrate timeline history after resume.
    async resumeAgentFromPersistence(handle, overrides, agentId, options) {
        const resolvedAgentId = validateAgentId(agentId ?? this.idFactory(), "resumeAgentFromPersistence");
        const metadata = (handle.metadata ?? {});
        const mergedConfig = {
            ...metadata,
            ...overrides,
            provider: handle.provider,
        };
        const normalizedConfig = await this.normalizeConfig(mergedConfig);
        const resumeOverrides = normalizedConfig.model !== mergedConfig.model
            ? { ...overrides, model: normalizedConfig.model }
            : overrides;
        const launchContext = this.buildLaunchContext(resolvedAgentId);
        const client = this.requireClient(handle.provider);
        const session = await client.resumeSession(handle, resumeOverrides, launchContext);
        return this.registerSession(session, normalizedConfig, resolvedAgentId, options);
    }
    // Hot-reload an active agent session with config overrides while preserving
    // in-memory timeline state.
    async reloadAgentSession(agentId, overrides) {
        let existing = this.requireAgent(agentId);
        if (this.hasInFlightRun(agentId)) {
            await this.cancelAgentRun(agentId);
            existing = this.requireAgent(agentId);
        }
        const timelineState = this.ensureTimelineState(existing);
        const preservedTimeline = [...existing.timeline];
        const preservedTimelineRows = timelineState.rows.map((row) => ({ ...row }));
        const preservedTimelineEpoch = timelineState.epoch;
        const preservedTimelineNextSeq = timelineState.nextSeq;
        const preservedHistoryPrimed = existing.historyPrimed;
        const preservedLastUsage = existing.lastUsage;
        const preservedLastError = existing.lastError;
        const preservedAttention = existing.attention;
        const handle = existing.persistence;
        const provider = handle?.provider ?? existing.provider;
        const client = this.requireClient(provider);
        const refreshConfig = {
            ...existing.config,
            ...overrides,
            provider,
        };
        const normalizedConfig = await this.normalizeConfig(refreshConfig);
        const launchContext = this.buildLaunchContext(agentId);
        const session = handle
            ? await client.resumeSession(handle, normalizedConfig, launchContext)
            : await client.createSession(normalizedConfig, launchContext);
        // Remove the existing agent entry before swapping sessions
        this.agents.delete(agentId);
        if (existing.unsubscribeSession) {
            existing.unsubscribeSession();
            existing.unsubscribeSession = null;
        }
        for (const waiter of existing.foregroundTurnWaiters) {
            this.settleForegroundTurnWaiter(waiter);
        }
        existing.foregroundTurnWaiters.clear();
        this.settlePendingForegroundRun(agentId);
        try {
            await existing.session.close();
        }
        catch (error) {
            this.logger.warn({ err: error, agentId }, "Failed to close previous session during refresh");
        }
        // Preserve existing labels and timeline during reload.
        return this.registerSession(session, normalizedConfig, agentId, {
            labels: existing.labels,
            createdAt: existing.createdAt,
            updatedAt: existing.updatedAt,
            lastUserMessageAt: existing.lastUserMessageAt,
            timeline: preservedTimeline,
            timelineRows: preservedTimelineRows,
            timelineEpoch: preservedTimelineEpoch,
            timelineNextSeq: preservedTimelineNextSeq,
            historyPrimed: preservedHistoryPrimed,
            lastUsage: preservedLastUsage,
            lastError: preservedLastError,
            attention: preservedAttention,
        });
    }
    async closeAgent(agentId) {
        const agent = this.requireAgent(agentId);
        this.logger.trace({
            agentId,
            lifecycle: agent.lifecycle,
            activeForegroundTurnId: agent.activeForegroundTurnId,
            pendingPermissions: agent.pendingPermissions.size,
        }, "closeAgent: start");
        this.agents.delete(agentId);
        // Clean up previousStatus to prevent memory leak
        this.previousStatuses.delete(agentId);
        if (agent.unsubscribeSession) {
            agent.unsubscribeSession();
            agent.unsubscribeSession = null;
        }
        for (const waiter of agent.foregroundTurnWaiters) {
            // Wake up the generator so it can exit the await loop
            waiter.callback({
                type: "turn_canceled",
                provider: agent.provider,
                reason: "agent closed",
                turnId: waiter.turnId,
            });
            this.settleForegroundTurnWaiter(waiter);
        }
        agent.foregroundTurnWaiters.clear();
        this.settlePendingForegroundRun(agentId);
        const session = agent.session;
        const closedAgent = {
            ...agent,
            lifecycle: "closed",
            session: null,
            activeForegroundTurnId: null,
        };
        await session.close();
        this.emitState(closedAgent);
        this.logger.trace({ agentId }, "closeAgent: completed");
    }
    async archiveAgent(agentId) {
        const agent = this.requireAgent(agentId);
        if (!this.registry) {
            throw new Error("Agent storage is not configured");
        }
        await this.registry.applySnapshot(agent, {
            internal: agent.internal,
        });
        const stored = await this.registry.get(agentId);
        if (!stored) {
            throw new Error(`Agent ${agentId} not found in storage after snapshot`);
        }
        const archivedAt = new Date().toISOString();
        const normalizedStatus = stored.lastStatus === "running" || stored.lastStatus === "initializing"
            ? "idle"
            : stored.lastStatus;
        await this.registry.upsert({
            ...stored,
            archivedAt,
            updatedAt: archivedAt,
            lastStatus: normalizedStatus,
            requiresAttention: false,
            attentionReason: null,
            attentionTimestamp: null,
        });
        this.notifyAgentState(agentId);
        await this.closeAgent(agentId);
        return { archivedAt };
    }
    async setAgentMode(agentId, modeId) {
        const agent = this.requireAgent(agentId);
        await agent.session.setMode(modeId);
        agent.currentModeId = modeId;
        // Update runtimeInfo to reflect the new mode
        if (agent.runtimeInfo) {
            agent.runtimeInfo = { ...agent.runtimeInfo, modeId };
        }
        this.touchUpdatedAt(agent);
        this.emitState(agent);
    }
    async setAgentModel(agentId, modelId) {
        const agent = this.requireAgent(agentId);
        const normalizedModelId = typeof modelId === "string" && modelId.trim().length > 0 ? modelId : null;
        if (agent.session.setModel) {
            await agent.session.setModel(normalizedModelId);
        }
        agent.config.model = normalizedModelId ?? undefined;
        if (agent.runtimeInfo) {
            agent.runtimeInfo = { ...agent.runtimeInfo, model: normalizedModelId };
        }
        this.touchUpdatedAt(agent);
        this.emitState(agent);
    }
    async setAgentThinkingOption(agentId, thinkingOptionId) {
        const agent = this.requireAgent(agentId);
        const normalizedThinkingOptionId = typeof thinkingOptionId === "string" && thinkingOptionId.trim().length > 0
            ? thinkingOptionId
            : null;
        if (agent.session.setThinkingOption) {
            await agent.session.setThinkingOption(normalizedThinkingOptionId);
        }
        agent.config.thinkingOptionId = normalizedThinkingOptionId ?? undefined;
        this.touchUpdatedAt(agent);
        this.emitState(agent);
    }
    async setAgentFeature(agentId, featureId, value) {
        const agent = this.requireAgent(agentId);
        if (!agent.session.setFeature) {
            throw new Error("Agent session does not support setting features");
        }
        await agent.session.setFeature(featureId, value);
        agent.config.featureValues = { ...agent.config.featureValues, [featureId]: value };
        this.touchUpdatedAt(agent);
        this.emitState(agent);
    }
    async setTitle(agentId, title) {
        const agent = this.requireAgent(agentId);
        const normalizedTitle = title.trim();
        if (!normalizedTitle) {
            return;
        }
        this.touchUpdatedAt(agent);
        await this.persistSnapshot(agent, { title: normalizedTitle });
        this.emitState(agent);
    }
    async setLabels(agentId, labels) {
        const agent = this.requireAgent(agentId);
        agent.labels = { ...agent.labels, ...labels };
        await this.persistSnapshot(agent);
        this.touchUpdatedAt(agent);
        this.emitState(agent);
    }
    notifyAgentState(agentId) {
        const agent = this.agents.get(agentId);
        if (!agent || agent.internal) {
            return;
        }
        this.touchUpdatedAt(agent);
        this.emitState(agent);
    }
    async clearAgentAttention(agentId) {
        const agent = this.requireAgent(agentId);
        if (agent.attention.requiresAttention) {
            agent.attention = { requiresAttention: false };
            await this.persistSnapshot(agent);
            this.emitState(agent);
        }
    }
    async runAgent(agentId, prompt, options) {
        const events = this.streamAgent(agentId, prompt, options);
        const timeline = [];
        let finalText = "";
        let usage;
        let canceled = false;
        for await (const event of events) {
            if (event.type === "timeline") {
                timeline.push(event.item);
            }
            else if (event.type === "turn_completed") {
                usage = event.usage;
            }
            else if (event.type === "turn_failed") {
                throw new Error(this.formatTurnFailedMessage(event));
            }
            else if (event.type === "turn_canceled") {
                canceled = true;
            }
        }
        finalText = this.getLastAssistantMessageFromTimeline(timeline) ?? "";
        const agent = this.requireAgent(agentId);
        const sessionId = agent.persistence?.sessionId;
        if (!sessionId) {
            throw new Error(`Agent ${agentId} has no persistence.sessionId after run completed`);
        }
        return {
            sessionId,
            finalText,
            usage,
            timeline,
            canceled,
        };
    }
    recordUserMessage(agentId, text, options) {
        const agent = this.requireAgent(agentId);
        const normalizedMessageId = normalizeMessageId(options?.messageId);
        const item = {
            type: "user_message",
            text,
            messageId: normalizedMessageId,
        };
        const updatedAt = this.touchUpdatedAt(agent);
        agent.lastUserMessageAt = updatedAt;
        const row = this.recordTimeline(agent, item);
        this.dispatchStream(agentId, {
            type: "timeline",
            item,
            provider: agent.provider,
        }, {
            seq: row.seq,
            epoch: this.ensureTimelineState(agent).epoch,
        });
        if (options?.emitState !== false) {
            this.emitState(agent);
        }
    }
    async appendTimelineItem(agentId, item) {
        const agent = this.requireAgent(agentId);
        this.touchUpdatedAt(agent);
        const row = this.recordTimeline(agent, item);
        this.dispatchStream(agentId, {
            type: "timeline",
            item,
            provider: agent.provider,
        }, {
            seq: row.seq,
            epoch: this.ensureTimelineState(agent).epoch,
        });
        await this.persistSnapshot(agent);
    }
    async emitLiveTimelineItem(agentId, item) {
        const agent = this.requireAgent(agentId);
        this.touchUpdatedAt(agent);
        this.dispatchStream(agentId, {
            type: "timeline",
            item,
            provider: agent.provider,
        });
    }
    streamAgent(agentId, prompt, options) {
        const existingAgent = this.requireAgent(agentId);
        this.logger.trace({
            agentId,
            lifecycle: existingAgent.lifecycle,
            activeForegroundTurnId: existingAgent.activeForegroundTurnId,
            hasPendingForegroundRun: this.hasPendingForegroundRun(agentId),
            promptType: typeof prompt === "string" ? "string" : "structured",
            hasRunOptions: Boolean(options),
        }, "streamAgent: requested");
        if (existingAgent.activeForegroundTurnId || this.hasPendingForegroundRun(agentId)) {
            this.logger.trace({
                agentId,
                lifecycle: existingAgent.lifecycle,
                hasPendingForegroundRun: this.hasPendingForegroundRun(agentId),
            }, "streamAgent: rejected because a foreground run is already in flight");
            throw new Error(`Agent ${agentId} already has an active run`);
        }
        const agent = existingAgent;
        agent.pendingReplacement = false;
        agent.lastError = undefined;
        const self = this;
        const pendingRun = self.createPendingForegroundRun();
        self.pendingForegroundRuns.set(agentId, pendingRun);
        const streamForwarder = (async function* streamForwarder() {
            let turnId;
            let waiter = null;
            try {
                const result = await agent.session.startTurn(prompt, options);
                turnId = result.turnId;
            }
            catch (error) {
                const errorMsg = error instanceof Error ? error.message : "Failed to start turn";
                self.handleStreamEvent(agent, {
                    type: "turn_failed",
                    provider: agent.provider,
                    error: errorMsg,
                });
                self.finalizeForegroundTurn(agent);
                throw error;
            }
            pendingRun.started = true;
            agent.activeForegroundTurnId = turnId;
            agent.lifecycle = "running";
            self.touchUpdatedAt(agent);
            self.emitState(agent);
            self.logger.trace({
                agentId,
                lifecycle: agent.lifecycle,
                activeForegroundTurnId: agent.activeForegroundTurnId,
            }, "streamAgent: started");
            // Create a pushable queue for this foreground turn
            const queue = [];
            let queueResolve = null;
            let done = false;
            let resolveSettled;
            const settledPromise = new Promise((resolve) => {
                resolveSettled = resolve;
            });
            waiter = {
                turnId,
                settled: false,
                settledPromise,
                resolveSettled,
                callback: (event) => {
                    queue.push(event);
                    if (queueResolve) {
                        queueResolve();
                        queueResolve = null;
                    }
                },
            };
            agent.foregroundTurnWaiters.add(waiter);
            try {
                while (!done) {
                    while (queue.length > 0) {
                        const event = queue.shift();
                        yield event;
                        if (isTurnTerminalEvent(event)) {
                            done = true;
                            break;
                        }
                    }
                    if (!done && queue.length === 0) {
                        if (waiter.settled) {
                            break;
                        }
                        await new Promise((resolve) => {
                            queueResolve = resolve;
                        });
                    }
                }
            }
            finally {
                if (waiter) {
                    agent.foregroundTurnWaiters.delete(waiter);
                    self.settleForegroundTurnWaiter(waiter);
                }
                self.settlePendingForegroundRun(agentId, pendingRun.token);
                if (!agent.activeForegroundTurnId) {
                    await self.refreshRuntimeInfo(agent);
                }
            }
        })();
        return streamForwarder;
    }
    finalizeForegroundTurn(agent) {
        const mutableAgent = agent;
        mutableAgent.activeForegroundTurnId = null;
        const terminalError = mutableAgent.lastError;
        const shouldHoldBusyForReplacement = mutableAgent.pendingReplacement && !terminalError;
        mutableAgent.lifecycle = shouldHoldBusyForReplacement
            ? "running"
            : terminalError
                ? "error"
                : "idle";
        const persistenceHandle = mutableAgent.session.describePersistence() ??
            (mutableAgent.runtimeInfo?.sessionId
                ? { provider: mutableAgent.provider, sessionId: mutableAgent.runtimeInfo.sessionId }
                : null);
        if (persistenceHandle) {
            mutableAgent.persistence = attachPersistenceCwd(persistenceHandle, mutableAgent.cwd);
        }
        this.logger.trace({
            agentId: agent.id,
            lifecycle: mutableAgent.lifecycle,
            terminalError,
            pendingReplacement: mutableAgent.pendingReplacement,
        }, "finalizeForegroundTurn: applying terminal state");
        if (!shouldHoldBusyForReplacement) {
            this.touchUpdatedAt(mutableAgent);
            this.emitState(mutableAgent);
        }
    }
    replaceAgentRun(agentId, prompt, options) {
        const snapshot = this.requireAgent(agentId);
        if (snapshot.lifecycle !== "running" &&
            !snapshot.activeForegroundTurnId &&
            !this.hasPendingForegroundRun(agentId)) {
            return this.streamAgent(agentId, prompt, options);
        }
        const agent = snapshot;
        agent.pendingReplacement = true;
        const self = this;
        return (async function* replaceRunForwarder() {
            try {
                await self.cancelAgentRun(agentId);
                const nextRun = self.streamAgent(agentId, prompt, options);
                for await (const event of nextRun) {
                    yield event;
                }
            }
            catch (error) {
                const latest = self.agents.get(agentId);
                if (latest) {
                    const latestActive = latest;
                    latestActive.pendingReplacement = false;
                    if (!latestActive.activeForegroundTurnId && latestActive.lifecycle === "running") {
                        latestActive.lifecycle = "idle";
                        self.touchUpdatedAt(latestActive);
                        self.emitState(latestActive);
                    }
                }
                throw error;
            }
        })();
    }
    async waitForAgentRunStart(agentId, options) {
        const snapshot = this.getAgent(agentId);
        if (!snapshot) {
            throw new Error(`Agent ${agentId} not found`);
        }
        const pendingRun = this.getPendingForegroundRun(agentId);
        if ((snapshot.lifecycle === "running" || pendingRun?.started) && !snapshot.pendingReplacement) {
            return;
        }
        if (!snapshot.activeForegroundTurnId && !pendingRun && !snapshot.pendingReplacement) {
            throw new Error(`Agent ${agentId} has no pending run`);
        }
        if (options?.signal?.aborted) {
            throw createAbortError(options.signal, "wait_for_agent_start aborted");
        }
        await new Promise((resolve, reject) => {
            if (options?.signal?.aborted) {
                reject(createAbortError(options.signal, "wait_for_agent_start aborted"));
                return;
            }
            let unsubscribe = null;
            let abortHandler = null;
            const cleanup = () => {
                if (unsubscribe) {
                    try {
                        unsubscribe();
                    }
                    catch {
                        // ignore cleanup errors
                    }
                    unsubscribe = null;
                }
                if (abortHandler && options?.signal) {
                    try {
                        options.signal.removeEventListener("abort", abortHandler);
                    }
                    catch {
                        // ignore cleanup errors
                    }
                    abortHandler = null;
                }
            };
            const finishOk = () => {
                cleanup();
                resolve();
            };
            const finishErr = (error) => {
                cleanup();
                reject(error);
            };
            if (options?.signal) {
                abortHandler = () => finishErr(createAbortError(options.signal, "wait_for_agent_start aborted"));
                options.signal.addEventListener("abort", abortHandler, { once: true });
            }
            const checkCurrentState = () => {
                const current = this.getAgent(agentId);
                if (!current) {
                    finishErr(new Error(`Agent ${agentId} not found`));
                    return true;
                }
                const currentPendingRun = this.getPendingForegroundRun(agentId);
                if ((current.lifecycle === "running" || currentPendingRun?.started) &&
                    !current.pendingReplacement) {
                    finishOk();
                    return true;
                }
                if (current.lifecycle === "error" && !currentPendingRun?.started) {
                    finishErr(new Error(current.lastError ?? `Agent ${agentId} failed to start`));
                    return true;
                }
                if (!currentPendingRun && !current.activeForegroundTurnId && !current.pendingReplacement) {
                    finishErr(new Error(`Agent ${agentId} run finished before starting`));
                    return true;
                }
                return false;
            };
            unsubscribe = this.subscribe((event) => {
                if (event.type !== "agent_state" || event.agent.id !== agentId) {
                    return;
                }
                checkCurrentState();
            }, { agentId, replayState: false });
            checkCurrentState();
        });
    }
    async respondToPermission(agentId, requestId, response) {
        const agent = this.requireAgent(agentId);
        agent.inFlightPermissionResponses.add(requestId);
        try {
            const result = await agent.session.respondToPermission(requestId, response);
            agent.pendingPermissions.delete(requestId);
            try {
                await this.refreshSessionState(agent);
            }
            catch {
                // Ignore refresh errors - state sync after permission approval is best effort.
            }
            this.touchUpdatedAt(agent);
            await this.persistSnapshot(agent);
            this.emitState(agent);
            const bufferedResolution = agent.bufferedPermissionResolutions.get(requestId);
            if (bufferedResolution) {
                agent.bufferedPermissionResolutions.delete(requestId);
                this.dispatchStream(agent.id, bufferedResolution);
            }
            return result;
        }
        finally {
            agent.inFlightPermissionResponses.delete(requestId);
            agent.bufferedPermissionResolutions.delete(requestId);
        }
    }
    async cancelAgentRun(agentId) {
        const agent = this.requireAgent(agentId);
        const pendingRun = this.getPendingForegroundRun(agentId);
        const foregroundTurnId = agent.activeForegroundTurnId;
        const hasForegroundTurn = Boolean(foregroundTurnId);
        const isAutonomousRunning = agent.lifecycle === "running" && !hasForegroundTurn && !pendingRun;
        if (!hasForegroundTurn && !isAutonomousRunning && !pendingRun) {
            return false;
        }
        try {
            await agent.session.interrupt();
        }
        catch (error) {
            this.logger.error({ err: error, agentId }, "Failed to interrupt session");
        }
        // The interrupt will produce a turn_canceled/turn_failed event via subscribe(),
        // which flows through the session event dispatcher and settles the foreground turn waiter.
        // Wait briefly for the event to propagate if there's an active foreground turn.
        if (foregroundTurnId) {
            const waiter = Array.from(agent.foregroundTurnWaiters).find((candidate) => candidate.turnId === foregroundTurnId);
            const timeout = new Promise((resolve) => setTimeout(resolve, 2000));
            if (waiter) {
                await Promise.race([waiter.settledPromise, timeout]);
            }
            else if (agent.activeForegroundTurnId === foregroundTurnId) {
                await Promise.race([
                    new Promise((resolve) => {
                        const unsubscribe = this.subscribe((event) => {
                            if (event.type === "agent_state" &&
                                event.agent.id === agentId &&
                                !event.agent.activeForegroundTurnId) {
                                unsubscribe();
                                resolve();
                            }
                        }, { agentId, replayState: false });
                    }),
                    timeout,
                ]);
            }
            // The waiter settling wakes up the streamForwarder generator, but its
            // finally block (which deletes the pendingForegroundRun) runs asynchronously.
            // Wait for the pending run to be fully cleaned up so the next streamAgent
            // call doesn't see a stale entry and reject with "already has an active run".
            if (pendingRun && !pendingRun.settled) {
                await Promise.race([pendingRun.settledPromise, timeout]);
            }
        }
        else if (pendingRun) {
            const timeout = new Promise((resolve) => setTimeout(resolve, 2000));
            await Promise.race([pendingRun.settledPromise, timeout]);
        }
        // If the foreground turn is still stuck after the timeout, force-dispatch a
        // synthetic turn_canceled so the normal event pipeline cleans up
        // activeForegroundTurnId, settles waiters, and unblocks the streamForwarder.
        if (foregroundTurnId && agent.activeForegroundTurnId === foregroundTurnId) {
            this.logger.warn({ agentId, foregroundTurnId }, "cancelAgentRun: foreground turn still active after timeout, force-canceling");
            this.dispatchSessionEvent(agent, {
                type: "turn_canceled",
                provider: agent.provider,
                reason: "interrupted",
                turnId: foregroundTurnId,
            });
            // The synthetic event unblocks the streamForwarder generator, whose finally
            // block settles the pending foreground run asynchronously. Wait for it.
            const staleRun = this.getPendingForegroundRun(agentId);
            if (staleRun && !staleRun.settled) {
                await staleRun.settledPromise;
            }
        }
        // Clear any pending permissions that weren't cleaned up by handleStreamEvent.
        if (agent.pendingPermissions.size > 0) {
            for (const [requestId] of agent.pendingPermissions) {
                this.dispatchStream(agent.id, {
                    type: "permission_resolved",
                    provider: agent.provider,
                    requestId,
                    resolution: { behavior: "deny", message: "Interrupted" },
                });
            }
            agent.pendingPermissions.clear();
            this.touchUpdatedAt(agent);
            this.emitState(agent);
        }
        return true;
    }
    getPendingPermissions(agentId) {
        const agent = this.requireAgent(agentId);
        return Array.from(agent.pendingPermissions.values());
    }
    peekPendingPermission(agent) {
        const iterator = agent.pendingPermissions.values().next();
        return iterator.done ? null : iterator.value;
    }
    async hydrateTimelineFromProvider(agentId) {
        const agent = this.requireAgent(agentId);
        await this.hydrateTimeline(agent);
    }
    getLastAssistantMessage(agentId) {
        const agent = this.agents.get(agentId);
        if (!agent) {
            return null;
        }
        return this.getLastAssistantMessageFromTimeline(agent.timeline);
    }
    getLastAssistantMessageFromTimeline(timeline) {
        // Collect the last contiguous assistant messages (Claude streams chunks)
        const chunks = [];
        for (let i = timeline.length - 1; i >= 0; i--) {
            const item = timeline[i];
            if (item.type !== "assistant_message") {
                if (chunks.length) {
                    break;
                }
                continue;
            }
            chunks.push(item.text);
        }
        if (!chunks.length) {
            return null;
        }
        return chunks.reverse().join("");
    }
    async waitForAgentEvent(agentId, options) {
        const snapshot = this.getAgent(agentId);
        if (!snapshot) {
            throw new Error(`Agent ${agentId} not found`);
        }
        const pendingForegroundRun = this.getPendingForegroundRun(agentId);
        const hasForegroundTurn = Boolean(snapshot.activeForegroundTurnId) || Boolean(pendingForegroundRun);
        const immediatePermission = this.peekPendingPermission(snapshot);
        if (immediatePermission) {
            return {
                status: snapshot.lifecycle,
                permission: immediatePermission,
                lastMessage: this.getLastAssistantMessage(agentId),
            };
        }
        const initialStatus = snapshot.lifecycle;
        const initialBusy = isAgentBusy(initialStatus) || hasForegroundTurn;
        const waitForActive = options?.waitForActive ?? false;
        if (!waitForActive && !initialBusy) {
            return {
                status: initialStatus,
                permission: null,
                lastMessage: this.getLastAssistantMessage(agentId),
            };
        }
        if (waitForActive && !initialBusy && !hasForegroundTurn) {
            return {
                status: initialStatus,
                permission: null,
                lastMessage: this.getLastAssistantMessage(agentId),
            };
        }
        if (options?.signal?.aborted) {
            throw createAbortError(options.signal, "wait_for_agent aborted");
        }
        return await new Promise((resolve, reject) => {
            // Bug #1 Fix: Check abort signal AGAIN inside Promise constructor
            // to avoid race condition between pre-Promise check and abort listener registration
            if (options?.signal?.aborted) {
                reject(createAbortError(options.signal, "wait_for_agent aborted"));
                return;
            }
            let currentStatus = initialStatus;
            let hasStarted = isAgentBusy(initialStatus) ||
                Boolean(snapshot.activeForegroundTurnId) ||
                Boolean(pendingForegroundRun?.started);
            let terminalStatusOverride = null;
            // Bug #3 Fix: Declare unsubscribe and abortHandler upfront so cleanup can reference them
            let unsubscribe = null;
            let abortHandler = null;
            const cleanup = () => {
                // Clean up subscription
                if (unsubscribe) {
                    try {
                        unsubscribe();
                    }
                    catch {
                        // ignore cleanup errors
                    }
                    unsubscribe = null;
                }
                // Clean up abort listener
                if (abortHandler && options?.signal) {
                    try {
                        options.signal.removeEventListener("abort", abortHandler);
                    }
                    catch {
                        // ignore cleanup errors
                    }
                    abortHandler = null;
                }
            };
            const finish = (permission) => {
                cleanup();
                resolve({
                    status: currentStatus,
                    permission,
                    lastMessage: this.getLastAssistantMessage(agentId),
                });
            };
            // Bug #3 Fix: Set up abort handler BEFORE subscription
            // to ensure cleanup handlers exist before callback can fire
            if (options?.signal) {
                abortHandler = () => {
                    cleanup();
                    reject(createAbortError(options.signal, "wait_for_agent aborted"));
                };
                options.signal.addEventListener("abort", abortHandler, { once: true });
            }
            // Bug #3 Fix: Now subscribe with cleanup handlers already in place
            // This prevents race condition if callback fires synchronously with replayState: true
            unsubscribe = this.subscribe((event) => {
                if (event.type === "agent_state") {
                    currentStatus = event.agent.lifecycle;
                    const pending = this.peekPendingPermission(event.agent);
                    if (pending) {
                        finish(pending);
                        return;
                    }
                    if (isAgentBusy(event.agent.lifecycle)) {
                        hasStarted = true;
                        return;
                    }
                    if (!waitForActive || hasStarted) {
                        if (terminalStatusOverride) {
                            currentStatus = terminalStatusOverride;
                        }
                        finish(null);
                    }
                    return;
                }
                if (event.type === "agent_stream") {
                    if (event.event.type === "permission_requested") {
                        finish(event.event.request);
                        return;
                    }
                    if (event.event.type === "turn_failed") {
                        hasStarted = true;
                        terminalStatusOverride = "error";
                        return;
                    }
                    if (event.event.type === "turn_completed") {
                        hasStarted = true;
                    }
                    if (event.event.type === "turn_canceled") {
                        hasStarted = true;
                    }
                }
            }, { agentId, replayState: true });
        });
    }
    async registerSession(session, config, agentId, options) {
        const resolvedAgentId = validateAgentId(agentId, "registerSession");
        if (this.agents.has(resolvedAgentId)) {
            throw new Error(`Agent with id ${resolvedAgentId} already exists`);
        }
        const initialPersistedTitle = await this.resolveInitialPersistedTitle(resolvedAgentId, config);
        const now = new Date();
        const initialTimeline = options?.timeline ? [...options.timeline] : [];
        const initialTimelineRows = options?.timelineRows?.length
            ? options.timelineRows.map((row) => ({ ...row }))
            : this.buildTimelineRowsFromItems(initialTimeline, options?.timelineNextSeq ?? 1, (options?.updatedAt ?? options?.createdAt ?? now).toISOString());
        const derivedNextSeq = options?.timelineNextSeq ??
            (initialTimelineRows.length
                ? initialTimelineRows[initialTimelineRows.length - 1].seq + 1
                : 1);
        const managed = {
            id: resolvedAgentId,
            provider: config.provider,
            cwd: config.cwd,
            session,
            capabilities: session.capabilities,
            config,
            runtimeInfo: undefined,
            lifecycle: "initializing",
            createdAt: options?.createdAt ?? now,
            updatedAt: options?.updatedAt ?? now,
            availableModes: [],
            currentModeId: null,
            pendingPermissions: new Map(),
            bufferedPermissionResolutions: new Map(),
            inFlightPermissionResponses: new Set(),
            pendingReplacement: false,
            activeForegroundTurnId: null,
            foregroundTurnWaiters: new Set(),
            unsubscribeSession: null,
            timeline: initialTimeline,
            timelineRows: initialTimelineRows,
            timelineEpoch: options?.timelineEpoch ?? randomUUID(),
            timelineNextSeq: derivedNextSeq,
            persistence: attachPersistenceCwd(session.describePersistence(), config.cwd),
            historyPrimed: options?.historyPrimed ?? false,
            lastUserMessageAt: options?.lastUserMessageAt ?? null,
            lastUsage: options?.lastUsage,
            lastError: options?.lastError,
            attention: options?.attention != null
                ? options.attention.requiresAttention
                    ? {
                        requiresAttention: true,
                        attentionReason: options.attention.attentionReason,
                        attentionTimestamp: new Date(options.attention.attentionTimestamp),
                    }
                    : { requiresAttention: false }
                : { requiresAttention: false },
            internal: config.internal ?? false,
            labels: options?.labels ?? {},
        };
        this.agents.set(resolvedAgentId, managed);
        // Initialize previousStatus to track transitions
        this.previousStatuses.set(resolvedAgentId, managed.lifecycle);
        await this.refreshRuntimeInfo(managed);
        await this.persistSnapshot(managed, {
            title: initialPersistedTitle,
        });
        this.emitState(managed);
        await this.refreshSessionState(managed);
        managed.lifecycle = "idle";
        await this.persistSnapshot(managed);
        this.emitState(managed);
        this.subscribeToSession(managed);
        return { ...managed };
    }
    subscribeToSession(agent) {
        if (agent.unsubscribeSession) {
            return;
        }
        const agentId = agent.id;
        const unsubscribe = agent.session.subscribe((event) => {
            const current = this.agents.get(agentId);
            if (!current) {
                return;
            }
            this.dispatchSessionEvent(current, event);
        });
        agent.unsubscribeSession = unsubscribe;
    }
    dispatchSessionEvent(agent, event) {
        const turnId = event.turnId;
        const matchingWaiters = turnId == null
            ? []
            : Array.from(agent.foregroundTurnWaiters).filter((waiter) => waiter.turnId === turnId && !waiter.settled);
        this.handleStreamEvent(agent, event);
        for (const waiter of matchingWaiters) {
            waiter.callback(event);
            if (isTurnTerminalEvent(event)) {
                this.settleForegroundTurnWaiter(waiter);
            }
        }
    }
    settleForegroundTurnWaiter(waiter) {
        if (waiter.settled) {
            return;
        }
        waiter.settled = true;
        waiter.resolveSettled();
    }
    createPendingForegroundRun() {
        let resolveSettled;
        const settledPromise = new Promise((resolve) => {
            resolveSettled = resolve;
        });
        return {
            token: randomUUID(),
            started: false,
            settled: false,
            settledPromise,
            resolveSettled,
        };
    }
    getPendingForegroundRun(agentId) {
        return this.pendingForegroundRuns.get(agentId) ?? null;
    }
    hasPendingForegroundRun(agentId) {
        return this.pendingForegroundRuns.has(agentId);
    }
    settlePendingForegroundRun(agentId, token) {
        const pendingRun = this.pendingForegroundRuns.get(agentId);
        if (!pendingRun) {
            return;
        }
        if (token && pendingRun.token !== token) {
            return;
        }
        this.pendingForegroundRuns.delete(agentId);
        if (pendingRun.settled) {
            return;
        }
        pendingRun.settled = true;
        pendingRun.resolveSettled();
    }
    async resolveInitialPersistedTitle(agentId, config) {
        const existing = await this.registry?.get(agentId);
        if (existing) {
            return existing.title ?? null;
        }
        if (Object.prototype.hasOwnProperty.call(config, "title")) {
            return config.title ?? null;
        }
        return null;
    }
    buildTimelineRowsFromItems(items, startSeq, timestamp) {
        let nextSeq = startSeq;
        return items.map((item) => {
            const row = {
                seq: nextSeq,
                timestamp,
                item,
            };
            nextSeq += 1;
            return row;
        });
    }
    ensureTimelineState(agent) {
        const minSeq = agent.timelineRows.length ? agent.timelineRows[0].seq : 0;
        const maxSeq = agent.timelineRows.length
            ? agent.timelineRows[agent.timelineRows.length - 1].seq
            : 0;
        return {
            rows: agent.timelineRows,
            epoch: agent.timelineEpoch,
            nextSeq: agent.timelineNextSeq,
            minSeq,
            maxSeq,
        };
    }
    async persistSnapshot(agent, options) {
        if (!this.registry) {
            return;
        }
        // Don't persist internal agents - they're ephemeral system tasks
        if (agent.internal) {
            return;
        }
        await this.registry.applySnapshot(agent, options);
    }
    async refreshSessionState(agent) {
        try {
            const modes = await agent.session.getAvailableModes();
            agent.availableModes = modes;
        }
        catch {
            agent.availableModes = [];
        }
        try {
            agent.currentModeId = await agent.session.getCurrentMode();
        }
        catch {
            agent.currentModeId = null;
        }
        try {
            const pending = agent.session.getPendingPermissions();
            agent.pendingPermissions = new Map(pending.map((request) => [request.id, request]));
        }
        catch {
            agent.pendingPermissions.clear();
        }
        this.syncFeaturesFromSession(agent);
        await this.refreshRuntimeInfo(agent);
    }
    async refreshRuntimeInfo(agent) {
        try {
            const newInfo = await agent.session.getRuntimeInfo();
            const changed = newInfo.model !== agent.runtimeInfo?.model ||
                newInfo.thinkingOptionId !== agent.runtimeInfo?.thinkingOptionId ||
                newInfo.sessionId !== agent.runtimeInfo?.sessionId ||
                newInfo.modeId !== agent.runtimeInfo?.modeId;
            agent.runtimeInfo = newInfo;
            if (!agent.persistence && newInfo.sessionId) {
                agent.persistence = attachPersistenceCwd({ provider: agent.provider, sessionId: newInfo.sessionId }, agent.cwd);
            }
            // Emit state if runtimeInfo changed so clients get the updated model
            if (changed) {
                this.emitState(agent);
            }
        }
        catch {
            // Keep existing runtimeInfo if refresh fails.
        }
    }
    async hydrateTimeline(agent) {
        if (agent.historyPrimed) {
            return;
        }
        agent.historyPrimed = true;
        const canonicalUserMessagesById = new Map(agent.timelineRows.flatMap((row) => {
            if (row.item.type !== "user_message") {
                return [];
            }
            const messageId = normalizeMessageId(row.item.messageId);
            if (!messageId) {
                return [];
            }
            return [[messageId, row.item.text]];
        }));
        try {
            for await (const event of agent.session.streamHistory()) {
                this.handleStreamEvent(agent, event, {
                    fromHistory: true,
                    canonicalUserMessagesById: canonicalUserMessagesById.size > 0 ? canonicalUserMessagesById : undefined,
                });
            }
        }
        catch {
            // ignore history failures
        }
    }
    handleStreamEvent(agent, event, options) {
        const eventTurnId = event.turnId;
        const isForegroundEvent = Boolean(eventTurnId && agent.activeForegroundTurnId === eventTurnId);
        // Only update timestamp for live events, not history replay
        if (!options?.fromHistory) {
            this.touchUpdatedAt(agent);
        }
        let timelineRow = null;
        let shouldDispatchEvent = true;
        switch (event.type) {
            case "thread_started":
                {
                    const previousSessionId = agent.persistence?.sessionId ?? null;
                    const handle = agent.session.describePersistence();
                    if (handle) {
                        agent.persistence = attachPersistenceCwd(handle, agent.cwd);
                        if (agent.persistence?.sessionId !== previousSessionId) {
                            this.emitState(agent);
                        }
                    }
                    void this.refreshRuntimeInfo(agent);
                }
                break;
            case "usage_updated":
                agent.lastUsage = event.usage;
                this.emitState(agent);
                break;
            case "timeline":
                // Skip provider-replayed user_message items during history hydration.
                if (options?.fromHistory && event.item.type === "user_message") {
                    const eventMessageId = normalizeMessageId(event.item.messageId);
                    if (eventMessageId) {
                        const canonicalText = options?.canonicalUserMessagesById?.get(eventMessageId);
                        if (canonicalText === event.item.text) {
                            break;
                        }
                    }
                }
                // Suppress user_message echoes for the active foreground turn —
                // these are already recorded by recordUserMessage().
                if (!options?.fromHistory && event.item.type === "user_message" && isForegroundEvent) {
                    const eventMessageId = normalizeMessageId(event.item.messageId);
                    const eventText = event.item.text;
                    if (eventMessageId) {
                        const alreadyRecorded = agent.timelineRows.some((row) => {
                            if (row.item.type !== "user_message") {
                                return false;
                            }
                            const rowMessageId = normalizeMessageId(row.item.messageId);
                            return rowMessageId === eventMessageId && row.item.text === eventText;
                        });
                        if (alreadyRecorded) {
                            break;
                        }
                    }
                }
                timelineRow = this.recordTimeline(agent, event.item);
                if (!options?.fromHistory && event.item.type === "user_message") {
                    agent.lastUserMessageAt = new Date();
                    this.emitState(agent);
                }
                break;
            case "turn_completed":
                this.logger.trace({
                    agentId: agent.id,
                    lifecycle: agent.lifecycle,
                    activeForegroundTurnId: agent.activeForegroundTurnId,
                    eventTurnId,
                }, "handleStreamEvent: turn_completed");
                agent.lastUsage = event.usage;
                agent.lastError = undefined;
                // For autonomous turns (not foreground), transition to idle
                // unless a replacement is pending (avoid idle flash during replace)
                if (!isForegroundEvent && agent.lifecycle !== "idle" && !agent.pendingReplacement) {
                    agent.lifecycle = "idle";
                    this.emitState(agent);
                }
                void this.refreshRuntimeInfo(agent);
                break;
            case "turn_failed":
                this.logger.warn({
                    agentId: agent.id,
                    lifecycle: agent.lifecycle,
                    activeForegroundTurnId: agent.activeForegroundTurnId,
                    eventTurnId,
                    error: event.error,
                    code: event.code,
                    diagnostic: event.diagnostic,
                }, "handleStreamEvent: turn_failed");
                // For autonomous turns, set error state directly
                if (!isForegroundEvent) {
                    agent.lifecycle = "error";
                }
                agent.lastError = event.error;
                this.appendSystemErrorTimelineMessage(agent, event.provider, this.formatTurnFailedMessage(event), options);
                for (const [requestId] of agent.pendingPermissions) {
                    agent.pendingPermissions.delete(requestId);
                    if (!options?.fromHistory) {
                        this.dispatchStream(agent.id, {
                            type: "permission_resolved",
                            provider: event.provider,
                            requestId,
                            resolution: { behavior: "deny", message: "Turn failed" },
                        });
                    }
                }
                if (!isForegroundEvent) {
                    this.emitState(agent);
                }
                break;
            case "turn_canceled":
                this.logger.trace({
                    agentId: agent.id,
                    lifecycle: agent.lifecycle,
                    activeForegroundTurnId: agent.activeForegroundTurnId,
                    eventTurnId,
                }, "handleStreamEvent: turn_canceled");
                // For autonomous turns, transition to idle
                // unless a replacement is pending (avoid idle flash during replace)
                if (!isForegroundEvent && !agent.pendingReplacement) {
                    agent.lifecycle = "idle";
                }
                agent.lastError = undefined;
                for (const [requestId] of agent.pendingPermissions) {
                    agent.pendingPermissions.delete(requestId);
                    if (!options?.fromHistory) {
                        this.dispatchStream(agent.id, {
                            type: "permission_resolved",
                            provider: event.provider,
                            requestId,
                            resolution: { behavior: "deny", message: "Interrupted" },
                        });
                    }
                }
                if (!isForegroundEvent) {
                    this.emitState(agent);
                }
                break;
            case "turn_started":
                this.logger.trace({
                    agentId: agent.id,
                    lifecycle: agent.lifecycle,
                    activeForegroundTurnId: agent.activeForegroundTurnId,
                    eventTurnId,
                }, "handleStreamEvent: turn_started");
                // For autonomous turn_started (no foreground match), set running
                if (!isForegroundEvent) {
                    agent.lifecycle = "running";
                    this.emitState(agent);
                }
                break;
            case "permission_requested":
                {
                    const hadPendingPermissions = agent.pendingPermissions.size > 0;
                    agent.pendingPermissions.set(event.request.id, event.request);
                    if (!hadPendingPermissions && !agent.internal) {
                        this.broadcastAgentAttention(agent, "permission");
                    }
                }
                this.emitState(agent);
                break;
            case "permission_resolved":
                agent.pendingPermissions.delete(event.requestId);
                if (!options?.fromHistory && agent.inFlightPermissionResponses.has(event.requestId)) {
                    agent.bufferedPermissionResolutions.set(event.requestId, event);
                    shouldDispatchEvent = false;
                    break;
                }
                this.emitState(agent);
                break;
            default:
                break;
        }
        if (!options?.fromHistory && isForegroundEvent && isTurnTerminalEvent(event)) {
            this.finalizeForegroundTurn(agent);
        }
        // Skip dispatching individual stream events during history replay.
        if (!options?.fromHistory && shouldDispatchEvent) {
            this.dispatchStream(agent.id, event, timelineRow
                ? {
                    seq: timelineRow.seq,
                    epoch: this.ensureTimelineState(agent).epoch,
                }
                : undefined);
        }
    }
    appendSystemErrorTimelineMessage(agent, provider, message, options) {
        if (options?.fromHistory) {
            return;
        }
        const normalized = message.trim();
        if (!normalized) {
            return;
        }
        const text = `${SYSTEM_ERROR_PREFIX} ${normalized}`;
        const lastItem = agent.timelineRows[agent.timelineRows.length - 1]?.item;
        if (lastItem?.type === "assistant_message" && lastItem.text === text) {
            return;
        }
        const item = { type: "assistant_message", text };
        const row = this.recordTimeline(agent, item);
        this.dispatchStream(agent.id, {
            type: "timeline",
            item,
            provider,
        }, {
            seq: row.seq,
            epoch: this.ensureTimelineState(agent).epoch,
        });
    }
    formatTurnFailedMessage(event) {
        const base = event.error.trim();
        const parts = [base.length > 0 ? base : "Provider run failed"];
        const code = event.code?.trim();
        if (code) {
            parts.push(`code: ${code}`);
        }
        const diagnostic = event.diagnostic?.trim();
        if (diagnostic && diagnostic !== base) {
            parts.push(diagnostic);
        }
        return parts.join("\n\n");
    }
    recordTimeline(agent, item) {
        const timelineState = this.ensureTimelineState(agent);
        const row = {
            seq: timelineState.nextSeq,
            timestamp: new Date().toISOString(),
            item,
        };
        agent.timelineNextSeq = timelineState.nextSeq + 1;
        agent.timeline.push(item);
        timelineState.rows.push(row);
        if (typeof this.maxTimelineItems === "number" &&
            agent.timeline.length > this.maxTimelineItems) {
            const removeCount = agent.timeline.length - this.maxTimelineItems;
            agent.timeline.splice(0, removeCount);
            timelineState.rows.splice(0, removeCount);
        }
        return row;
    }
    emitState(agent) {
        // Keep attention as an edge-triggered unread signal, not a level signal.
        this.checkAndSetAttention(agent);
        this.syncFeaturesFromSession(agent);
        this.dispatch({
            type: "agent_state",
            agent: { ...agent },
        });
    }
    syncFeaturesFromSession(agent) {
        if ("session" in agent && agent.session?.features) {
            agent.features = agent.session.features;
        }
    }
    checkAndSetAttention(agent) {
        const previousStatus = this.previousStatuses.get(agent.id);
        const currentStatus = agent.lifecycle;
        // Track the new status
        this.previousStatuses.set(agent.id, currentStatus);
        // Skip attention tracking for internal agents
        if (agent.internal) {
            return;
        }
        // Skip if already requires attention
        if (agent.attention.requiresAttention) {
            return;
        }
        // Check if agent transitioned from running to idle (finished)
        if (previousStatus === "running" && currentStatus === "idle") {
            agent.attention = {
                requiresAttention: true,
                attentionReason: "finished",
                attentionTimestamp: new Date(),
            };
            this.broadcastAgentAttention(agent, "finished");
            this.enqueueBackgroundPersist(agent);
            return;
        }
        // Check if agent entered error state
        if (previousStatus !== "error" && currentStatus === "error") {
            agent.attention = {
                requiresAttention: true,
                attentionReason: "error",
                attentionTimestamp: new Date(),
            };
            this.broadcastAgentAttention(agent, "error");
            this.enqueueBackgroundPersist(agent);
            return;
        }
    }
    enqueueBackgroundPersist(agent) {
        const task = this.persistSnapshot(agent).catch((err) => {
            this.logger.error({ err, agentId: agent.id }, "Failed to persist agent snapshot");
        });
        this.trackBackgroundTask(task);
    }
    trackBackgroundTask(task) {
        this.backgroundTasks.add(task);
        void task.finally(() => {
            this.backgroundTasks.delete(task);
        });
    }
    /**
     * Flush any background persistence work (best-effort).
     * Used by daemon shutdown paths to avoid unhandled rejections after cleanup.
     */
    async flush() {
        // Drain tasks, including tasks spawned while awaiting.
        while (this.backgroundTasks.size > 0) {
            const pending = Array.from(this.backgroundTasks);
            await Promise.allSettled(pending);
        }
    }
    broadcastAgentAttention(agent, reason) {
        this.onAgentAttention?.({
            agentId: agent.id,
            provider: agent.provider,
            reason,
        });
    }
    dispatchStream(agentId, event, metadata) {
        this.dispatch({ type: "agent_stream", agentId, event, ...metadata });
    }
    dispatch(event) {
        for (const subscriber of this.subscribers) {
            if (subscriber.agentId &&
                event.type === "agent_stream" &&
                subscriber.agentId !== event.agentId) {
                continue;
            }
            if (subscriber.agentId &&
                event.type === "agent_state" &&
                subscriber.agentId !== event.agent.id) {
                continue;
            }
            // Skip internal agents for global subscribers (those without a specific agentId)
            if (!subscriber.agentId) {
                if (event.type === "agent_state" && event.agent.internal) {
                    continue;
                }
                if (event.type === "agent_stream") {
                    const agent = this.agents.get(event.agentId);
                    if (agent?.internal) {
                        continue;
                    }
                }
            }
            subscriber.callback(event);
        }
    }
    async normalizeConfig(config) {
        const normalized = { ...config };
        // Always resolve cwd to absolute path for consistent history file lookup
        if (normalized.cwd) {
            normalized.cwd = resolve(normalized.cwd);
            try {
                const cwdStats = await stat(normalized.cwd);
                if (!cwdStats.isDirectory()) {
                    throw new Error(`Working directory is not a directory: ${normalized.cwd}`);
                }
            }
            catch (error) {
                if (error instanceof Error &&
                    "code" in error &&
                    error.code === "ENOENT") {
                    throw new Error(`Working directory does not exist: ${normalized.cwd}`);
                }
                if (error instanceof Error) {
                    throw error;
                }
                throw new Error(`Failed to access working directory: ${normalized.cwd}`);
            }
        }
        if (typeof normalized.model === "string") {
            const trimmed = normalized.model.trim();
            normalized.model = trimmed.length > 0 && trimmed !== "default" ? trimmed : undefined;
        }
        if (!normalized.model) {
            const client = this.clients.get(normalized.provider);
            if (client) {
                try {
                    const models = await client.listModels();
                    const defaultModel = models.find((model) => model.isDefault) ?? models[0];
                    if (defaultModel) {
                        normalized.model = defaultModel.id;
                    }
                }
                catch {
                    // Provider may not support model listing — leave model undefined
                }
            }
        }
        if (!normalized.modeId) {
            try {
                normalized.modeId =
                    getAgentProviderDefinition(normalized.provider).defaultModeId ?? undefined;
            }
            catch {
                // Unknown provider
            }
        }
        return normalized;
    }
    buildLaunchContext(agentId) {
        return {
            env: {
                SEAWORK_AGENT_ID: agentId,
            },
        };
    }
    requireClient(provider) {
        const client = this.clients.get(provider);
        if (!client) {
            throw new Error(`No client registered for provider '${provider}'`);
        }
        return client;
    }
    requireAgent(id) {
        const normalizedId = validateAgentId(id, "requireAgent");
        const agent = this.agents.get(normalizedId);
        if (!agent) {
            throw new Error(`Unknown agent '${normalizedId}'`);
        }
        return agent;
    }
}
//# sourceMappingURL=agent-manager.js.map