import { mkdirSync } from 'fs';
import express from 'express';
import { createServer } from 'http';
import { randomBytes, timingSafeEqual } from 'crypto';
import { basename } from 'path';
import { readAllowedMediaFile, resolveMediaPathParam } from './media-path.js';
import { Server as SocketServer } from 'socket.io';
import { activityTracker, parseIdleMsQuery, } from './activity-tracker.js';
function markAgentWorkingInState(state, agentId) {
    const tabs = (state.tabs ?? []).map((t) => {
        const match = t.id === agentId ||
            t.composerId === agentId ||
            (state.activeComposerId === agentId && t.active);
        return match && !t.isWorking ? { ...t, isWorking: true } : t;
    });
    let sidebarRepos;
    if (state.sidebarRepos?.length) {
        let changed = false;
        sidebarRepos = state.sidebarRepos.map((repo) => ({
            ...repo,
            agents: repo.agents.map((a) => {
                if (a.id !== agentId || a.isWorking)
                    return a;
                changed = true;
                return { ...a, isWorking: true };
            }),
        }));
        if (!changed)
            sidebarRepos = undefined;
    }
    return {
        agentWorking: true,
        agentStatus: 'working',
        tabs,
        ...(sidebarRepos ? { sidebarRepos } : {}),
    };
}
import { isSyntheticAgentId, resolveJsonlFilePath } from './agent-title-match.js';
import { isAgentsWindowTitle } from './cursor-window-kind.js';
import { ChatDisplayStore } from './chat-display-store.js';
import { resolveHistoryLimit } from './history-request.js';
import { AGENT_HISTORY_INITIAL_TURNS, sliceMessagesByTurns, } from './history-turns.js';
import { assertMonotonicLenta, filterClientDisplayList, prepareChatMessagesForDisplay, } from './chat-display.js';
import { isKeepAwakeActive } from './keep-awake.js';
function sleepMs(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
import { mergeSidebarWithJsonl } from './sidebar-merge.js';
import { transcribeAudioBuffer } from './openai-transcribe.js';
import { extensionForMime, saveUploadedImage } from './image-upload-store.js';
import { AgentCompletionPush, suppressAgentCompletionPush } from './agent-completion-push.js';
import { RelayUpstream } from './relay-upstream.js';
import { bridgePipelineLog, bridgePipelineReportLines, bridgePipelineSnapshot, } from './history-pipeline-log.js';
import { DEBUG_CHATS_PAGE_HTML } from './debug-chats-page.js';
import { isChatHistoryFromJsonl } from './chat-history-mode.js';
import { isAgentGenerating, isChatSyncedWithCursor, isComposerUuid, resolveCursorActiveComposerId, } from './chat-sync.js';
import { readJsonlLiveSnapshot } from './jsonl-live-debug.js';
import { patchStateForEditorNewChat } from './editor-new-chat-state.js';
import { checkMonotonicOrder, checkOverlayAfterArchive, checkOverlayDuplicatesArchive, checkPayloadCompose, checkStoreCompose, checkSyncGate, compareDomTailToLenta, formatLentaIssue, previewRow, } from './lenta-debug.js';
import { getLastLentaEmit, getLentaSeqJournal, getLentaSeqLogPath, messagesFingerprint, recordLentaSeqEmit, } from './lenta-seq-journal.js';
import { isLentaDeliveryPending } from './lenta-delivery.js';
import { captureLentaIfEnabled, captureSessionDir, listCaptureSteps, readCaptureConfig, writeCaptureConfig, clearCaptureConfig, } from './lenta-capture.js';
export class Relay {
    stateManager;
    commandExecutor;
    cdpBridge;
    jsonlIndex;
    messageDebugStore;
    domExtractor;
    lastJsonlIndex = { repos: [], updatedAt: 0 };
    indexEmitTimer = null;
    lastIndexBroadcastAt = 0;
    lastSidebarIndexKey = '';
    lastJsonlIndexKey = '';
    config;
    app = express();
    httpServer = createServer(this.app);
    io;
    tokens = new Set();
    upstream = null;
    agentCompletionPush = null;
    pendingPushPayloads = [];
    chatDisplay = new ChatDisplayStore();
    /** Raw JSONL row count last sent per agent (live `append` emits). */
    lastEmittedJsonlRows = new Map();
    /** Last display tail pushed to app — re-emit when user/assistant tail changes. */
    lastEmittedLentaSig = new Map();
    /** Display bubble count last sent — block regression 214→74 style wipes on phone. */
    lastEmittedHistLen = new Map();
    /** How many user-turns (tail) to send to the phone for this agent. */
    clientHistoryTurns = new Map();
    lentaPendingSince = new Map();
    /** After `agents:unsubscribe` while DOM overlay still pending — JSONL watch until merged. */
    jsonlDrainingAgents = new Map();
    static LENTA_PENDING_FORCE_MS = 12_000;
    static JSONL_DRAIN_MAX_MS = parseInt(process.env.JSONL_DRAIN_MAX_MS ?? '900000', 10);
    /** Socket `agent:messages` coalesce — store ingest is immediate (see ingestDomOverlayFromState). */
    static DOM_OVERLAY_EMIT_MS = parseInt(process.env.DOM_OVERLAY_EMIT_MS ?? '50', 10);
    domOverlayTimer = null;
    cachedLentaPendingByAgent = {};
    /** One CDP command at a time — parallel focus/send/stop was causing 30–60s stalls. */
    commandChain = Promise.resolve();
    /** Subscribe/focus serialized — must not block socket handlers (phone waits on connect). */
    subscribeChain = Promise.resolve();
    /** CDP focus_agent serialized separately from subscribe (JSONL emit stays fast). */
    focusChain = Promise.resolve();
    constructor(config, stateManager, commandExecutor, cdpBridge, jsonlIndex, messageDebugStore, domExtractor) {
        this.stateManager = stateManager;
        this.commandExecutor = commandExecutor;
        this.cdpBridge = cdpBridge;
        this.jsonlIndex = jsonlIndex;
        this.messageDebugStore = messageDebugStore;
        this.domExtractor = domExtractor;
        this.config = config;
        this.io = new SocketServer(this.httpServer, {
            cors: { origin: true, credentials: true },
            maxHttpBufferSize: 20e6,
        });
        this.setupHttp();
        this.setupSocket();
        this.wireEvents();
        if (this.config.relayUrl) {
            if (!this.config.relayToken) {
                console.warn('[relay] RELAY_URL set but RELAY_TOKEN empty — upstream disabled');
            }
            else {
                this.upstream = new RelayUpstream(this.config, (event, ...args) => {
                    this.handleRemoteClientEvent(event, ...args);
                }, () => this.flushPendingPushPayloads());
                this.agentCompletionPush = new AgentCompletionPush((payload) => this.emitAgentCompletedPush(payload), () => this.lastJsonlIndex, () => this.stateManager.getState(), {
                    readiness: {
                        isSubscribed: (agentId) => this.jsonlIndex.getSubscribedAgents().has(agentId),
                        getSubscribeTitle: (agentId) => this.jsonlIndex.getSubscribedAgents().get(agentId)?.title,
                        getDisplayMessages: (agentId) => this.chatDisplay.getDisplayMessages(agentId),
                        getJsonlHistory: (agentId) => this.chatDisplay.getJsonlHistory(agentId),
                        getClientTurnLimit: (agentId) => this.getClientTurnLimit(agentId),
                    },
                    requestContentSync: (agentId, state) => this.requestAgentCompletionContentSync(agentId, state),
                });
                this.upstream.connect();
            }
        }
    }
    emitAgentCompletedPush(payload) {
        if (this.upstream?.connected) {
            this.upstream.emit('push:agent-completed', payload);
            console.log(`[agent-completion-push] upstream emit agentId=${payload.agentId.slice(0, 12)} title=${payload.chatTitle.slice(0, 40)}`);
            return;
        }
        this.pendingPushPayloads.push(payload);
        console.warn(`[agent-completion-push] queued for relay (${this.pendingPushPayloads.length}) upstream disconnected`);
    }
    flushPendingPushPayloads() {
        if (!this.upstream?.connected || !this.pendingPushPayloads.length)
            return;
        const byAgent = new Map();
        for (const payload of this.pendingPushPayloads.splice(0)) {
            byAgent.set(payload.agentId, payload);
        }
        for (const payload of byAgent.values()) {
            this.upstream.emit('push:agent-completed', payload);
        }
        console.log(`[agent-completion-push] flushed ${byAgent.size} queued push(es) to relay`);
    }
    observeAgentCompletionForPush() {
        this.agentCompletionPush?.observe(this.stateManager.getState());
    }
    requestAgentCompletionContentSync(agentId, state) {
        void this.ensureJsonlBaselineForAgent(agentId, state);
        this.flushLentaDelivery(agentId, 'push_ready');
    }
    listen() {
        return new Promise((resolve) => {
            this.httpServer.listen(this.config.serverPort, this.config.serverHost, () => {
                console.log(`[relay] http://${this.config.serverHost}:${this.config.serverPort}`);
                resolve();
            });
        });
    }
    get authEnabled() {
        return this.config.webappPassword.length > 0;
    }
    lentaAgentIds(state) {
        const subscribed = [...this.jsonlIndex.getSubscribedAgents().keys()];
        const draining = [...this.jsonlDrainingAgents.keys()];
        return new Set([
            ...subscribed,
            ...draining,
            ...(state.activeComposerId ? [state.activeComposerId] : []),
        ]);
    }
    /** Read-only view of in-memory bridge state (no writes to stores). */
    readOnlyChatSnapshot() {
        const state = this.stateManager.getState();
        const subscribed = [...this.jsonlIndex.getSubscribedAgents().entries()].map(([agentId, meta]) => ({ agentId, title: meta.title }));
        const displayCache = {};
        const domOverlay = {};
        const displayMessages = {};
        const domRaw = {};
        const lentaByAgent = {};
        for (const agentId of this.lentaAgentIds(state)) {
            const lenta = this.chatDisplay.getLentaDebug(agentId);
            lentaByAgent[agentId] = lenta;
            if (lenta.jsonlHistory.length)
                displayCache[agentId] = lenta.jsonlHistory;
            if (lenta.domOverlay.length)
                domOverlay[agentId] = lenta.domOverlay;
            if (lenta.messages.length)
                displayMessages[agentId] = lenta.messages;
            if (lenta.domRaw.length)
                domRaw[agentId] = lenta.domRaw;
        }
        return {
            at: Date.now(),
            health: { cdp: this.cdpBridge.getClient()?.isConnected() ?? false },
            cursor: {
                activeComposerId: state.activeComposerId,
                activeChatTitle: state.activeChatTitle,
                updatedAt: state.updatedAt,
                lastError: state.lastError,
                agentWorking: state.agentWorking,
                tabs: state.tabs,
                composerIdByTitle: state.composerIdByTitle,
                domMessageCount: state.messages.length,
                domMessages: state.messages,
            },
            domExtractDebug: {
                latest: this.messageDebugStore.latest(),
                ringSize: this.messageDebugStore.list().length,
            },
            subscribed,
            lentaByAgent,
            displayCache,
            domOverlay,
            displayMessages,
            domRaw,
            /** @deprecated use `domOverlay` */
            domTranscript: domOverlay,
            chatHistoryJsonl: isChatHistoryFromJsonl(),
        };
    }
    async buildLentaDebugReport(agentId, title, opts) {
        const state = this.stateManager.getState();
        const subMeta = this.jsonlIndex.getSubscribedAgents().get(agentId);
        const subTitle = title ?? subMeta?.title;
        const synced = isChatSyncedWithCursor(agentId, subTitle, state);
        if (opts?.reloadJsonl) {
            const history = await this.jsonlIndex.loadHistory(agentId, {
                title: subTitle,
                composerIdByTitle: state.composerIdByTitle,
            });
            this.chatDisplay.setJsonlBaseline(agentId, history.messages);
        }
        this.syncGeneratingFlags(state);
        if (state.messages?.length)
            this.ingestDomOverlayFromState(state);
        const lenta = this.chatDisplay.getLentaDebug(agentId);
        const snap = this.agentMessagesSnapshot(agentId);
        const issues = [
            ...checkStoreCompose(lenta.jsonlHistory, lenta.domOverlay, lenta.messages),
            ...checkPayloadCompose(snap.messages, snap.historyMessages, snap.liveMessages),
            ...checkMonotonicOrder(lenta.messages, 'store'),
            ...checkOverlayAfterArchive(lenta.jsonlHistory, lenta.domOverlay),
            ...checkOverlayDuplicatesArchive(lenta.jsonlHistory, lenta.domOverlay),
            ...checkSyncGate(synced, lenta.domOverlay, lenta.domRaw.length),
        ];
        const activeId = state.activeComposerId;
        const domViewport = activeId && activeId !== agentId
            ? []
            : state.messages.map((m) => previewRow(m, 'dom-viewport'));
        const domNote = activeId && activeId !== agentId
            ? `DOM viewport is active ${activeId.slice(0, 8)}, not ${agentId.slice(0, 8)}`
            : null;
        const skipDomTailInvariant = !synced ||
            lenta.domOverlay.length > 0 ||
            lenta.agentGenerating ||
            state.agentWorking;
        const domCmp = skipDomTailInvariant
            ? { issues: [], domTail: domViewport.slice(-4), lentaTail: [] }
            : compareDomTailToLenta(domViewport, lenta.messages.map((m) => previewRow(m, 'lenta')), 12, {
                hasDomOverlay: lenta.domOverlay.length > 0,
                hasJsonlArchive: lenta.jsonlHistory.length > 0,
                agentWorking: state.agentWorking,
                agentGenerating: lenta.agentGenerating,
            });
        issues.push(...domCmp.issues);
        return {
            at: new Date().toISOString(),
            agentId,
            synced,
            domNote,
            health: { cdp: this.cdpBridge.getClient()?.isConnected() ?? false },
            activeComposerId: activeId,
            activeChatTitle: state.activeChatTitle,
            agentWorking: state.agentWorking,
            agentGenerating: lenta.agentGenerating,
            jsonlRowCount: lenta.jsonlRowCount,
            counts: {
                jsonl: lenta.jsonlHistory.length,
                domOverlay: lenta.domOverlay.length,
                domRaw: lenta.domRaw.length,
                messages: lenta.messages.length,
                domViewport: domViewport.length,
            },
            lenta,
            emitSnapshot: snap,
            issues,
            issuesSummary: issues.map((i) => formatLentaIssue(i)),
            match: issues.length === 0,
            domTail: domCmp.domTail,
            lentaTail: domCmp.lentaTail,
            seqJournal: getLentaSeqJournal(agentId).slice(-24),
            seqLogFile: getLentaSeqLogPath(),
            currentSeq: this.historySeqByAgent.get(agentId) ?? 0,
        };
    }
    /** Push JSONL file updates to every subscribed route id (e.g. sidebar-0) for that composer. */
    syncJsonlToSubscribedAgents(fileAgentId, messages, totalMessages) {
        const state = this.stateManager.getState();
        const activeTab = state.tabs.find((t) => t.active);
        const targets = new Set([fileAgentId]);
        for (const [subId, meta] of this.jsonlIndex.getSubscribedAgents()) {
            const path = resolveJsonlFilePath(this.config.cursorProjectsDir, subId, {
                title: meta.title,
                composerIdByTitle: state.composerIdByTitle,
                activeComposerId: state.activeComposerId,
                activeTabTitle: activeTab?.title,
            });
            if (path && basename(path, '.jsonl') === fileAgentId)
                targets.add(subId);
        }
        for (const [drainId, meta] of this.jsonlDrainingAgents) {
            const path = resolveJsonlFilePath(this.config.cursorProjectsDir, drainId, {
                title: meta.title,
                composerIdByTitle: state.composerIdByTitle,
                activeComposerId: state.activeComposerId,
                activeTabTitle: activeTab?.title,
            });
            if (path && basename(path, '.jsonl') === fileAgentId)
                targets.add(drainId);
        }
        for (const agentId of targets) {
            this.emitJsonlLiveForAgent(agentId, messages, totalMessages);
        }
    }
    /** JSONL baseline + DOM tail (`liveMessages`); `messages` = append-only compose. */
    lentaSnapshot(agentId, totalMessages) {
        const historyMessages = this.chatDisplay.getJsonlHistory(agentId);
        const liveMessages = this.chatDisplay.getDomLive(agentId);
        const messages = this.chatDisplay.getDisplayMessages(agentId);
        assertMonotonicLenta(messages, agentId.slice(0, 8));
        const source = liveMessages.length ? 'hybrid' : 'jsonl';
        return {
            historyMessages,
            liveMessages,
            messages,
            totalMessages: totalMessages ?? messages.length,
            source: source,
        };
    }
    getClientTurnLimit(agentId) {
        return this.clientHistoryTurns.get(agentId) ?? AGENT_HISTORY_INITIAL_TURNS;
    }
    sliceLentaForClient(agentId, messages) {
        const page = sliceMessagesByTurns(messages, {
            turnLimit: this.getClientTurnLimit(agentId),
        });
        return page;
    }
    isClientLentaPending(agentId, storeMessages) {
        return isLentaDeliveryPending(agentId, storeMessages, this.getClientTurnLimit(agentId));
    }
    emitLentaForAgent(agentId, opts) {
        const snap = this.lentaSnapshot(agentId, opts.totalMessages);
        const historyOut = opts.historyPayload ?? snap.historyMessages;
        if (!historyOut.length && !snap.liveMessages.length)
            return;
        this.emitAgentMessages(agentId, historyOut, snap.liveMessages, snap.source, {
            totalMessages: opts.totalMessages,
            append: opts.append,
            reason: opts.reason,
            forceSeq: opts.forceSeq,
        });
    }
    lentaTailSignature(messages) {
        return messages
            .slice(-4)
            .map((m) => `${m.role}:${m.id ?? ''}:${(m.text ?? '').length}:${(m.text ?? '').slice(-48)}`)
            .join('|');
    }
    /** Live JSONL → `agent:messages` for subscribed chats (phone lenta). */
    emitJsonlLiveForAgent(agentId, rows, totalMessages) {
        const prevTotal = this.lastEmittedJsonlRows.get(agentId) ?? 0;
        const prevSig = this.lastEmittedLentaSig.get(agentId);
        const state = this.stateManager.getState();
        this.chatDisplay.setAgentGenerating(agentId, isAgentGenerating(agentId, state));
        this.chatDisplay.setJsonlBaseline(agentId, rows);
        this.lastEmittedJsonlRows.set(agentId, totalMessages);
        this.writeLentaCapture(agentId, 'jsonl_file');
        const historyMessages = this.chatDisplay.getJsonlHistory(agentId);
        const liveN = this.chatDisplay.getDomLive(agentId).length;
        if (!historyMessages.length && !liveN)
            return;
        if (!this.jsonlIndex.getSubscribedAgents().has(agentId))
            return;
        const storeMessages = this.chatDisplay.getDisplayMessages(agentId);
        const deliveryPending = this.isClientLentaPending(agentId, storeMessages);
        const sig = this.lentaTailSignature(historyMessages);
        const totalGrew = totalMessages > prevTotal;
        const generating = isAgentGenerating(agentId, state);
        if (!deliveryPending && !generating && !totalGrew && sig === prevSig && prevSig)
            return;
        const histLen = historyMessages.length;
        const prevHistLen = this.lastEmittedHistLen.get(agentId) ?? 0;
        if (prevHistLen > 24 && histLen < prevHistLen - 12) {
            bridgePipelineLog({
                dir: 'internal',
                event: 'agent:messages:SKIP_REGRESSION',
                agentId,
                msgs: histLen,
                detail: `prev=${prevHistLen} total=${totalMessages}`,
            });
            return;
        }
        this.emitLentaForAgent(agentId, {
            totalMessages,
            reason: 'jsonl_live',
            forceSeq: deliveryPending || generating,
        });
        this.lastEmittedLentaSig.set(agentId, sig);
        this.lastEmittedHistLen.set(agentId, Math.max(prevHistLen, histLen));
        if (this.isClientLentaPending(agentId, this.chatDisplay.getDisplayMessages(agentId))) {
            this.flushLentaDelivery(agentId, 'delivery_flush');
        }
        this.tryFinishJsonlDrain(agentId);
        this.agentCompletionPush?.retryPendingContent(state);
    }
    /** Ingest CDP viewport into ChatDisplayStore — no debounce (debug + compose). */
    ingestDomOverlayFromState(state) {
        if (state.activeComposerId && state.todos?.length) {
            this.chatDisplay.setLiveTodos(state.activeComposerId, state.todos);
        }
        if (!state.messages?.length)
            return;
        const domRows = this.prepareStateMessages(state.messages);
        const targets = this.resolveDomOverlayAgentIds(state);
        if (!targets.size)
            return;
        for (const agentId of targets) {
            this.chatDisplay.setAgentGenerating(agentId, isAgentGenerating(agentId, state));
            if (!this.chatDisplay.getJsonlHistory(agentId).length) {
                void this.ensureJsonlBaselineForAgent(agentId, state);
            }
            const prevLive = this.chatDisplay.getDomLive(agentId).length;
            this.chatDisplay.mergeLiveForAgent(agentId, domRows);
            const liveN = this.chatDisplay.getDomLive(agentId).length;
            if (liveN > 0 && liveN !== prevLive) {
                bridgePipelineLog({
                    dir: 'internal',
                    event: 'dom:overlay:ingest',
                    agentId,
                    msgs: liveN,
                    detail: `domRows=${domRows.length} generating=${isAgentGenerating(agentId, state)} viewport=${domRows.length}`,
                });
            }
            this.writeLentaCapture(agentId, 'dom_ingest');
            this.tryFinishJsonlDrain(agentId);
        }
    }
    /** Push lenta with DOM live to clients while generation is in flight. */
    emitDomLiveWhileGenerating(state) {
        for (const agentId of this.resolveDomOverlayAgentIds(state)) {
            if (!isAgentGenerating(agentId, state))
                continue;
            if (!this.jsonlIndex.getSubscribedAgents().has(agentId))
                continue;
            const meta = this.jsonlIndex.getSubscribedAgents().get(agentId);
            if (!meta || !isChatSyncedWithCursor(agentId, meta.title, state))
                continue;
            const snap = this.lentaSnapshot(agentId, this.lastEmittedJsonlRows.get(agentId) ?? undefined);
            if (!snap.liveMessages.length)
                continue;
            this.lastEmittedLentaSig.delete(agentId);
            this.emitLentaForAgent(agentId, {
                totalMessages: snap.totalMessages,
                reason: 'dom_overlay',
                forceSeq: true,
            });
        }
    }
    syncGeneratingFlags(state) {
        const targets = this.resolveDomOverlayAgentIds(state);
        for (const agentId of targets) {
            this.chatDisplay.setAgentGenerating(agentId, isAgentGenerating(agentId, state));
        }
    }
    /** Push composed lenta to subscribed clients (debounced). */
    emitDomOverlaySocketForSubscribers(state) {
        for (const [subId, meta] of this.jsonlIndex.getSubscribedAgents()) {
            if (!isChatSyncedWithCursor(subId, meta.title, state))
                continue;
            const snap = this.lentaSnapshot(subId, this.lastEmittedJsonlRows.get(subId) ?? undefined);
            const deliveryPending = this.isClientLentaPending(subId, snap.messages);
            if (!snap.liveMessages.length && !state.agentWorking && !deliveryPending)
                continue;
            this.lastEmittedLentaSig.delete(subId);
            this.emitLentaForAgent(subId, {
                totalMessages: snap.totalMessages,
                reason: 'dom_overlay',
                forceSeq: deliveryPending,
            });
        }
    }
    /** While generating, push overlay growth without waiting for emit debounce tail. */
    maybeEmitDomOverlayLeading(state) {
        if (!state.agentWorking)
            return;
        for (const [subId, meta] of this.jsonlIndex.getSubscribedAgents()) {
            if (!isChatSyncedWithCursor(subId, meta.title, state))
                continue;
            if (!this.chatDisplay.getDomLive(subId).length)
                continue;
            const snap = this.lentaSnapshot(subId, this.lastEmittedJsonlRows.get(subId) ?? undefined);
            const sig = messagesFingerprint(snap.messages);
            const last = getLastLentaEmit(subId);
            if (last?.messagesSig === sig)
                continue;
            this.emitLentaForAgent(subId, {
                totalMessages: snap.totalMessages,
                reason: 'dom_overlay',
            });
        }
    }
    applyDomOverlayFromState(state) {
        this.syncGeneratingFlags(state);
        this.ingestDomOverlayFromState(state);
        this.emitDomLiveWhileGenerating(state);
        this.maybeEmitDomOverlayLeading(state);
        this.scheduleDomOverlayEmit();
    }
    scheduleDomOverlayEmit() {
        if (this.domOverlayTimer)
            clearTimeout(this.domOverlayTimer);
        this.domOverlayTimer = setTimeout(() => {
            this.domOverlayTimer = null;
            this.emitDomOverlaySocketForSubscribers(this.stateManager.getState());
        }, Relay.DOM_OVERLAY_EMIT_MS);
    }
    /**
     * Agent ids that receive **live CDP viewport** ingest (active composer + synced phone subscribe).
     * Draining agents (after `agents:unsubscribe`) are excluded: CDP shows whichever chat is open in
     * Cursor, not the phone-left chat — drain merges JSONL into the frozen `latestDomViewport` snapshot.
     */
    resolveDomOverlayAgentIds(state) {
        const targets = new Set();
        const activeTab = state.tabs.find((t) => t.active);
        const activeId = resolveCursorActiveComposerId(state);
        if (isComposerUuid(activeId))
            targets.add(activeId);
        for (const [subId, meta] of this.jsonlIndex.getSubscribedAgents()) {
            if (!isChatSyncedWithCursor(subId, meta.title, state))
                continue;
            targets.add(subId);
            if (isComposerUuid(subId))
                continue;
            const path = resolveJsonlFilePath(this.config.cursorProjectsDir, subId, {
                title: meta.title,
                composerIdByTitle: state.composerIdByTitle,
                activeComposerId: state.activeComposerId,
                activeTabTitle: activeTab?.title,
            });
            if (path)
                targets.add(basename(path, '.jsonl'));
        }
        return targets;
    }
    startJsonlDrain(agentId, title) {
        const prev = this.jsonlDrainingAgents.get(agentId);
        this.jsonlDrainingAgents.set(agentId, {
            title: title ?? prev?.title,
            since: prev?.since ?? Date.now(),
        });
        this.jsonlIndex.drain(agentId, title ?? prev?.title);
        bridgePipelineLog({
            dir: 'internal',
            event: 'jsonl:drain:start',
            agentId,
            msgs: this.chatDisplay.getDomLive(agentId).length,
            detail: `frozen_dom+jsonl_watch ${title?.slice(0, 32) ?? ''}`,
        });
    }
    cancelJsonlDrain(agentId) {
        if (!this.jsonlDrainingAgents.delete(agentId))
            return;
        this.jsonlIndex.stopDrain(agentId);
    }
    tryFinishJsonlDrain(agentId) {
        const meta = this.jsonlDrainingAgents.get(agentId);
        if (!meta)
            return;
        const state = this.stateManager.getState();
        const overlayN = this.chatDisplay.getDomLive(agentId).length;
        // Sidebar/tab state — not CDP viewport (draining chat may be unfocused in Cursor).
        const generating = isAgentGenerating(agentId, state);
        const timedOut = Date.now() - meta.since >= Relay.JSONL_DRAIN_MAX_MS;
        if ((overlayN > 0 || generating) && !timedOut)
            return;
        if (timedOut && (overlayN > 0 || generating)) {
            bridgePipelineLog({
                dir: 'internal',
                event: 'jsonl:drain:timeout',
                agentId,
                msgs: overlayN,
                detail: `generating=${generating}`,
            });
        }
        this.finishJsonlDrain(agentId);
    }
    finishJsonlDrain(agentId) {
        const wasDraining = this.jsonlDrainingAgents.delete(agentId);
        if (wasDraining)
            this.jsonlIndex.stopDrain(agentId);
        this.chatDisplay.clearAgent(agentId);
        this.lastEmittedJsonlRows.delete(agentId);
        this.lastEmittedLentaSig.delete(agentId);
        this.lastEmittedHistLen.delete(agentId);
        this.lentaPendingSince.delete(agentId);
        if (wasDraining) {
            bridgePipelineLog({
                dir: 'internal',
                event: 'jsonl:drain:done',
                agentId,
            });
        }
    }
    writeLentaCapture(agentId, reason, extra) {
        const state = this.stateManager.getState();
        const lenta = this.chatDisplay.getLentaDebug(agentId);
        const activeTab = state.tabs.find((t) => t.active);
        const jsonlPath = resolveJsonlFilePath(this.config.cursorProjectsDir, agentId, {
            title: this.jsonlIndex.getSubscribedAgents().get(agentId)?.title,
            composerIdByTitle: state.composerIdByTitle,
            activeComposerId: state.activeComposerId,
            activeTabTitle: activeTab?.title,
        });
        const domViewport = resolveCursorActiveComposerId(state) === agentId
            ? this.prepareStateMessages(state.messages)
            : [];
        captureLentaIfEnabled(agentId, {
            reason,
            source: lenta.source,
            emitSeq: extra?.emitSeq,
            seqHeld: extra?.seqHeld,
            transition: extra?.transition,
            jsonlRowCount: lenta.jsonlRowCount,
            historyMessages: lenta.jsonlHistory,
            liveMessages: lenta.domOverlay,
            messages: lenta.messages,
            domRaw: lenta.domRaw,
            domViewport,
            jsonlFilePath: jsonlPath ?? undefined,
            activeComposerId: state.activeComposerId,
            agentWorking: state.agentWorking,
        });
    }
    resolveHistoryOpts(agentId) {
        const state = this.stateManager.getState();
        const activeTab = state.tabs.find((t) => t.active);
        return {
            title: this.jsonlIndex.getSubscribedAgents().get(agentId)?.title,
            composerIdByTitle: state.composerIdByTitle,
            activeComposerId: state.activeComposerId,
            activeTabTitle: activeTab?.title,
        };
    }
    agentMessagesSnapshot(agentId, totalMessages) {
        return this.lentaSnapshot(agentId, totalMessages);
    }
    checkMediaAuth(req) {
        if (!this.authEnabled)
            return true;
        const token = (typeof req.query.token === 'string' ? req.query.token : undefined) ||
            (typeof req.headers.authorization === 'string'
                ? req.headers.authorization.replace(/^Bearer\s+/i, '')
                : undefined);
        return Boolean(token && this.tokens.has(token));
    }
    setupHttp() {
        this.app.get('/health', (req, res) => {
            const state = this.stateManager.getState();
            const idleMs = parseIdleMsQuery(req.query.idleMs);
            res.json({
                ok: true,
                cdp: this.cdpBridge.getClient()?.isConnected() ?? false,
                keepAwake: isKeepAwakeActive(),
                activity: activityTracker.getStatus(state, idleMs),
                debugChats: '/debug/chats',
                debugSnapshot: '/debug/chat-snapshot',
                debugLenta: '/debug/lenta?agentId=',
                debugLentaSeq: '/debug/lenta-seq?agentId=',
                debugJsonlLive: '/debug/jsonl-live',
            });
        });
        this.app.get('/debug/messages', (_req, res) => {
            const state = this.stateManager.getState();
            res.json({
                latest: this.messageDebugStore.latest(),
                history: this.messageDebugStore.list(),
                activeComposerId: state.activeComposerId,
                domMessageCount: state.messages.length,
                lastError: state.lastError,
            });
        });
        this.app.get('/debug/chats', (_req, res) => {
            res.type('html').send(DEBUG_CHATS_PAGE_HTML);
        });
        this.app.get('/debug/chat-snapshot', (_req, res) => {
            res.json(this.readOnlyChatSnapshot());
        });
        this.app.get('/debug/lenta', async (req, res) => {
            const agentId = typeof req.query.agentId === 'string' ? req.query.agentId.trim() : '';
            if (!agentId) {
                res.status(400).json({ error: 'agentId required' });
                return;
            }
            const title = typeof req.query.title === 'string' ? req.query.title.trim() : undefined;
            const reloadJsonl = req.query.reloadJsonl === '1' || req.query.reloadJsonl === 'true';
            try {
                const report = await this.buildLentaDebugReport(agentId, title, { reloadJsonl });
                res.json(report);
            }
            catch (err) {
                res.status(500).json({ error: err.message });
            }
        });
        this.app.post('/debug/lenta-capture/start', express.json(), (req, res) => {
            const body = (req.body ?? {});
            const agentId = String(body.agentId ?? (typeof req.query.agentId === 'string' ? req.query.agentId : '') ?? '').trim();
            if (!agentId) {
                res.status(400).json({ error: 'agentId required' });
                return;
            }
            writeCaptureConfig({
                enabled: true,
                agentId,
                title: body?.title?.trim(),
            });
            mkdirSync(captureSessionDir(agentId), { recursive: true });
            res.json({
                ok: true,
                agentId,
                sessionDir: captureSessionDir(agentId),
                hint: 'Reproduce chat activity; snapshots append under sessionDir',
            });
        });
        this.app.post('/debug/lenta-capture/stop', (_req, res) => {
            const cfg = readCaptureConfig();
            clearCaptureConfig();
            res.json({ ok: true, stopped: cfg?.agentId ?? null });
        });
        this.app.get('/debug/lenta-capture/status', (_req, res) => {
            const cfg = readCaptureConfig();
            res.json({
                config: cfg,
                sessionDir: cfg ? captureSessionDir(cfg.agentId) : null,
                steps: cfg ? listCaptureSteps(cfg.agentId) : [],
            });
        });
        this.app.get('/debug/lenta-seq', (req, res) => {
            const agentId = typeof req.query.agentId === 'string' ? req.query.agentId.trim() : undefined;
            const journal = getLentaSeqJournal(agentId || undefined);
            res.json({
                at: new Date().toISOString(),
                agentId: agentId ?? null,
                logFile: getLentaSeqLogPath(),
                currentSeq: agentId ? this.historySeqByAgent.get(agentId) ?? 0 : undefined,
                journal,
                handoffs: journal.filter((e) => e.transition === 'dom_to_jsonl'),
            });
        });
        this.app.get('/debug/jsonl-live', async (req, res) => {
            if (!this.checkMediaAuth(req)) {
                res.status(401).json({ error: 'Unauthorized' });
                return;
            }
            const agentId = String(req.query.agentId ?? '').trim();
            if (!agentId) {
                res.status(400).json({ error: 'agentId required' });
                return;
            }
            const title = typeof req.query.title === 'string' ? req.query.title.trim() : undefined;
            const afterRaw = Number(req.query.afterLine ?? 0);
            const tailRaw = Number(req.query.tail ?? 0);
            const afterLine = Number.isFinite(afterRaw) && afterRaw > 0 ? Math.floor(afterRaw) : 0;
            const tail = afterLine > 0
                ? 0
                : Number.isFinite(tailRaw) && tailRaw > 0
                    ? Math.min(500, Math.floor(tailRaw))
                    : 80;
            const state = this.stateManager.getState();
            const activeTab = state.tabs.find((t) => t.active);
            const filePath = resolveJsonlFilePath(this.config.cursorProjectsDir, agentId, {
                title,
                composerIdByTitle: state.composerIdByTitle,
                activeComposerId: state.activeComposerId,
                activeTabTitle: activeTab?.title,
            });
            if (!filePath) {
                res.json({
                    agentId,
                    filePath: null,
                    fileSize: 0,
                    totalLines: 0,
                    updatedAt: Date.now(),
                    rows: [],
                });
                return;
            }
            try {
                const snap = await readJsonlLiveSnapshot(filePath, agentId, {
                    afterLine,
                    tail,
                    maxNew: 96,
                });
                res.json(snap);
            }
            catch (err) {
                res.status(500).json({ error: err.message });
            }
        });
        this.app.get('/api/agents/index', async (req, res) => {
            if (!this.checkMediaAuth(req)) {
                res.status(401).json({ error: 'Unauthorized' });
                return;
            }
            try {
                const stale = Date.now() - (this.lastJsonlIndex.updatedAt ?? 0) > 60_000;
                const index = this.lastJsonlIndex.repos.length && !stale
                    ? this.lastJsonlIndex
                    : await this.jsonlIndex.rebuild({ broadcast: false });
                this.lastJsonlIndex = index;
                const merged = mergeSidebarWithJsonl(this.stateManager.getState().sidebarRepos, index, this.stateManager.getState().composerIdByTitle);
                const bytes = JSON.stringify(merged).length;
                bridgePipelineLog({
                    dir: 'out',
                    event: 'http:agents:index',
                    bytes,
                    detail: `repos=${merged.repos?.length ?? 0}`,
                });
                res.json(merged);
            }
            catch (err) {
                res.status(500).json({ error: err.message });
            }
        });
        this.app.get('/api/agents/history', async (req, res) => {
            if (!this.checkMediaAuth(req)) {
                res.status(401).json({ error: 'Unauthorized' });
                return;
            }
            const agentId = String(req.query.agentId ?? '').trim();
            const title = typeof req.query.title === 'string' ? req.query.title.trim() : undefined;
            const limitRaw = Number(req.query.limit ?? 0);
            const minRaw = Number(req.query.minMessages ?? 0);
            const turnLimitRaw = Number(req.query.turnLimit ?? 0);
            const turnLimit = Number.isFinite(turnLimitRaw) && turnLimitRaw > 0
                ? Math.floor(turnLimitRaw)
                : undefined;
            const limit = resolveHistoryLimit({
                limit: Number.isFinite(limitRaw) && limitRaw > 0 ? limitRaw : undefined,
                minMessages: Number.isFinite(minRaw) && minRaw > 0 ? minRaw : undefined,
            });
            const offsetRaw = Number(req.query.offset ?? 0);
            const offset = Number.isFinite(offsetRaw) && offsetRaw > 0 ? offsetRaw : undefined;
            const requestId = typeof req.query.requestId === 'string' ? req.query.requestId.trim() : undefined;
            if (!agentId) {
                res.status(400).json({ error: 'agentId required' });
                return;
            }
            bridgePipelineLog({
                dir: 'in',
                event: 'http:agents:history',
                requestId,
                agentId,
                detail: `limit=${limit ?? 'all'} turnLimit=${turnLimit ?? '-'} title=${title?.slice(0, 32) ?? '-'}`,
            });
            try {
                const full = await this.jsonlIndex.loadHistory(agentId, {
                    title,
                    composerIdByTitle: this.stateManager.getState().composerIdByTitle,
                    offset,
                });
                this.chatDisplay.setJsonlBaseline(agentId, full.messages);
                if (turnLimit) {
                    this.clientHistoryTurns.set(agentId, Math.max(this.getClientTurnLimit(agentId), turnLimit));
                }
                else {
                    this.emitJsonlLiveForAgent(agentId, full.messages, full.totalMessages);
                }
                const snap = this.agentMessagesSnapshot(agentId, full.totalMessages);
                let outMessages = snap.messages;
                let totalTurns;
                let hasMoreOlder;
                if (turnLimit) {
                    const page = sliceMessagesByTurns(snap.messages, { turnLimit });
                    outMessages = page.messages;
                    totalTurns = page.totalTurns;
                    hasMoreOlder = page.hasMoreOlder;
                }
                else if (limit > 0 && snap.messages.length > limit) {
                    outMessages = snap.messages.slice(-limit);
                }
                if (!isChatHistoryFromJsonl()) {
                    res.json({
                        agentId,
                        ...snap,
                        messages: outMessages,
                        totalMessages: full.totalMessages,
                        totalTurns,
                        hasMoreOlder,
                        requestId,
                        updatedAt: Date.now(),
                    });
                    return;
                }
                const body = {
                    agentId,
                    ...snap,
                    messages: outMessages,
                    totalMessages: full.totalMessages,
                    totalTurns,
                    hasMoreOlder,
                    requestId,
                    updatedAt: Date.now(),
                };
                const bytes = JSON.stringify(body).length;
                bridgePipelineLog({
                    dir: 'out',
                    event: 'http:agents:history',
                    requestId,
                    agentId,
                    bytes,
                    msgs: outMessages.length,
                    detail: `total=${full.totalMessages ?? '?'} turns=${totalTurns ?? '?'}`,
                });
                res.json(body);
            }
            catch (err) {
                bridgePipelineLog({
                    dir: 'internal',
                    event: 'http:agents:history:ERR',
                    requestId,
                    agentId,
                    detail: err.message,
                });
                res.status(500).json({ error: err.message });
            }
        });
        this.app.get('/api/debug/history-pipeline', (_req, res) => {
            res.json({
                ok: true,
                at: Date.now(),
                lines: bridgePipelineReportLines(),
                entries: bridgePipelineSnapshot(),
            });
        });
        this.app.get('/media/file', (req, res) => {
            if (!this.checkMediaAuth(req)) {
                res.status(401).json({ error: 'Unauthorized' });
                return;
            }
            const filePath = resolveMediaPathParam(String(req.query.path ?? ''));
            if (!filePath) {
                res.status(400).json({ error: 'path required' });
                return;
            }
            const file = readAllowedMediaFile(filePath);
            if (!file) {
                res.status(404).json({ error: 'Not found' });
                return;
            }
            res.setHeader('Content-Type', file.contentType);
            res.setHeader('Cache-Control', 'private, max-age=3600');
            res.send(file.data);
        });
        this.app.post('/api/upload-image', express.json({ limit: '20mb' }), async (req, res) => {
            if (!this.checkMediaAuth(req)) {
                res.status(401).json({ error: 'Unauthorized' });
                return;
            }
            const imageB64 = String(req.body?.imageBase64 ?? '');
            const mimeType = String(req.body?.mimeType ?? 'image/jpeg');
            if (!imageB64) {
                res.status(400).json({ error: 'imageBase64 required' });
                return;
            }
            let buffer;
            try {
                buffer = Buffer.from(imageB64, 'base64');
            }
            catch {
                res.status(400).json({ error: 'Invalid imageBase64' });
                return;
            }
            if (buffer.length < 32) {
                res.status(400).json({ error: 'Image too small' });
                return;
            }
            if (buffer.length > 10 * 1024 * 1024) {
                res.status(413).json({ error: 'Image too large (max 10 MB)' });
                return;
            }
            if (!extensionForMime(mimeType)) {
                res.status(400).json({ error: 'Unsupported image type' });
                return;
            }
            try {
                const path = await saveUploadedImage(buffer, mimeType);
                res.json({ path });
            }
            catch (err) {
                res.status(500).json({ error: err.message });
            }
        });
        this.app.post('/api/transcribe', express.json({ limit: '20mb' }), async (req, res) => {
            if (!this.checkMediaAuth(req)) {
                res.status(401).json({ error: 'Unauthorized' });
                return;
            }
            const apiKey = this.config.openaiApiKey;
            if (!apiKey) {
                res.status(503).json({
                    error: 'Голосовой ввод недоступен: задайте OPENAI_API_KEY в bridge/.env (локальный режим без relay)',
                });
                return;
            }
            const audioB64 = String(req.body?.audioBase64 ?? '');
            const mimeType = String(req.body?.mimeType ?? 'audio/m4a');
            if (!audioB64) {
                res.status(400).json({ error: 'audioBase64 required' });
                return;
            }
            let audio;
            try {
                audio = Buffer.from(audioB64, 'base64');
            }
            catch {
                res.status(400).json({ error: 'Invalid audioBase64' });
                return;
            }
            if (audio.length < 32) {
                res.status(400).json({ error: 'Recording too short' });
                return;
            }
            if (audio.length > 10 * 1024 * 1024) {
                res.status(413).json({ error: 'Recording too large (max 10 MB)' });
                return;
            }
            try {
                const text = await transcribeAudioBuffer(apiKey, audio, mimeType, this.config.openaiTranscribeModel);
                res.json({ text });
            }
            catch (err) {
                console.warn('[relay] transcribe:', err.message);
                res.status(502).json({ error: err.message });
            }
        });
        this.app.post('/api/login', express.json(), (req, res) => {
            if (!this.authEnabled) {
                res.json({ token: 'open' });
                return;
            }
            const pw = String(req.body?.password ?? '');
            const expected = Buffer.from(this.config.webappPassword);
            const got = Buffer.from(pw);
            if (expected.length !== got.length || !timingSafeEqual(expected, got)) {
                res.status(401).json({ error: 'Invalid password' });
                return;
            }
            const token = randomBytes(24).toString('hex');
            this.tokens.add(token);
            res.json({ token });
        });
    }
    setupSocket() {
        this.io.use((socket, next) => {
            if (!this.authEnabled)
                return next();
            const token = socket.handshake.auth?.token;
            if (token && this.tokens.has(token))
                return next();
            return next(new Error('Unauthorized'));
        });
        this.io.on('connection', (socket) => this.onConnect(socket));
    }
    broadcast(event, payload) {
        if (event === 'agent:messages') {
            const p = payload;
            bridgePipelineLog({
                dir: 'out',
                event: 'broadcast:agent:messages',
                agentId: p?.agentId,
                msgs: p?.historyMessages?.length ?? 0,
                detail: `live=${p?.liveMessages?.length ?? 0} append=${!!p?.append} seq=${p?.seq ?? '-'} upstream=${this.upstream?.connected ?? false}`,
            });
        }
        if (event === 'agents:history' || event === 'agent:history') {
            const h = payload;
            const bytes = JSON.stringify(payload ?? {}).length;
            bridgePipelineLog({
                dir: 'out',
                event: `broadcast:${event}`,
                requestId: h?.requestId,
                agentId: h?.agentId,
                bytes,
                msgs: h?.messages?.length ?? 0,
                detail: `upstream=${this.upstream?.connected ?? false}`,
            });
        }
        this.io.emit(event, payload);
        this.upstream?.emit(event, payload);
    }
    historySeqByAgent = new Map();
    nextHistorySeq(agentId) {
        const n = (this.historySeqByAgent.get(agentId) ?? 0) + 1;
        this.historySeqByAgent.set(agentId, n);
        return n;
    }
    emitAgentMessages(agentId, historyMessages, liveMessages, source, opts) {
        const display = this.chatDisplay.getDisplayMessages(agentId);
        const page = this.sliceLentaForClient(agentId, display);
        const messages = page.messages;
        const sig = messagesFingerprint(messages);
        const prevSeq = this.historySeqByAgent.get(agentId) ?? 0;
        const prevHeld = getLastLentaEmit(agentId);
        let outSeq;
        let seqHeld = false;
        if (opts?.forceSeq) {
            outSeq = this.nextHistorySeq(agentId);
        }
        else if (prevHeld && prevHeld.messagesSig === sig) {
            outSeq = prevHeld.seq;
            seqHeld = true;
            this.historySeqByAgent.set(agentId, outSeq);
        }
        else {
            outSeq = this.nextHistorySeq(agentId);
        }
        const entry = recordLentaSeqEmit(agentId, outSeq, prevSeq, seqHeld, messages, {
            reason: opts?.reason ?? 'emit',
            source,
            historyLen: historyMessages.length,
            liveLen: liveMessages.length,
            messagesLen: messages.length,
            append: opts?.append,
        });
        bridgePipelineLog({
            dir: 'out',
            event: 'lenta:seq',
            agentId,
            msgs: messages.length,
            detail: `seq=${outSeq} prev=${prevSeq} held=${seqHeld ? 1 : 0} ` +
                `reason=${entry.reason} transition=${entry.transition} ` +
                `hist=${entry.historyLen} live=${entry.liveLen} src=${source}`,
        });
        this.broadcast('agent:messages', {
            agentId,
            historyMessages,
            liveMessages,
            messages,
            totalMessages: opts?.totalMessages ?? display.length,
            totalTurns: page.totalTurns,
            hasMoreOlder: page.hasMoreOlder,
            source,
            updatedAt: Date.now(),
            seq: outSeq,
            append: opts?.append || undefined,
        });
        this.writeLentaCapture(agentId, 'emit_agent_messages', {
            emitSeq: outSeq,
            seqHeld,
            transition: entry.transition,
        });
        this.noteLentaDelivery(agentId, messages);
        this.refreshLentaPendingPatch();
    }
    prepareStateMessages(raw) {
        return filterClientDisplayList(prepareChatMessagesForDisplay(raw));
    }
    withDisplayState(payload) {
        if (!payload.messages?.length)
            return payload;
        return {
            ...payload,
            messages: this.prepareStateMessages(payload.messages),
        };
    }
    buildLentaPendingByAgent() {
        const state = this.stateManager.getState();
        const out = {};
        for (const [agentId, meta] of this.jsonlIndex.getSubscribedAgents()) {
            if (!isChatSyncedWithCursor(agentId, meta.title, state))
                continue;
            const messages = this.chatDisplay.getDisplayMessages(agentId);
            if (this.isClientLentaPending(agentId, messages))
                out[agentId] = true;
        }
        return out;
    }
    refreshLentaPendingCache() {
        this.cachedLentaPendingByAgent = this.buildLentaPendingByAgent();
    }
    withRelayState(payload) {
        const base = this.withDisplayState(payload);
        if (payload.messages?.length) {
            this.refreshLentaPendingCache();
        }
        return {
            ...base,
            lentaPendingByAgent: this.cachedLentaPendingByAgent,
            updatedAt: base.updatedAt ?? Date.now(),
        };
    }
    refreshLentaPendingPatch() {
        this.refreshLentaPendingCache();
        this.broadcast('state:patch', {
            lentaPendingByAgent: this.cachedLentaPendingByAgent,
            updatedAt: Date.now(),
        });
    }
    noteLentaDelivery(agentId, storeMessages) {
        if (this.isClientLentaPending(agentId, storeMessages)) {
            if (!this.lentaPendingSince.has(agentId)) {
                this.lentaPendingSince.set(agentId, Date.now());
            }
            return;
        }
        this.lentaPendingSince.delete(agentId);
        this.agentCompletionPush?.retryPendingContent(this.stateManager.getState());
    }
    /** Push `agent:messages` when store is ahead of last emit (subscribed + synced). */
    flushLentaDelivery(agentId, reason) {
        if (!this.jsonlIndex.getSubscribedAgents().has(agentId))
            return false;
        const state = this.stateManager.getState();
        const meta = this.jsonlIndex.getSubscribedAgents().get(agentId);
        if (!meta || !isChatSyncedWithCursor(agentId, meta.title, state))
            return false;
        const messages = this.chatDisplay.getDisplayMessages(agentId);
        if (!this.isClientLentaPending(agentId, messages)) {
            this.lentaPendingSince.delete(agentId);
            return false;
        }
        const since = this.lentaPendingSince.get(agentId) ?? Date.now();
        if (!this.lentaPendingSince.has(agentId))
            this.lentaPendingSince.set(agentId, since);
        bridgePipelineLog({
            dir: 'internal',
            event: 'lenta:delivery:FLUSH',
            agentId,
            msgs: messages.length,
            detail: `reason=${reason} ageMs=${Date.now() - since}`,
        });
        this.emitLentaForAgent(agentId, {
            totalMessages: this.lastEmittedJsonlRows.get(agentId) ?? messages.length,
            reason,
            forceSeq: true,
        });
        this.agentCompletionPush?.retryPendingContent(this.stateManager.getState());
        return true;
    }
    flushPendingLentaForSubscribers(reason) {
        for (const agentId of this.jsonlIndex.getSubscribedAgents().keys()) {
            this.flushLentaDelivery(agentId, reason);
        }
        this.refreshLentaPendingPatch();
    }
    maybeForceStaleLentaDelivery() {
        const now = Date.now();
        for (const agentId of this.jsonlIndex.getSubscribedAgents().keys()) {
            const since = this.lentaPendingSince.get(agentId);
            if (since == null)
                continue;
            if (now - since < Relay.LENTA_PENDING_FORCE_MS)
                continue;
            this.flushLentaDelivery(agentId, 'delivery_flush');
        }
    }
    wireEvents() {
        this.stateManager.on('state:full', (state) => {
            this.refreshLentaPendingCache();
            this.broadcast('state:full', this.withRelayState(state));
            this.emitAgentsIndex();
            this.observeAgentCompletionForPush();
            if (state.messages?.length)
                this.applyDomOverlayFromState(state);
            if (!state.agentWorking)
                this.flushPendingLentaForSubscribers('dom_overlay');
            this.maybeForceStaleLentaDelivery();
        });
        this.stateManager.on('state:patch', (patch) => {
            this.broadcast('state:patch', this.withRelayState(patch));
            this.observeAgentCompletionForPush();
            const state = this.stateManager.getState();
            if (patch.messages?.length || state.messages?.length) {
                this.applyDomOverlayFromState(state);
            }
            if (patch.agentWorking === false || (!state.agentWorking && patch.agentWorking !== true)) {
                this.syncGeneratingFlags(state);
                this.flushPendingLentaForSubscribers('jsonl_live');
                this.refreshLentaPendingCache();
            }
            else if (state.agentWorking) {
                this.syncGeneratingFlags(state);
            }
            this.maybeForceStaleLentaDelivery();
            if (patch.sidebarRepos ||
                patch.composerIdByTitle ||
                patch.agentWorking !== undefined ||
                patch.tabs !== undefined) {
                const key = JSON.stringify([
                    patch.sidebarRepos?.map((r) => [
                        r.id,
                        r.agents.map((a) => [
                            a.id,
                            a.isWorking,
                            a.hasUnread,
                            a.needsAttention,
                        ]),
                    ]),
                    patch.composerIdByTitle,
                    patch.agentWorking,
                    patch.tabs?.map((t) => [
                        t.id,
                        t.isWorking,
                        t.hasUnread,
                        t.needsAttention,
                    ]),
                ]);
                if (key !== this.lastSidebarIndexKey) {
                    this.lastSidebarIndexKey = key;
                    this.emitAgentsIndex();
                }
            }
        });
        this.jsonlIndex.on('agents:index', (index) => {
            const key = JSON.stringify(index.repos?.map((r) => [r.id, r.agents.length]));
            if (key === this.lastJsonlIndexKey)
                return;
            this.lastJsonlIndexKey = key;
            this.lastJsonlIndex = index;
            this.emitAgentsIndex();
        });
        this.jsonlIndex.on('agent:jsonl:updated', (payload) => {
            const total = payload.totalMessages ?? payload.messages.length;
            this.syncJsonlToSubscribedAgents(payload.agentId, payload.messages, total);
            this.writeLentaCapture(payload.agentId, 'jsonl_file');
        });
        this.jsonlIndex.on('agent:history', (history) => {
            const total = history.totalMessages ?? history.messages.length;
            this.emitJsonlLiveForAgent(history.agentId, history.messages, total);
            if (isChatHistoryFromJsonl()) {
                const snap = this.agentMessagesSnapshot(history.agentId, total);
                this.broadcast('agent:history', {
                    ...history,
                    historyMessages: snap.historyMessages,
                    liveMessages: snap.liveMessages,
                    messages: snap.messages,
                    seq: this.historySeqByAgent.get(history.agentId),
                    totalMessages: total,
                });
            }
        });
    }
    emitAgentsIndex(force = false) {
        const send = () => {
            const merged = mergeSidebarWithJsonl(this.stateManager.getState().sidebarRepos, this.lastJsonlIndex, this.stateManager.getState().composerIdByTitle);
            this.broadcast('agents:index', merged);
            this.lastIndexBroadcastAt = Date.now();
        };
        if (force) {
            if (this.indexEmitTimer)
                clearTimeout(this.indexEmitTimer);
            this.indexEmitTimer = null;
            send();
            return;
        }
        const elapsed = Date.now() - this.lastIndexBroadcastAt;
        if (elapsed >= 60_000) {
            send();
            return;
        }
        if (this.indexEmitTimer)
            return;
        this.indexEmitTimer = setTimeout(() => {
            this.indexEmitTimer = null;
            send();
        }, 60_000 - elapsed);
    }
    refreshAgentsIndex(force = false) {
        void this.jsonlIndex.rebuild({ broadcast: false }).then((index) => {
            this.lastJsonlIndex = index;
            this.emitAgentsIndex(force);
        });
    }
    pushFullStateToRemote() {
        this.broadcast('state:full', this.withRelayState(this.stateManager.getState()));
        if (this.lastJsonlIndex.updatedAt > 0) {
            this.emitAgentsIndex(true);
            return;
        }
        this.refreshAgentsIndex(true);
    }
    enqueueCommand(payload, reply) {
        this.commandChain = this.commandChain
            .then(() => this.runCommand(payload, reply))
            .catch((err) => {
            console.warn(`[relay] command ${payload.type} id=${payload.id} chain err:`, err.message);
            reply('command:result', {
                id: payload.id,
                ok: false,
                error: err.message,
            });
        });
    }
    enqueueSubscribe(work, label) {
        this.subscribeChain = this.subscribeChain
            .then(work)
            .catch((err) => {
            console.warn(`[relay] ${label}:`, err.message);
        });
    }
    enqueueFocus(work, label) {
        this.focusChain = this.focusChain
            .then(work)
            .catch((err) => {
            console.warn(`[relay] ${label}:`, err.message);
        });
    }
    async ensureCdpAgentsWindow() {
        await this.cdpBridge.refreshWindows();
        const agentsWin = this.cdpBridge.windows.find((w) => w.kind === 'agents' || isAgentsWindowTitle(w.title));
        if (!agentsWin || agentsWin.id === this.cdpBridge.activeTargetId)
            return;
        this.cdpBridge.suppressFocusSync(4000);
        await this.cdpBridge.switchWindow(agentsWin.id);
        this.commandExecutor.setClient(this.cdpBridge.getClient());
        console.log(`[relay] CDP → Agents window "${agentsWin.title}"`);
    }
    async trySwitchWindowForAgent(agentId, windowId, title, repoName) {
        if (isSyntheticAgentId(agentId) || (title?.trim() && !/^[0-9a-f-]{36}$/i.test(agentId))) {
            await this.ensureCdpAgentsWindow();
            return;
        }
        if (windowId) {
            const win = this.cdpBridge.windows.find((w) => w.id === windowId);
            if (win && win.id === this.cdpBridge.activeTargetId)
                return;
            await this.cdpBridge.refreshWindows();
            if (win && win.id !== this.cdpBridge.activeTargetId) {
                await this.cdpBridge.switchWindow(win.id);
                console.log(`[relay] CDP → editor window "${win.title}" (${windowId.slice(0, 8)})`);
            }
            return;
        }
        const projectDir = this.jsonlIndex.findProjectDirForAgent(agentId);
        if (!projectDir)
            return;
        const folder = basename(projectDir);
        const repoSlug = folder.includes('-Projects-')
            ? (folder.split('-Projects-').pop() ?? '')
            : folder;
        const repoSlugNorm = repoSlug.replace(/-/g, ' ').toLowerCase();
        if (!repoSlugNorm)
            return;
        const match = this.cdpBridge.windows.find((w) => {
            const t = w.title.toLowerCase();
            return (t.includes(repoSlugNorm) || repoSlugNorm.split(' ').every((wrd) => t.includes(wrd)));
        });
        if (!match || match.id === this.cdpBridge.activeTargetId)
            return;
        await this.cdpBridge.refreshWindows();
        const refreshed = this.cdpBridge.windows.find((w) => w.id === match.id);
        if (refreshed && refreshed.id !== this.cdpBridge.activeTargetId) {
            await this.cdpBridge.switchWindow(refreshed.id);
            console.log(`[relay] CDP → window "${refreshed.title}" for agent ${agentId.slice(0, 8)}`);
        }
    }
    handleRemoteClientEvent(event, ...args) {
        if (event === 'relay:sync') {
            this.pushFullStateToRemote();
            return;
        }
        const reply = (ev, payload) => this.broadcast(ev, payload);
        this.dispatchClientEvent(event, args, reply);
    }
    onConnect(socket) {
        socket.emit('state:full', this.withRelayState(this.stateManager.getState()));
        const sendIndex = (index) => {
            socket.emit('agents:index', mergeSidebarWithJsonl(this.stateManager.getState().sidebarRepos, index, this.stateManager.getState().composerIdByTitle));
        };
        if (this.lastJsonlIndex.updatedAt > 0) {
            sendIndex(this.lastJsonlIndex);
        }
        else {
            void this.jsonlIndex.rebuild({ broadcast: false }).then((index) => {
                this.lastJsonlIndex = index;
                sendIndex(index);
            });
        }
        const reply = (ev, payload) => {
            socket.emit(ev, payload);
        };
        socket.on('command', (payload) => {
            this.enqueueCommand(payload, reply);
        });
        socket.on('agents:history', async (data) => {
            await this.runAgentsHistory(data, reply);
        });
        socket.on('agents:subscribe', (data) => {
            this.enqueueSubscribe(() => this.runAgentsSubscribe(data), `agents:subscribe ${data?.agentId?.slice(0, 8) ?? '?'}`);
        });
        socket.on('agents:unsubscribe', (data) => {
            this.runAgentsUnsubscribe(data);
        });
        socket.on('agents:focus', (data) => {
            this.enqueueSubscribe(() => this.runAgentsFocus(data, reply), `agents:focus ${data?.agentId?.slice(0, 8) ?? '?'}`);
        });
        socket.on('agents:refresh', () => {
            this.refreshAgentsIndex(true);
        });
        socket.on('disconnect', () => {
            /* subscriptions stay global — one phone session typical */
        });
    }
    dispatchClientEvent(event, args, reply) {
        if (event === 'command') {
            this.enqueueCommand(args[0], reply);
            return;
        }
        if (event === 'agents:history') {
            void this.runAgentsHistory(args[0], reply);
            return;
        }
        if (event === 'agents:subscribe') {
            const data = args[0];
            this.enqueueSubscribe(() => this.runAgentsSubscribe(data), `upstream:subscribe ${data?.agentId?.slice(0, 8) ?? '?'}`);
            return;
        }
        if (event === 'agents:unsubscribe') {
            this.runAgentsUnsubscribe(args[0]);
            return;
        }
        if (event === 'agents:focus') {
            const data = args[0];
            this.enqueueSubscribe(() => this.runAgentsFocus(data, reply), `upstream:focus ${data?.agentId?.slice(0, 8) ?? '?'}`);
            return;
        }
        if (event === 'agents:refresh') {
            this.refreshAgentsIndex(true);
        }
    }
    async runCommand(payload, reply) {
        console.log(`[relay] command ${payload.type} id=${payload.id}`);
        const cmdT0 = Date.now();
        // Avoid races with focus-follow during Editor multi-window command execution.
        this.cdpBridge.suppressFocusSync(2500);
        if (payload.type === 'switch_window' && payload.windowId) {
            await this.cdpBridge.refreshWindows();
            await this.cdpBridge.switchWindow(payload.windowId);
            this.stateManager.patchNow({
                windows: this.cdpBridge.windows,
                activeWindowId: this.cdpBridge.activeTargetId,
            });
            this.domExtractor.pollNow();
            return;
        }
        if (payload.type === 'new_chat' && (payload.repoName?.trim() || !payload.windowId)) {
            await this.ensureCdpAgentsWindow();
            this.commandExecutor.setClient(this.cdpBridge.getClient());
        }
        if (payload.type === 'new_chat' && payload.windowId) {
            await this.cdpBridge.refreshWindows();
            await this.cdpBridge.switchWindow(payload.windowId);
            this.stateManager.patchNow({
                windows: this.cdpBridge.windows,
                activeWindowId: this.cdpBridge.activeTargetId,
            });
        }
        if (payload.type === 'close_chat' &&
            (payload.repoName?.trim() || !payload.windowId)) {
            await this.ensureCdpAgentsWindow();
            this.commandExecutor.setClient(this.cdpBridge.getClient());
        }
        if (payload.type === 'close_chat' && payload.windowId) {
            await this.cdpBridge.refreshWindows();
            await this.cdpBridge.switchWindow(payload.windowId);
            this.stateManager.patchNow({
                windows: this.cdpBridge.windows,
                activeWindowId: this.cdpBridge.activeTargetId,
            });
        }
        if (payload.agentId &&
            (payload.type === 'remove_queued' ||
                payload.type === 'send_message' ||
                payload.type === 'stop_agent' ||
                payload.type === 'focus_agent' ||
                payload.type === 'close_chat')) {
            await this.trySwitchWindowForAgent(payload.agentId, payload.windowId, payload.tabTitle, payload.repoName);
        }
        if (payload.agentId &&
            this.agentCompletionPush &&
            payload.type !== 'stop_agent' &&
            payload.type !== 'focus_agent' &&
            payload.type !== 'close_chat') {
            this.agentCompletionPush.markAppInitiated(payload.agentId, payload.id);
        }
        const st = this.stateManager.getState();
        if (payload.type === 'send_message' && payload.agentId?.trim()) {
            this.stateManager.patchNow(markAgentWorkingInState(st, payload.agentId.trim()));
        }
        this.commandExecutor.setActiveChat(st.activeComposerId, st.activeChatTitle);
        const client = this.cdpBridge.getClient();
        const result = client
            ? await client.runExclusive(() => this.commandExecutor.execute(payload))
            : await this.commandExecutor.execute(payload);
        console.log(`[relay] command ${payload.type} id=${payload.id} done ms=${Date.now() - cmdT0} ok=${result.ok}${result.error ? ` err=${result.error.slice(0, 80)}` : ''}`);
        reply('command:result', result);
        this.cdpBridge.suppressFocusSync(result.ok ? 1500 : 3000);
        if (payload.type === 'new_chat' && result.ok) {
            const newId = result.activeAgentId?.trim();
            if (newId && isComposerUuid(newId) && payload.windowId) {
                this.stateManager.patchNow(patchStateForEditorNewChat(this.stateManager.getState(), newId, result.activeChatTitle));
            }
            this.domExtractor.pollNow();
            setTimeout(() => {
                void this.refreshAgentsIndex(true);
            }, 500);
        }
        if (payload.type === 'close_chat' && result.ok) {
            this.domExtractor.pollNow();
            setTimeout(() => {
                void this.refreshAgentsIndex(true);
            }, 400);
        }
        if ((payload.type === 'stop_agent' ||
            payload.type === 'remove_queued' ||
            payload.type === 'focus_agent' ||
            payload.type === 'click_selector') &&
            result.ok &&
            (payload.type !== 'click_selector' ||
                payload.questionnaireAction ||
                payload.questionnaireFreeform ||
                payload.selectorPath?.includes('questionnaire') ||
                payload.selectorPath?.includes('ui-tray-option') ||
                payload.selectorPath?.includes('ui-tray-actions'))) {
            if (payload.type === 'stop_agent') {
                const id = typeof payload.agentId === 'string' && payload.agentId.trim()
                    ? payload.agentId.trim()
                    : this.stateManager.getState().activeComposerId?.trim() || '';
                if (id)
                    suppressAgentCompletionPush(id, 60_000);
                if (id)
                    this.agentCompletionPush?.clearAppInitiated(id);
                this.stateManager.patchNow({
                    agentWorking: false,
                    agentStatus: undefined,
                    agentStatusMessage: undefined,
                    composerDraft: undefined,
                    composerDraftImages: undefined,
                });
            }
            this.domExtractor.pollNow();
            if (payload.type === 'focus_agent' &&
                result.ok &&
                payload.agentId &&
                isSyntheticAgentId(payload.agentId)) {
                await sleepMs(350);
                await this.emitJsonlBaselineForAgent(payload.agentId, payload.tabTitle, false);
            }
            if (payload.type === 'click_selector' &&
                (payload.questionnaireAction || payload.selectorPath?.includes('ui-tray'))) {
                setTimeout(() => this.domExtractor.pollNow(), 280);
            }
        }
    }
    async runAgentsHistory({ agentId, title, requestId, limit, offset, minMessages, turnLimit: turnLimitReq, }, reply) {
        const t0 = Date.now();
        const viaUpstream = this.upstream?.connected ?? false;
        const turnLimit = typeof turnLimitReq === 'number' && turnLimitReq > 0
            ? Math.floor(turnLimitReq)
            : undefined;
        const socketLimit = turnLimit
            ? turnLimit
            : resolveHistoryLimit({ limit, minMessages });
        console.log(`[relay] agents:history req agentId=${agentId} title=${title?.slice(0, 48) ?? '-'} turnLimit=${turnLimit ?? '-'} limit=${socketLimit} rid=${requestId ?? '-'} upstream=${viaUpstream}`);
        this.jsonlIndex.historyReplyInFlight.add(agentId);
        bridgePipelineLog({
            dir: 'in',
            event: 'agents:history:req',
            requestId,
            agentId,
            detail: `turnLimit=${turnLimit ?? '-'} limit=${socketLimit} title=${title?.slice(0, 32) ?? '-'} upstream=${viaUpstream}`,
        });
        try {
            const full = await this.jsonlIndex.loadHistory(agentId, {
                title,
                composerIdByTitle: this.stateManager.getState().composerIdByTitle,
                offset,
            });
            this.chatDisplay.setJsonlBaseline(agentId, full.messages);
            if (!turnLimit) {
                this.emitJsonlLiveForAgent(agentId, full.messages, full.totalMessages);
            }
            else {
                this.clientHistoryTurns.set(agentId, Math.max(this.getClientTurnLimit(agentId), turnLimit));
            }
            const snap = this.agentMessagesSnapshot(agentId, full.totalMessages);
            let outMessages = snap.messages;
            let totalTurns;
            let hasMoreOlder;
            if (turnLimit) {
                const page = sliceMessagesByTurns(snap.messages, { turnLimit });
                outMessages = page.messages;
                totalTurns = page.totalTurns;
                hasMoreOlder = page.hasMoreOlder;
            }
            else if (socketLimit > 0 && snap.messages.length > socketLimit) {
                outMessages = snap.messages.slice(-socketLimit);
            }
            if (!isChatHistoryFromJsonl()) {
                const seq = this.historySeqByAgent.get(agentId) ?? 0;
                const ms = Date.now() - t0;
                console.log(`[relay] agents:history jsonl agentId=${agentId} hist=${snap.historyMessages.length} msgs=${outMessages.length} total=${full.totalMessages} ms=${ms} rid=${requestId ?? '-'}`);
                reply('agents:history', {
                    agentId,
                    ...snap,
                    messages: outMessages,
                    totalMessages: full.totalMessages,
                    totalTurns,
                    hasMoreOlder,
                    requestId,
                    updatedAt: Date.now(),
                    seq,
                });
                return;
            }
            const ms = Date.now() - t0;
            const bytes = JSON.stringify(outMessages).length;
            console.log(`[relay] agents:history ok agentId=${agentId} msgs=${outMessages.length}/${full.totalMessages ?? '?'} turns=${totalTurns ?? '?'} bytes=${bytes} ms=${ms} rid=${requestId ?? '-'}`);
            bridgePipelineLog({
                dir: 'out',
                event: 'agents:history:reply',
                requestId,
                agentId,
                bytes,
                msgs: outMessages.length,
                detail: `total=${full.totalMessages ?? '?'} turns=${totalTurns ?? '?'} ms=${ms}`,
            });
            const seq = this.historySeqByAgent.get(agentId) ?? 0;
            reply('agents:history', {
                agentId,
                historyMessages: snap.historyMessages,
                liveMessages: snap.liveMessages,
                messages: outMessages,
                totalMessages: full.totalMessages,
                totalTurns,
                hasMoreOlder,
                requestId,
                updatedAt: Date.now(),
                seq,
            });
        }
        catch (err) {
            console.error(`[relay] agents:history fail agentId=${agentId} ms=${Date.now() - t0} rid=${requestId ?? '-'}:`, err.message);
            bridgePipelineLog({
                dir: 'internal',
                event: 'agents:history:fail',
                requestId,
                agentId,
                detail: err.message,
            });
            reply('agents:history', {
                agentId,
                messages: [],
                totalMessages: 0,
                requestId,
                updatedAt: Date.now(),
            });
        }
        finally {
            this.jsonlIndex.historyReplyInFlight.delete(agentId);
        }
    }
    async runAgentsSubscribe({ agentId, title, focus, windowId, }) {
        if (!agentId)
            return;
        this.cancelJsonlDrain(agentId);
        const alreadySubscribed = this.jsonlIndex.getSubscribedAgents().has(agentId);
        const clearStore = !alreadySubscribed;
        if (!alreadySubscribed) {
            this.clientHistoryTurns.set(agentId, AGENT_HISTORY_INITIAL_TURNS);
        }
        this.jsonlIndex.subscribe(agentId, title, { emitHistory: !alreadySubscribed });
        this.stateManager.patchNow({ lastError: undefined });
        const st = this.stateManager.getState();
        const activeId = st.activeComposerId?.trim() ?? '';
        const titleNorm = title?.trim().toLowerCase() ?? '';
        const activeTitleNorm = st.activeChatTitle?.trim().toLowerCase() ?? '';
        const alreadyFocusedInCursor = activeId === agentId ||
            (!!titleNorm && !!activeTitleNorm && titleNorm === activeTitleNorm);
        await this.emitJsonlBaselineForAgent(agentId, title, clearStore);
        if (alreadySubscribed && alreadyFocusedInCursor && focus !== false) {
            this.domExtractor.pollNow();
            return;
        }
        if (focus === false) {
            this.domExtractor.pollNow();
            return;
        }
        this.enqueueFocus(() => this.runSubscribeFocusWork(agentId, title, windowId), `subscribe-focus ${agentId.slice(0, 8)}`);
    }
    /** CDP focus + scroll — off hot path so socket.io stays responsive. */
    inferRepoNameForAgent(agentId, title) {
        const titleNorm = title?.trim().toLowerCase();
        const repos = this.stateManager.getState().sidebarRepos?.length
            ? this.stateManager.getState().sidebarRepos
            : this.lastJsonlIndex.repos;
        for (const repo of repos ?? []) {
            for (const agent of repo.agents) {
                if (agent.id === agentId)
                    return repo.name;
                if (titleNorm && agent.title.trim().toLowerCase() === titleNorm)
                    return repo.name;
            }
        }
        return undefined;
    }
    async runSubscribeFocusWork(agentId, title, windowId, repoName) {
        try {
            const repo = repoName ?? this.inferRepoNameForAgent(agentId, title);
            this.cdpBridge.suppressFocusSync(4000);
            await this.trySwitchWindowForAgent(agentId, windowId, title, repo);
            const result = await this.commandExecutor.execute({
                id: `subscribe-focus-${Date.now()}`,
                type: 'focus_agent',
                agentId,
                tabTitle: title,
                repoName: repo,
            });
            if (!result.ok) {
                console.warn('[relay] subscribe focus (non-fatal):', result.error);
            }
            try {
                await this.commandExecutor.scrollChatToBottom();
            }
            catch {
                /* non-fatal */
            }
            this.domExtractor.pollNow();
            if (result.ok && isSyntheticAgentId(agentId)) {
                await sleepMs(350);
                await this.emitJsonlBaselineForAgent(agentId, title, false);
            }
        }
        catch (err) {
            console.warn('[relay] subscribe focus work:', err.message);
        }
    }
    /** Push JSONL lenta to phone before CDP focus (fast chat open / switch). */
    async emitJsonlBaselineForAgent(agentId, title, clear = true) {
        try {
            if (clear) {
                this.chatDisplay.clearAgent(agentId);
                this.lastEmittedJsonlRows.delete(agentId);
                this.lastEmittedLentaSig.delete(agentId);
                this.lastEmittedHistLen.delete(agentId);
                this.lentaPendingSince.delete(agentId);
                this.clientHistoryTurns.set(agentId, AGENT_HISTORY_INITIAL_TURNS);
            }
            const st = this.stateManager.getState();
            const history = await this.jsonlIndex.loadHistory(agentId, {
                title,
                composerIdByTitle: st.composerIdByTitle,
                activeComposerId: st.activeComposerId,
                activeTabTitle: st.activeChatTitle,
            });
            this.lastEmittedLentaSig.delete(agentId);
            this.emitJsonlLiveForAgent(agentId, history.messages, history.totalMessages);
        }
        catch (err) {
            console.warn('[relay] emitJsonlBaseline (non-fatal):', err.message);
        }
    }
    /** Load JSONL into store when DOM ingest runs but baseline empty (e.g. after bridge restart). */
    async ensureJsonlBaselineForAgent(agentId, state) {
        if (this.chatDisplay.getJsonlHistory(agentId).length > 0)
            return;
        const sub = this.jsonlIndex.getSubscribedAgents().get(agentId);
        try {
            const history = await this.jsonlIndex.loadHistory(agentId, {
                title: sub?.title,
                composerIdByTitle: state.composerIdByTitle,
            });
            if (!history.messages.length)
                return;
            this.chatDisplay.setJsonlBaseline(agentId, history.messages);
            this.lastEmittedJsonlRows.set(agentId, history.totalMessages);
            this.domExtractor.pollNow();
        }
        catch (err) {
            console.warn('[relay] ensureJsonlBaseline (non-fatal):', err.message);
        }
    }
    /** Legacy path — prefer emitJsonlBaselineForAgent + focus in runAgentsSubscribe. */
    async refreshDomChatOnSubscribe(agentId, title, opts) {
        await this.emitJsonlBaselineForAgent(agentId, title, opts?.clear !== false);
        try {
            await this.commandExecutor.scrollChatToBottom();
        }
        catch {
            /* non-fatal */
        }
        this.domExtractor.pollNow();
    }
    runAgentsUnsubscribe({ agentId }) {
        if (!agentId)
            return;
        const subMeta = this.jsonlIndex.getSubscribedAgents().get(agentId);
        const title = subMeta?.title;
        const state = this.stateManager.getState();
        const snap = this.lentaSnapshot(agentId, this.lastEmittedJsonlRows.get(agentId));
        const needsDrain = snap.liveMessages.length > 0 ||
            this.chatDisplay.isAgentGenerating(agentId) ||
            isAgentGenerating(agentId, state) ||
            this.isClientLentaPending(agentId, snap.messages);
        this.jsonlIndex.unsubscribe(agentId);
        this.lastEmittedJsonlRows.delete(agentId);
        this.lastEmittedLentaSig.delete(agentId);
        this.lastEmittedHistLen.delete(agentId);
        this.lentaPendingSince.delete(agentId);
        this.clientHistoryTurns.delete(agentId);
        this.refreshLentaPendingPatch();
        if (needsDrain) {
            this.startJsonlDrain(agentId, title);
        }
        else {
            this.finishJsonlDrain(agentId);
        }
    }
    async runAgentsFocus({ agentId, title, windowId, repoName, }, reply) {
        if (!agentId)
            return;
        const id = `focus-${Date.now()}`;
        this.enqueueFocus(async () => {
            const repo = repoName ?? this.inferRepoNameForAgent(agentId, title);
            this.cdpBridge.suppressFocusSync(4000);
            await this.trySwitchWindowForAgent(agentId, windowId, title, repo);
            const result = await this.commandExecutor.execute({
                id,
                type: 'focus_agent',
                agentId,
                tabTitle: title,
                repoName: repo,
            });
            reply('command:result', result);
            if (result.ok) {
                if (isSyntheticAgentId(agentId)) {
                    await this.emitJsonlBaselineForAgent(agentId, title, false);
                }
                this.domExtractor.pollNow();
            }
        }, `agents:focus ${agentId.slice(0, 8)}`);
    }
}
