"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveUserMessageLanguageCodeForTest = resolveUserMessageLanguageCodeForTest;
exports.shouldQueueUserSupplementAtGenerationBoundary = shouldQueueUserSupplementAtGenerationBoundary;
exports.wrapCriticalUserInterjectionPromptAtIngress = wrapCriticalUserInterjectionPromptAtIngress;
exports.handleWebSocketMessage = handleWebSocketMessage;
exports.setupWebSocketServer = setupWebSocketServer;
exports.cleanupEventSystems = cleanupEventSystems;
/**
 * Module: server/websocket-handler
 *
 * Common WebSocket handling functionality for dialog communication
 */
const diligence_1 = require("@longrun-ai/kernel/diligence");
const evt_1 = require("@longrun-ai/kernel/evt");
const language_1 = require("@longrun-ai/kernel/types/language");
const storage_1 = require("@longrun-ai/kernel/types/storage");
const time_1 = require("@longrun-ai/kernel/utils/time");
const crypto_1 = require("crypto");
const promises_1 = __importDefault(require("fs/promises"));
const path_1 = __importDefault(require("path"));
const ws_1 = require("ws");
const runtime_1 = require("../apps/runtime");
const global_dialog_event_broadcaster_1 = require("../bootstrap/global-dialog-event-broadcaster");
const dialog_1 = require("../dialog");
const dialog_display_state_1 = require("../dialog-display-state");
const dialog_global_registry_1 = require("../dialog-global-registry");
const dialog_instance_registry_1 = require("../dialog-instance-registry");
const dialog_latest_state_1 = require("../dialog-latest-state");
const evt_registry_1 = require("../evt-registry");
const kernel_driver_1 = require("../llm/kernel-driver");
const context_health_1 = require("../llm/kernel-driver/context-health");
const runtime_2 = require("../llm/kernel-driver/runtime");
const log_1 = require("../log");
const persistence_1 = require("../persistence");
const persistence_errors_1 = require("../persistence-errors");
const priming_1 = require("../priming");
const problems_1 = require("../problems");
const reply_delivery_recovery_1 = require("../recovery/reply-delivery-recovery");
const driver_messages_1 = require("../runtime/driver-messages");
const inter_dialog_format_1 = require("../runtime/inter-dialog-format");
const work_language_1 = require("../runtime/work-language");
const team_1 = require("../team");
const team_config_updates_1 = require("../team-config-updates");
const tool_availability_updates_1 = require("../tool-availability-updates");
const pending_tellask_reminder_1 = require("../tools/pending-tellask-reminder");
const id_1 = require("../utils/id");
const auth_1 = require("./auth");
const create_dialog_contract_1 = require("./create-dialog-contract");
const dominds_runtime_status_1 = require("./dominds-runtime-status");
const dominds_self_update_1 = require("./dominds-self-update");
function resolveMemberDiligencePushMax(team, agentId) {
    const member = team.getMember(agentId);
    if (member && member.diligence_push_max !== undefined) {
        return member.diligence_push_max;
    }
    return diligence_1.DEFAULT_DILIGENCE_PUSH_MAX;
}
function normalizeDiligencePushMax(value) {
    if (!Number.isFinite(value))
        return 0;
    return Math.floor(value);
}
const USER_IMAGE_ATTACHMENT_MAX_COUNT = 10;
const USER_IMAGE_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
function userImageMimeTypeToExt(mimeType) {
    switch (mimeType) {
        case 'image/png':
            return 'png';
        case 'image/jpeg':
            return 'jpg';
        case 'image/webp':
            return 'webp';
        case 'image/gif':
            return 'gif';
        default:
            return null;
    }
}
function sanitizeArtifactPathSegment(value) {
    const cleaned = value.replace(/[^a-zA-Z0-9._-]/g, '_').replace(/_+/g, '_');
    const trimmed = cleaned.replace(/^_+|_+$/g, '');
    return trimmed.length > 0 ? trimmed.slice(0, 96) : 'item';
}
function parseUserImageAttachments(raw) {
    if (raw === undefined)
        return [];
    if (!Array.isArray(raw)) {
        throw new Error('attachments must be an array when provided');
    }
    if (raw.length > USER_IMAGE_ATTACHMENT_MAX_COUNT) {
        throw new Error(`at most ${String(USER_IMAGE_ATTACHMENT_MAX_COUNT)} image attachments are allowed`);
    }
    return raw.map((item, index) => {
        if (typeof item !== 'object' || item === null || Array.isArray(item)) {
            throw new Error(`attachments[${String(index)}] must be an object`);
        }
        const record = item;
        const kind = record['kind'];
        const mimeType = record['mimeType'];
        const byteLength = record['byteLength'];
        const dataBase64 = record['dataBase64'];
        if (kind !== 'image') {
            throw new Error(`attachments[${String(index)}].kind must be image`);
        }
        if (typeof mimeType !== 'string' || userImageMimeTypeToExt(mimeType) === null) {
            throw new Error(`attachments[${String(index)}].mimeType is unsupported`);
        }
        if (typeof byteLength !== 'number' ||
            !Number.isFinite(byteLength) ||
            byteLength <= 0 ||
            byteLength > USER_IMAGE_ATTACHMENT_MAX_BYTES) {
            throw new Error(`attachments[${String(index)}].byteLength must be between 1 and ${String(USER_IMAGE_ATTACHMENT_MAX_BYTES)}`);
        }
        if (typeof dataBase64 !== 'string' || dataBase64.trim() === '') {
            throw new Error(`attachments[${String(index)}].dataBase64 is required`);
        }
        return { kind, mimeType, byteLength, dataBase64 };
    });
}
function isStrictBase64Payload(value) {
    if (value.length === 0 || value.length % 4 !== 0)
        return false;
    return /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value);
}
function prepareUserImageAttachments(attachments) {
    return attachments.map((attachment, index) => {
        if (!isStrictBase64Payload(attachment.dataBase64)) {
            throw new Error(`attachments[${String(index)}].dataBase64 must be strict base64`);
        }
        const bytes = Buffer.from(attachment.dataBase64, 'base64');
        if (bytes.length !== attachment.byteLength) {
            throw new Error(`attachments[${String(index)}].byteLength mismatch: declared=${String(attachment.byteLength)} decoded=${String(bytes.length)}`);
        }
        return {
            mimeType: attachment.mimeType,
            bytes,
        };
    });
}
async function persistPreparedUserImageAttachments(args) {
    if (args.attachments.length === 0)
        return undefined;
    const eventsBase = persistence_1.DialogPersistence.getDialogEventsPath(args.dialog.id, args.dialog.status);
    const safeMsgId = sanitizeArtifactPathSegment(args.msgId);
    const contentItems = [];
    for (let index = 0; index < args.attachments.length; index += 1) {
        const attachment = args.attachments[index];
        const ext = userImageMimeTypeToExt(attachment.mimeType);
        if (ext === null) {
            throw new Error(`attachments[${String(index)}].mimeType is unsupported`);
        }
        const relPath = path_1.default.posix.join('artifacts', 'user-input', safeMsgId, `${String(index + 1).padStart(2, '0')}-${(0, crypto_1.randomUUID)()}.${ext}`);
        const absPath = path_1.default.join(eventsBase, ...relPath.split('/'));
        await promises_1.default.mkdir(path_1.default.dirname(absPath), { recursive: true });
        await promises_1.default.writeFile(absPath, attachment.bytes);
        contentItems.push({
            type: 'input_image',
            mimeType: attachment.mimeType,
            byteLength: attachment.bytes.length,
            artifact: {
                rootId: args.dialog.id.rootId,
                selfId: args.dialog.id.selfId,
                status: args.dialog.status,
                relPath,
            },
        });
    }
    return contentItems;
}
function parsePersistableDialogStatus(raw) {
    if (raw !== 'running' && raw !== 'completed' && raw !== 'archived') {
        return null;
    }
    return raw;
}
function readOptionalPersistableDialogStatus(raw) {
    if (raw === undefined) {
        return { kind: 'missing' };
    }
    const status = parsePersistableDialogStatus(raw);
    if (status === null) {
        return { kind: 'invalid' };
    }
    return { kind: 'value', status };
}
const DIALOG_STATUS_RESOLUTION_CANDIDATES = [
    'running',
    'completed',
    'archived',
];
function orderedDialogStatusCandidates(preferredStatus) {
    return [
        preferredStatus,
        ...DIALOG_STATUS_RESOLUTION_CANDIDATES.filter((status) => status !== preferredStatus),
    ];
}
async function restoreDialogForDisplay(dialogId, preferredStatus) {
    for (const status of orderedDialogStatusCandidates(preferredStatus)) {
        const metadata = await persistence_1.DialogPersistence.loadDialogMetadata(dialogId, status);
        if (!metadata)
            continue;
        const dialogState = await persistence_1.DialogPersistence.restoreDialog(dialogId, status);
        if (!dialogState) {
            throw new Error(`Dialog ${dialogId.valueOf()} metadata exists in ${status}, but dialog restoration failed`);
        }
        return { status, dialogState, metadata };
    }
    return null;
}
function formatDeclaredDeadSideDialogNotice(language, dialogId, callName) {
    if (language === 'zh') {
        switch (callName) {
            case 'tellask':
                return `${(0, driver_messages_1.formatSystemNoticePrefix)('zh')} 支线对话 ${dialogId} 已被用户宣布卡死（不可逆）。后续仍可重用相同的 slug 发起全新支线对话；只是此前的上下文已不再，新的诉请正文请提供最新的完整上下文信息。`;
            case 'tellaskSessionless':
                return `${(0, driver_messages_1.formatSystemNoticePrefix)('zh')} 支线对话 ${dialogId} 已被用户宣布卡死（不可逆）。这是一次性支线对话；后续若仍需继续，请重新发起新的支线对话。由于不会续接此前上下文，新的诉请正文请提供最新的完整上下文信息。`;
            case 'freshBootsReasoning':
                return `${(0, driver_messages_1.formatSystemNoticePrefix)('zh')} 支线对话 ${dialogId} 已被用户宣布卡死（不可逆）。这是一次扪心自问（FBR）支线对话；后续若仍需继续，请重新发起新的扪心自问（FBR）支线对话。由于不会续接此前上下文，新的诉请正文请提供最新的完整上下文信息。`;
        }
    }
    switch (callName) {
        case 'tellask':
            return `${(0, driver_messages_1.formatSystemNoticePrefix)('en')} Side Dialog ${dialogId} has been declared dead by the user (irreversible). You may reuse the same slug to start a brand-new Side Dialog, but previous context is no longer retained; include the latest complete context in the new tellask body.`;
        case 'tellaskSessionless':
            return `${(0, driver_messages_1.formatSystemNoticePrefix)('en')} Side Dialog ${dialogId} has been declared dead by the user (irreversible). This was a one-shot Side Dialog; if you still need the work, start a new Side Dialog. Previous context will not carry over, so include the latest complete context in the new tellask body.`;
        case 'freshBootsReasoning':
            return `${(0, driver_messages_1.formatSystemNoticePrefix)('en')} Side Dialog ${dialogId} has been declared dead by the user (irreversible). This was an FBR Side Dialog; if you still need the work, start a new FBR Side Dialog. Previous context will not carry over, so include the latest complete context in the new tellask body.`;
    }
}
const log = (0, log_1.createLogger)('websocket-handler');
const wsLiveDlg = new WeakMap();
const wsSub = new WeakMap();
const wsUiLanguage = new WeakMap();
let broadcastRunControlRefreshMessage = null;
let broadcastDialogsIndexMessage = null;
let broadcastDiligencePushUpdatedMessage = null;
function emitRunControlRefresh(reason) {
    broadcastRunControlRefreshMessage?.({
        type: 'run_control_refresh',
        reason,
        timestamp: (0, time_1.formatUnifiedTimestamp)(new Date()),
    });
}
function buildResumeIneligibleMessage(latest) {
    // WARNING:
    // `resume_dialog` eligibility is intentionally based on the freshly healed projection, not on a
    // naive local check of raw wait facts. In particular, the paused-interjection stopped state
    // remains resumable here so explicit Continue can re-evaluate the true state even while the
    // underlying dialog may still be waiting for human input or a side dialog.
    //
    // The actual outcome of that Continue attempt is decided later in `flow.ts` from fresh facts:
    // it may restore the concrete wait state, or it may immediately continue driving. Do not reinterpret a
    // resumable stopped state here as "guaranteed to run now".
    const state = latest?.displayState;
    if (!state) {
        return {
            reason: 'missing',
            message: 'Dialog is not currently eligible for resumption.',
        };
    }
    switch (state.kind) {
        case 'blocked':
            return {
                reason: 'needs_human_input',
                message: 'Fresh state scan shows this dialog is waiting for human input, so it cannot resume yet.',
            };
        case 'waiting_side_dialog':
            return {
                reason: 'waiting_side_dialog',
                message: 'Fresh state scan shows this dialog is waiting for a side dialog, so it cannot resume yet.',
            };
        case 'idle_waiting_user':
            return {
                reason: 'idle_waiting_user',
                message: 'Fresh state scan shows this dialog is no longer interrupted and is now waiting for a new user input.',
            };
        case 'proceeding':
        case 'proceeding_stop_requested':
            return {
                reason: 'already_running',
                message: 'Fresh state scan shows this dialog is already running, so it cannot be resumed again.',
            };
        case 'stopped':
            return {
                reason: 'stopped_not_resumable',
                message: 'Fresh state scan shows this dialog is stopped but not currently resumable.',
            };
        case 'dead':
            return {
                reason: 'dead',
                message: 'Fresh state scan shows this dialog has been declared dead and cannot be resumed.',
            };
        default: {
            const _exhaustive = state;
            return {
                reason: 'missing',
                message: `Dialog is not currently eligible for resumption: ${String(_exhaustive)}`,
            };
        }
    }
}
async function syncPendingTellaskReminderBestEffort(dialog, where) {
    try {
        await (0, pending_tellask_reminder_1.syncPendingTellaskReminderState)(dialog);
    }
    catch (err) {
        log.warn(`Failed to sync pending tellask reminder at ${where}`, err, {
            dialogId: dialog.id.selfId,
            rootId: dialog.id.rootId,
        });
    }
}
function resolveUserLanguageCode(ws, raw, fallbackDialog) {
    if (typeof raw === 'string') {
        const parsed = (0, language_1.normalizeLanguageCode)(raw);
        if (parsed)
            return parsed;
    }
    const fromWs = wsUiLanguage.get(ws);
    if (fromWs)
        return fromWs;
    if (fallbackDialog)
        return fallbackDialog.getLastUserLanguageCode();
    return (0, work_language_1.getWorkLanguage)();
}
function resolveUserMessageLanguageCodeForTest(args) {
    return resolveUserLanguageCode(args.ws, args.raw, args.fallbackDialog);
}
function syncDialogLanguagePreference(dialog, language, options) {
    const previousLanguage = dialog.getLastUserLanguageCode();
    dialog.setUiLanguage(language);
    dialog.setLastUserLanguageCode(language);
    if (options?.appendSwitchNotice) {
        dialog.appendCourseLanguageChangedNotice(previousLanguage, language);
        return;
    }
    dialog.resetCourseLanguageNotice();
}
function shouldQueueUserSupplementAtGenerationBoundary(args) {
    if (!args.isLocked) {
        return false;
    }
    return args.latestGenerating || args.inMemoryGenerating;
}
async function queueUserSupplementAtGenerationBoundary(dialog, prompt) {
    const latest = await persistence_1.DialogPersistence.loadDialogLatest(dialog.id, 'running');
    const inMemoryGenerating = dialog.hasActiveGeneration;
    // Live UX can observe generating_start before latest.yaml flips generating=true, so boundary
    // queuing must honor either persisted or in-memory generation state.
    if (!shouldQueueUserSupplementAtGenerationBoundary({
        latestGenerating: latest?.generating === true,
        inMemoryGenerating,
        isLocked: dialog.isLocked(),
    })) {
        return false;
    }
    const queued = dialog.queueUserPromptAtGenerationBoundary({
        prompt: prompt.content,
        contentItems: prompt.contentItems,
        msgId: prompt.msgId,
        grammar: prompt.grammar,
        userLanguageCode: prompt.userLanguageCode,
    });
    (0, evt_registry_1.postDialogEvent)(dialog, {
        type: 'queue_user_msg_evt',
        course: dialog.currentCourse,
        msgId: queued.msgId,
        content: queued.prompt,
        contentItems: queued.contentItems,
        grammar: queued.grammar ?? 'markdown',
        origin: 'user',
        userLanguageCode: queued.userLanguageCode,
    });
    log.debug('Queued user supplement for next generation boundary', undefined, {
        rootId: dialog.id.rootId,
        selfId: dialog.id.selfId,
        msgId: queued.msgId,
        incomingMsgId: prompt.msgId,
        latestGenerating: latest?.generating === true,
        inMemoryGenerating,
    });
    return true;
}
function wrapCriticalUserInterjectionPromptAtIngress(dialog, prompt) {
    if ((0, driver_messages_1.isAgentFacingCriticalUserInterjectionRemediationGuideContent)(prompt.content)) {
        return prompt;
    }
    const snapshot = dialog.getLastContextHealth();
    if (snapshot?.kind !== 'available' || snapshot.level !== 'critical') {
        return prompt;
    }
    const remaining = (0, context_health_1.resolveCriticalCountdownRemaining)(dialog.id.key(), snapshot);
    if (remaining <= 0) {
        return prompt;
    }
    const language = prompt.userLanguageCode ?? dialog.getLastUserLanguageCode();
    const dialogScope = dialog instanceof dialog_1.MainDialog ? 'mainDialog' : 'sideDialog';
    const prefix = (0, driver_messages_1.formatAgentFacingCriticalUserInterjectionRemediationGuide)(language, {
        dialogScope,
        promptsRemainingAfterThis: (0, context_health_1.consumeCriticalCountdown)(dialog.id.key()),
    });
    return {
        ...prompt,
        content: `${prefix}\n\n${prompt.content}`,
    };
}
/**
 * Get error code from unknown error
 */
function getErrorCode(error) {
    if (typeof error !== 'object' || error === null)
        return undefined;
    const maybeCode = error.code;
    return typeof maybeCode === 'string' ? maybeCode : undefined;
}
function sendCreateDialogFailure(ws, requestId, errorCode, error) {
    const payload = (0, create_dialog_contract_1.makeCreateDialogFailure)(requestId, errorCode, error);
    ws.send(JSON.stringify(payload));
}
/**
 * Cleanup WebSocket client: cancel active forwarder and clear live dialog state
 */
function cleanupWsClient(ws) {
    const existingSub = wsSub.get(ws);
    if (existingSub) {
        try {
            existingSub.subChan.cancel();
        }
        catch (err) {
            log.warn('Failed to cancel forwarder on cleanupWsClient', err);
        }
        wsSub.delete(ws);
    }
    wsLiveDlg.delete(ws);
    wsUiLanguage.delete(ws);
}
/**
 * Setup WebSocket subscription for real-time dialog events
 * Ensures only one subscription per WebSocket connection
 */
async function setupWebSocketSubscription(ws, dialog) {
    // Cancel any existing subscription
    const existingSub = wsSub.get(ws);
    if (existingSub) {
        try {
            existingSub.subChan.cancel();
        }
        catch (err) {
            log.warn('Failed to cancel existing subscription', undefined, err);
        }
    }
    // Store dialog in wsLiveDlg
    wsLiveDlg.set(ws, dialog);
    // Create new subscription for real-time events
    const subChan = evt_registry_1.dialogEventRegistry.createSubChan(dialog.id);
    wsSub.set(ws, { dialogKey: dialog.id.valueOf(), subChan });
    // Forward events from SubChan to WebSocket
    (async () => {
        try {
            for await (const event of subChan.stream()) {
                if (ws.readyState === 1) {
                    ws.send(JSON.stringify(event));
                }
                else {
                    break;
                }
            }
        }
        catch (err) {
            if (err !== evt_1.EndOfStream) {
                log.warn(`Event forwarding error for dialog ${dialog.id.selfId}:`, err);
            }
        }
    })().catch((err) => {
        log.warn(`Event forwarding task failed for dialog ${dialog.id.selfId}:`, err);
    });
}
/**
 * Handle incoming WebSocket messages
 */
async function handleWebSocketMessage(ws, packet) {
    try {
        switch (packet.type) {
            case 'set_ui_language':
                await handleSetUiLanguage(ws, packet);
                break;
            case 'get_problems':
                await handleGetProblems(ws, packet);
                break;
            case 'clear_resolved_problems':
                await handleClearResolvedProblems(ws, packet);
                break;
            case 'create_dialog':
                await handleCreateDialog(ws, packet);
                break;
            case 'display_dialog':
                await handleDisplayDialog(ws, packet);
                break;
            case 'set_diligence_push':
                await handleSetDiligencePush(ws, packet);
                break;
            case 'refill_diligence_push_budget':
                await handleRefillDiligencePushBudget(ws, packet);
                break;
            case 'get_q4h_state':
                await handleGetQ4HState(ws, packet);
                break;
            case 'ack_a2h':
                await handleAckA2H(ws, packet);
                break;
            case 'display_reminders':
                await handleDisplayReminders(ws, packet);
                break;
            case 'display_course':
                await handleDisplayCourse(ws, packet);
                break;
            case 'drive_dlg_by_user_msg':
                await handleUserMsg2Dlg(ws, packet);
                break;
            case 'drive_dialog_by_user_answer':
                await handleReceiveHumanReply(ws, packet);
                break;
            case 'interrupt_dialog':
                await handleInterruptDialog(ws, packet);
                break;
            case 'emergency_stop':
                await handleEmergencyStop(ws, packet);
                break;
            case 'resume_dialog':
                await handleResumeDialog(ws, packet);
                break;
            case 'resume_all':
                await handleResumeAll(ws, packet);
                break;
            case 'declare_sideDialog_dead':
                await handleDeclareSideDialogDead(ws, packet);
                break;
            default:
                log.warn('Unknown WebSocket packet type:', undefined, packet.type);
                ws.send(JSON.stringify({
                    type: 'error',
                    message: `Unknown packet type: ${packet.type}`,
                }));
        }
    }
    catch (error) {
        log.error('Error processing WebSocket packet:', error);
        ws.send(JSON.stringify({
            type: 'error',
            message: error instanceof Error ? error.message : 'Unknown error',
        }));
    }
}
async function handleDeclareSideDialogDead(ws, packet) {
    const dialog = packet.dialog;
    const noteRaw = typeof packet.note === 'string' ? packet.note : '';
    const note = noteRaw.trim();
    if (!dialog || typeof dialog.selfId !== 'string' || typeof dialog.rootId !== 'string') {
        ws.send(JSON.stringify({
            type: 'error',
            message: 'declare_sideDialog_dead requires dialog.selfId/rootId',
        }));
        return;
    }
    if (dialog.selfId === dialog.rootId) {
        ws.send(JSON.stringify({
            type: 'error',
            message: 'declare_sideDialog_dead is allowed only for sideDialogs (selfId must differ)',
        }));
        return;
    }
    const dialogIdObj = new dialog_1.DialogID(dialog.selfId, dialog.rootId);
    const requestedStatusInput = readOptionalPersistableDialogStatus(dialog.status);
    if (requestedStatusInput.kind === 'invalid') {
        ws.send(JSON.stringify({
            type: 'error',
            message: 'declare_sideDialog_dead requires status running, completed, or archived',
        }));
        return;
    }
    const requestedStatus = requestedStatusInput.kind === 'missing' ? 'running' : requestedStatusInput.status;
    if (requestedStatus !== 'running') {
        ws.send(JSON.stringify({
            type: 'error',
            message: 'declare_sideDialog_dead is available only for running dialogs',
        }));
        return;
    }
    const latest = await persistence_1.DialogPersistence.loadDialogLatest(dialogIdObj, requestedStatus);
    if (!latest) {
        ws.send(JSON.stringify({
            type: 'error',
            message: `Dialog not found: ${dialogIdObj.valueOf()}`,
        }));
        return;
    }
    if (latest.executionMarker?.kind !== 'dead') {
        // Best-effort abort if the dialog is currently proceeding.
        await (0, dialog_display_state_1.requestInterruptDialog)(dialogIdObj, 'emergency_stop');
        await (0, dialog_display_state_1.setDialogExecutionMarker)(dialogIdObj, {
            kind: 'dead',
            reason: { kind: 'declared_by_user' },
        });
        await (0, dialog_display_state_1.setDialogDisplayState)(dialogIdObj, {
            kind: 'dead',
            reason: { kind: 'declared_by_user' },
        });
    }
    // If an askerDialog still has an active callee dispatch for this dialog, supply a system-style
    // response so the askerDialog can handle the failure reason.
    const metadata = await persistence_1.DialogPersistence.loadDialogMetadata(dialogIdObj, requestedStatus);
    if (!metadata)
        return;
    if (typeof metadata.sessionSlug === 'string' && metadata.sessionSlug.trim() !== '') {
        const rootRestored = await restoreDialogForDrive(new dialog_1.DialogID(dialogIdObj.rootId), 'running');
        if (!(rootRestored instanceof dialog_1.MainDialog)) {
            throw new Error(`Expected main dialog instance for ${dialogIdObj.rootId}`);
        }
        const removed = rootRestored.unregisterSideDialog(metadata.agentId, metadata.sessionSlug);
        if (removed) {
            await rootRestored.saveSideDialogRegistry();
        }
    }
    const assignmentFromAsker = await persistence_1.DialogPersistence.loadSideDialogAssignmentFromAsker(dialogIdObj, requestedStatus);
    const askerDialogId = assignmentFromAsker.askerDialogId;
    if (typeof askerDialogId !== 'string' || askerDialogId.trim() === '')
        return;
    const askerDialogIdObj = new dialog_1.DialogID(askerDialogId, dialogIdObj.rootId);
    const activeCalleeDispatches = await persistence_1.DialogPersistence.loadActiveCalleeDispatches(askerDialogIdObj, requestedStatus);
    const activeCalleeDispatch = activeCalleeDispatches.find((dispatch) => dispatch.calleeDialogId === dialogIdObj.selfId &&
        dispatch.callId === assignmentFromAsker.callId);
    if (!activeCalleeDispatch) {
        // Asker is not waiting on this sideDialog anymore; do not schedule a continuation drive.
        return;
    }
    const callerDialog = await restoreDialogForDrive(askerDialogIdObj, 'running');
    const responseText = formatDeclaredDeadSideDialogNotice((0, work_language_1.getWorkLanguage)(), dialogIdObj.valueOf(), assignmentFromAsker.callName);
    const responseTextWithNote = note === ''
        ? responseText
        : (0, work_language_1.getWorkLanguage)() === 'zh'
            ? `${responseText}\n\n人类用户补充：\n${note}`
            : `${responseText}\n\nHuman user note:\n${note}`;
    await (0, kernel_driver_1.supplyResponseToAskerDialog)(callerDialog, dialogIdObj, responseTextWithNote, activeCalleeDispatch.callType, assignmentFromAsker.callId, 'failed');
}
async function handleSetDiligencePush(ws, packet) {
    try {
        const { dialog, disableDiligencePush } = packet;
        if (!isRecord(dialog)) {
            ws.send(JSON.stringify({ type: 'error', message: 'dialog is required' }));
            return;
        }
        const selfId = typeof dialog.selfId === 'string' ? dialog.selfId : null;
        const rootId = typeof dialog.rootId === 'string' ? dialog.rootId : null;
        if (!selfId || !rootId) {
            ws.send(JSON.stringify({
                type: 'error',
                message: 'Invalid dialog identifiers for set_diligence_push: selfId/rootId must be strings',
            }));
            return;
        }
        if (typeof disableDiligencePush !== 'boolean') {
            ws.send(JSON.stringify({ type: 'error', message: 'disableDiligencePush must be a boolean' }));
            return;
        }
        const dialogIdObj = new dialog_1.DialogID(selfId, rootId);
        const requestedStatusInput = readOptionalPersistableDialogStatus(dialog.status);
        if (requestedStatusInput.kind === 'invalid') {
            ws.send(JSON.stringify({
                type: 'error',
                message: 'set_diligence_push requires status running, completed, or archived',
            }));
            return;
        }
        const requestedStatus = requestedStatusInput.kind === 'missing' ? 'running' : requestedStatusInput.status;
        const metadata = await persistence_1.DialogPersistence.loadDialogMetadata(dialogIdObj, requestedStatus);
        if (!metadata) {
            ws.send(JSON.stringify({
                type: 'error',
                message: `Dialog ${dialogIdObj.valueOf()} not found in ${requestedStatus}; dialog context is stale`,
            }));
            return;
        }
        const latestBefore = await persistence_1.DialogPersistence.loadDialogLatest(dialogIdObj, requestedStatus);
        const prevDisableDiligencePush = latestBefore && typeof latestBefore.disableDiligencePush === 'boolean'
            ? latestBefore.disableDiligencePush
            : false;
        await persistence_1.DialogPersistence.mutateDialogLatest(dialogIdObj, (previous) => ({
            kind: 'patch',
            patch: { disableDiligencePush },
        }), requestedStatus);
        const mainDialog = await (0, dialog_instance_registry_1.getOrRestoreMainDialog)(dialogIdObj.rootId, requestedStatus);
        if (mainDialog) {
            const loadedDialog = dialogIdObj.selfId === dialogIdObj.rootId
                ? mainDialog
                : await (0, dialog_instance_registry_1.ensureDialogLoaded)(mainDialog, dialogIdObj, requestedStatus);
            if (loadedDialog) {
                loadedDialog.disableDiligencePush = disableDiligencePush;
            }
        }
        const msg = {
            type: 'diligence_push_updated',
            dialog: { selfId: dialogIdObj.selfId, rootId: dialogIdObj.rootId, status: requestedStatus },
            disableDiligencePush,
            timestamp: (0, time_1.formatUnifiedTimestamp)(new Date()),
        };
        if (broadcastDiligencePushUpdatedMessage) {
            broadcastDiligencePushUpdatedMessage(msg);
        }
        else {
            ws.send(JSON.stringify(msg));
        }
        const shouldTriggerImmediateDiligence = requestedStatus === 'running' &&
            dialogIdObj.selfId === dialogIdObj.rootId &&
            prevDisableDiligencePush &&
            !disableDiligencePush &&
            mainDialog instanceof dialog_1.MainDialog;
        if (shouldTriggerImmediateDiligence) {
            void maybeTriggerImmediateDiligencePrompt(mainDialog);
        }
    }
    catch (error) {
        log.warn('Failed to handle set_diligence_push', error);
        ws.send(JSON.stringify({
            type: 'error',
            message: error instanceof Error ? error.message : 'Unknown error updating diligence push setting',
        }));
    }
}
function clampNonNegativeFiniteInt(value, fallback) {
    if (typeof value !== 'number' || !Number.isFinite(value))
        return fallback;
    return Math.max(0, Math.floor(value));
}
async function maybeTriggerImmediateDiligencePrompt(mainDialog) {
    try {
        if (mainDialog.disableDiligencePush) {
            return;
        }
        const gate = await (0, runtime_2.evaluateDiligenceAutoContinueGate)({
            dlg: mainDialog,
            requireIdleRunSlot: true,
        });
        if (gate.kind === 'blocked') {
            log.debug('Skipped immediate Diligence Push after enable toggle', undefined, {
                dialogId: mainDialog.id.valueOf(),
                reason: gate.reason,
            });
            return;
        }
        const team = await team_1.Team.load();
        const prepared = await (0, runtime_2.maybePrepareDiligenceAutoContinuePrompt)({
            dlg: mainDialog,
            remainingBudget: mainDialog.diligencePushRemainingBudget,
            diligencePushMax: resolveMemberDiligencePushMax(team, mainDialog.agentId),
        });
        mainDialog.diligencePushRemainingBudget = prepared.nextRemainingBudget;
        await persistence_1.DialogPersistence.mutateDialogLatest(mainDialog.id, () => ({
            kind: 'patch',
            patch: { diligencePushRemainingBudget: mainDialog.diligencePushRemainingBudget },
        }));
        if (prepared.kind !== 'disabled') {
            const maxInjectCount = Math.max(0, Math.floor(prepared.maxInjectCount));
            const remainingCount = Math.max(0, Math.floor(prepared.nextRemainingBudget));
            (0, evt_registry_1.postDialogEvent)(mainDialog, {
                type: 'diligence_budget_evt',
                maxInjectCount,
                injectedCount: maxInjectCount > 0 ? Math.max(0, maxInjectCount - remainingCount) : 0,
                remainingCount,
                disableDiligencePush: mainDialog.disableDiligencePush,
            });
        }
        if (prepared.kind === 'prompt') {
            await (0, kernel_driver_1.driveDialogStream)(mainDialog, prepared.prompt, true, {
                source: 'ws_diligence_push',
                reason: 'enable_keep_going_immediate_prompt',
            });
            return;
        }
        if (prepared.kind === 'budget_exhausted') {
            await (0, runtime_2.suspendForKeepGoingBudgetExhausted)({
                dlg: mainDialog,
                maxInjectCount: prepared.maxInjectCount,
            });
            mainDialog.diligencePushRemainingBudget = 0;
            await persistence_1.DialogPersistence.mutateDialogLatest(mainDialog.id, () => ({
                kind: 'patch',
                patch: { diligencePushRemainingBudget: mainDialog.diligencePushRemainingBudget },
            }));
        }
    }
    catch (error) {
        log.warn('Failed to trigger immediate diligence prompt after enabling keep-going', error, {
            dialogId: mainDialog.id.valueOf(),
        });
    }
}
async function handleRefillDiligencePushBudget(ws, packet) {
    const { dialog } = packet;
    if (!isRecord(dialog)) {
        ws.send(JSON.stringify({ type: 'error', message: 'dialog is required' }));
        return;
    }
    const rootId = typeof dialog.rootId === 'string' ? dialog.rootId : null;
    if (!rootId) {
        ws.send(JSON.stringify({
            type: 'error',
            message: 'Invalid dialog identifiers for refill_diligence_push_budget: rootId must be a string',
        }));
        return;
    }
    const mainDialogId = new dialog_1.DialogID(rootId);
    const requestedStatusInput = readOptionalPersistableDialogStatus(dialog.status);
    if (requestedStatusInput.kind === 'invalid') {
        ws.send(JSON.stringify({
            type: 'error',
            message: 'refill_diligence_push_budget requires status running, completed, or archived',
        }));
        return;
    }
    const requestedStatus = requestedStatusInput.kind === 'missing' ? 'running' : requestedStatusInput.status;
    const rootMeta = await persistence_1.DialogPersistence.loadDialogMetadata(mainDialogId, requestedStatus);
    if (!rootMeta) {
        ws.send(JSON.stringify({
            type: 'error',
            message: `Dialog ${mainDialogId.valueOf()} not found in ${requestedStatus}; dialog context is stale`,
        }));
        return;
    }
    const mainDialog = await (0, dialog_instance_registry_1.getOrRestoreMainDialog)(mainDialogId.rootId, requestedStatus);
    if (!mainDialog) {
        ws.send(JSON.stringify({
            type: 'error',
            message: `Main dialog ${mainDialogId.rootId} is not available for refill`,
        }));
        return;
    }
    const team = await team_1.Team.load();
    const configuredMax = normalizeDiligencePushMax(resolveMemberDiligencePushMax(team, mainDialog.agentId));
    if (configuredMax > 0) {
        mainDialog.diligencePushRemainingBudget = configuredMax;
    }
    else {
        mainDialog.diligencePushRemainingBudget =
            clampNonNegativeFiniteInt(mainDialog.diligencePushRemainingBudget, 0) + 3;
    }
    await persistence_1.DialogPersistence.mutateDialogLatest(mainDialogId, () => ({
        kind: 'patch',
        patch: { diligencePushRemainingBudget: mainDialog.diligencePushRemainingBudget },
    }), requestedStatus);
    (0, evt_registry_1.postDialogEvent)(mainDialog, {
        type: 'diligence_budget_evt',
        maxInjectCount: configuredMax > 0 ? configuredMax : 0,
        injectedCount: 0,
        remainingCount: mainDialog.diligencePushRemainingBudget,
        disableDiligencePush: mainDialog.disableDiligencePush,
    });
}
async function handleGetProblems(ws, packet) {
    if (packet.type !== 'get_problems') {
        throw new Error('Internal error: handleGetProblems called with non get_problems packet');
    }
    const _req = packet;
    ws.send(JSON.stringify((0, problems_1.createProblemsSnapshotMessage)()));
}
async function handleClearResolvedProblems(ws, packet) {
    if (packet.type !== 'clear_resolved_problems') {
        throw new Error('Internal error: handleClearResolvedProblems called with non clear_resolved_problems packet');
    }
    const _req = packet;
    const removedCount = (0, problems_1.clearResolvedProblems)();
    ws.send(JSON.stringify({
        type: 'clear_resolved_problems_result',
        removedCount,
        timestamp: (0, time_1.formatUnifiedTimestamp)(new Date()),
    }));
}
async function handleSetUiLanguage(ws, packet) {
    if (packet.type !== 'set_ui_language') {
        throw new Error('Internal error: handleSetUiLanguage called with non set_ui_language packet');
    }
    const raw = packet.uiLanguage;
    if (typeof raw !== 'string') {
        ws.send(JSON.stringify({ type: 'error', message: 'uiLanguage must be a string' }));
        return;
    }
    const parsed = (0, language_1.normalizeLanguageCode)(raw);
    if (!parsed) {
        ws.send(JSON.stringify({
            type: 'error',
            message: `Unsupported uiLanguage '${raw}'. Supported: ${language_1.supportedLanguageCodes.join(', ')}`,
        }));
        return;
    }
    wsUiLanguage.set(ws, parsed);
    const liveDialog = wsLiveDlg.get(ws);
    if (liveDialog) {
        syncDialogLanguagePreference(liveDialog, parsed, { appendSwitchNotice: true });
    }
    ws.send(JSON.stringify({ type: 'ui_language_set', uiLanguage: parsed }));
}
/**
 * Handle dialog creation via WebSocket
 */
async function handleCreateDialog(ws, packet) {
    const parsed = (0, create_dialog_contract_1.parseCreateDialogInput)(packet);
    if ('status' in parsed) {
        sendCreateDialogFailure(ws, parsed.requestId, parsed.errorCode, parsed.error);
        return;
    }
    try {
        const { requestId, agentId, taskDocPath, priming } = parsed;
        const generatedId = (0, id_1.generateDialogID)();
        // For main dialogs, self and root are the same
        const dialogId = new dialog_1.DialogID(generatedId);
        // Import Dialog and DiskFileDialogStore
        // Create DiskFileDialogStore for file-based persistence
        const dialogUI = new persistence_1.DiskFileDialogStore(dialogId);
        // Create MainDialog instance with the new store
        const dialog = new dialog_1.MainDialog(dialogUI, taskDocPath, dialogId, agentId);
        // display_dialog is intentionally read-only. Do not trigger replyTellask* recovery here:
        // merely opening a dialog must not deliver persisted replies or kick off follow-up drives.
        syncDialogLanguagePreference(dialog, resolveUserLanguageCode(ws, undefined, dialog));
        dialog_global_registry_1.globalDialogRegistry.register(dialog);
        // Setup WebSocket subscription for real-time events
        await setupWebSocketSubscription(ws, dialog);
        // Persist dialog metadata and latest.yaml (write-once pattern)
        const metadata = {
            id: dialogId.selfId,
            agentId,
            taskDocPath: taskDocPath,
            createdAt: (0, time_1.formatUnifiedTimestamp)(new Date()),
            priming: (0, priming_1.buildMainDialogPrimingMetadata)(priming),
        };
        await persistence_1.DialogPersistence.saveDialogMetadata(new dialog_1.DialogID(dialogId.selfId), metadata);
        const team = await team_1.Team.load();
        const diligencePushMax = normalizeDiligencePushMax(resolveMemberDiligencePushMax(team, agentId));
        const defaultDisableDiligencePush = false;
        dialog.disableDiligencePush = defaultDisableDiligencePush;
        dialog.diligencePushRemainingBudget = diligencePushMax > 0 ? diligencePushMax : 0;
        // Initialize latest.yaml via the mutation API (write-back will flush).
        await persistence_1.DialogPersistence.mutateDialogLatest(new dialog_1.DialogID(dialogId.selfId), () => ({
            kind: 'replace',
            next: {
                currentCourse: 1,
                lastModified: (0, time_1.formatUnifiedTimestamp)(new Date()),
                status: 'active',
                messageCount: 0,
                functionCallCount: 0,
                sideDialogCount: 0,
                nextStep: (0, dialog_latest_state_1.createEmptyDialogNextStepState)(),
                tellaskCalls: (0, dialog_latest_state_1.createEmptyDialogTellaskCallState)(),
                tellaskResults: (0, dialog_latest_state_1.createEmptyDialogTellaskResultState)(),
                displayState: { kind: 'idle_waiting_user' },
                disableDiligencePush: defaultDisableDiligencePush,
                diligencePushRemainingBudget: dialog.diligencePushRemainingBudget,
            },
        }));
        if (priming && priming.scriptRefs.length > 0) {
            await (0, priming_1.applyPrimingScriptsToDialog)({
                dialog,
                agentId,
                status: 'running',
                priming,
            });
        }
        // Send dialog_ready with full info so frontend can track the active dialog
        const response = {
            type: 'dialog_ready',
            dialog: {
                selfId: dialogId.selfId,
                rootId: dialogId.rootId,
            },
            agentId,
            taskDocPath: taskDocPath,
            disableDiligencePush: defaultDisableDiligencePush,
            diligencePushMax,
            diligencePushRemainingBudget: dialog.diligencePushRemainingBudget,
        };
        const createResult = {
            kind: 'success',
            requestId,
            selfId: dialogId.selfId,
            rootId: dialogId.rootId,
            agentId,
            taskDocPath,
        };
        ws.send(JSON.stringify(createResult));
        ws.send(JSON.stringify(response));
        broadcastDialogsIndexMessage?.({
            type: 'dialogs_created',
            scope: { kind: 'root', rootId: dialogId.selfId },
            status: 'running',
            createdRootIds: [dialogId.selfId],
            timestamp: (0, time_1.formatUnifiedTimestamp)(new Date()),
        });
    }
    catch (error) {
        log.warn('Failed to create dialog', undefined, error);
        const message = error instanceof Error ? error.message : 'Unknown error creating dialog';
        sendCreateDialogFailure(ws, parsed.requestId, (0, create_dialog_contract_1.normalizeCreateDialogErrorCode)(getErrorCode(error)), message);
    }
}
/**
 * Handle dialog retrieval via WebSocket
 */
async function handleDisplayDialog(ws, packet) {
    try {
        const { dialog: dialogIdent } = packet;
        if (!dialogIdent) {
            throw new Error('dialog is required');
        }
        // Extract dialog ID from DialogIdent
        let dialogId = dialogIdent.selfId;
        let mainDialogId = dialogIdent.rootId;
        // Handle case where dialogIdent properties might be objects instead of strings
        if (typeof dialogId !== 'string' || typeof mainDialogId !== 'string') {
            ws.send(JSON.stringify({
                type: 'error',
                message: 'Invalid dialog identifiers for display_dialog: selfId/rootId must be strings',
            }));
            return;
        }
        // IMPORTANT: cancel any existing event forwarder before emitting restoration events.
        // Otherwise, the same client can receive overlapping "replay" and "live" streams,
        // which surfaces as duplicate generation lifecycle events on the frontend.
        const existing = wsLiveDlg.get(ws);
        if (existing) {
            const existingId = existing.id;
            const isSameDialog = existingId.selfId === dialogId && existingId.rootId === mainDialogId;
            if (isSameDialog) {
                log.debug('display_dialog: refreshing the same dialog; cancelling existing subscription to prevent duplicate stream events', undefined, { dialogId, mainDialogId });
            }
            else {
                log.debug('display_dialog: switching dialogs; cancelling previous subscription', undefined, {
                    previousDialogId: existingId.valueOf(),
                    nextDialogId: new dialog_1.DialogID(dialogId, mainDialogId).valueOf(),
                });
            }
            cleanupWsClient(ws);
        }
        const dialogIdObj = new dialog_1.DialogID(dialogId, mainDialogId);
        const requestedStatusInput = readOptionalPersistableDialogStatus(dialogIdent.status);
        if (requestedStatusInput.kind === 'invalid') {
            ws.send(JSON.stringify({
                type: 'error',
                message: 'display_dialog requires status running, completed, or archived',
            }));
            return;
        }
        const preferredStatus = requestedStatusInput.kind === 'missing' ? 'running' : requestedStatusInput.status;
        const restoredForDisplay = await restoreDialogForDisplay(dialogIdObj, preferredStatus);
        if (!restoredForDisplay) {
            throw new Error(`Dialog ${dialogIdObj.valueOf()} not found in running/completed/archived; dialog context is stale`);
        }
        const { dialogState, metadata, status: requestedStatus } = restoredForDisplay;
        if (requestedStatus !== preferredStatus) {
            log.debug('display_dialog: resolved stale requested status', undefined, {
                dialogId: dialogIdObj.valueOf(),
                requestedStatus: preferredStatus,
                resolvedStatus: requestedStatus,
            });
        }
        const rootPrimingConfig = dialogIdObj.selfId === dialogIdObj.rootId ? (0, priming_1.getMainDialogPrimingConfig)(metadata) : undefined;
        const showPrimingEventsInUi = rootPrimingConfig?.showInUi !== false;
        if (packet.course !== undefined && (!Number.isInteger(packet.course) || packet.course <= 0)) {
            ws.send(JSON.stringify({
                type: 'error',
                message: 'display_dialog course must be a positive integer when provided',
            }));
            return;
        }
        const latestCourse = (await persistence_1.DialogPersistence.getCurrentCourseNumber(dialogIdObj, requestedStatus)) ||
            (dialogState.currentCourse ?? 1);
        const decidedCourse = packet.course ?? latestCourse;
        if (decidedCourse > latestCourse) {
            ws.send(JSON.stringify({
                type: 'error',
                message: `display_dialog course ${String(decidedCourse)} exceeds latest course ${String(latestCourse)}`,
            }));
            return;
        }
        const enableLive = requestedStatus === 'running';
        const mainDialog = await (0, dialog_instance_registry_1.getOrRestoreMainDialog)(dialogIdObj.rootId, requestedStatus);
        if (!mainDialog) {
            throw new Error('Main dialog not found');
        }
        if (enableLive) {
            dialog_global_registry_1.globalDialogRegistry.register(mainDialog);
        }
        let dialog;
        if (dialogIdObj.selfId === dialogIdObj.rootId) {
            dialog = mainDialog;
        }
        else {
            const loaded = await (0, dialog_instance_registry_1.ensureDialogLoaded)(mainDialog, dialogIdObj, requestedStatus);
            if (!loaded) {
                throw new Error('Dialog not found');
            }
            dialog = loaded;
        }
        syncDialogLanguagePreference(dialog, resolveUserLanguageCode(ws, undefined, dialog));
        // Subscribe BEFORE sending restoration events.
        // This avoids a race where new persisted events (e.g., Agent Priming replay) are emitted
        // between the restoration snapshot read and the subscription setup.
        // Live generation is still gated by dialog.status ('running') in drive handlers.
        await setupWebSocketSubscription(ws, dialog);
        // Send dialog events directly to requesting WebSocket only.
        // This bypasses PubChan to ensure only the requesting session receives restoration events.
        // Pass decidedCourse explicitly since dialog.currentCourse defaults to 1 for new Dialog objects.
        try {
            const dialogStore = dialog.dlgStore;
            if (dialogStore instanceof persistence_1.DiskFileDialogStore) {
                await dialogStore.sendDialogEventsDirectly(ws, dialog, decidedCourse, latestCourse, requestedStatus, { showPrimingEventsInUi });
            }
            else {
                throw new Error('Unexpected dialog store type for sendDialogEventsDirectly');
            }
        }
        catch (err) {
            log.warn(`Failed to send dialog events directly for ${dialogId}:`, err);
        }
        // Send dialog_ready with full info so frontend knows the current dialog ID
        const team = await team_1.Team.load();
        const diligencePushMax = normalizeDiligencePushMax(resolveMemberDiligencePushMax(team, metadata.agentId));
        const displayedLatest = await persistence_1.DialogPersistence.loadDialogLatest(dialogIdObj, requestedStatus);
        const defaultDisableDiligencePush = false;
        const persistedDisableDiligencePush = displayedLatest && typeof displayedLatest.disableDiligencePush === 'boolean'
            ? displayedLatest.disableDiligencePush
            : defaultDisableDiligencePush;
        const effectiveDisableDiligencePush = persistedDisableDiligencePush;
        if (dialogIdObj.selfId === dialogIdObj.rootId) {
            mainDialog.disableDiligencePush = effectiveDisableDiligencePush;
        }
        else {
            const loaded = await (0, dialog_instance_registry_1.ensureDialogLoaded)(mainDialog, dialogIdObj, requestedStatus);
            if (loaded) {
                loaded.disableDiligencePush = effectiveDisableDiligencePush;
            }
        }
        let derivedAskerDialogId;
        let assignmentFromAsker;
        if (dialogIdObj.selfId !== dialogIdObj.rootId) {
            assignmentFromAsker = await persistence_1.DialogPersistence.loadSideDialogAssignmentFromAsker(dialogIdObj, requestedStatus);
            derivedAskerDialogId = assignmentFromAsker ? assignmentFromAsker.askerDialogId.trim() : '';
        }
        if (dialogIdObj.selfId !== dialogIdObj.rootId && !derivedAskerDialogId) {
            const error = new Error(`dialog_ready invariant violation: missing assignmentFromAsker.askerDialogId ` +
                `(rootId=${dialogIdObj.rootId}, selfId=${dialogIdObj.selfId}, status=${requestedStatus})`);
            log.error('dialog_ready invariant violation: missing assignmentFromAsker.askerDialogId', error, {
                rootId: dialogIdObj.rootId,
                selfId: dialogIdObj.selfId,
                status: requestedStatus,
            });
            throw error;
        }
        const dialogReadyResponse = {
            type: 'dialog_ready',
            dialog: {
                selfId: dialogId,
                rootId: mainDialogId,
                status: requestedStatus,
            },
            agentId: metadata.agentId,
            taskDocPath: metadata.taskDocPath,
            askerDialogId: derivedAskerDialogId,
            sessionSlug: metadata.sessionSlug,
            assignmentFromAsker,
            disableDiligencePush: effectiveDisableDiligencePush,
            diligencePushMax,
            diligencePushRemainingBudget: clampNonNegativeFiniteInt(dialogIdObj.selfId === dialogIdObj.rootId ? mainDialog.diligencePushRemainingBudget : 0, diligencePushMax > 0 ? diligencePushMax : 0),
        };
        ws.send(JSON.stringify(dialogReadyResponse));
        // Running dialogs expose a persisted display-state snapshot for viewport controls.
        if (requestedStatus === 'running') {
            try {
                const latest = await persistence_1.DialogPersistence.loadDialogLatest(dialogIdObj, requestedStatus);
                const displayState = latest?.displayState ?? { kind: 'idle_waiting_user' };
                // `display_dialog` is a read/navigation action. Use persisted lastModified as timestamp
                // so the frontend list does not reorder as if there were new activity "now".
                const displayStateEvt = {
                    dialog: {
                        selfId: dialogIdObj.selfId,
                        rootId: dialogIdObj.rootId,
                    },
                    timestamp: latest?.lastModified ?? (0, time_1.formatUnifiedTimestamp)(new Date()),
                    type: 'dlg_display_state_evt',
                    displayState,
                };
                ws.send(JSON.stringify(displayStateEvt));
            }
            catch (err) {
                log.warn(`Failed to send dlg_display_state_evt for ${dialogIdObj.valueOf()}:`, err);
            }
        }
        // Emit one Q4H snapshot to ensure frontend has current global questions state.
        // Do NOT replay per-question `new_q4h_asked` events here: those are real-time
        // incremental events and replaying them on display refresh can create duplicate
        // delivery paths and blur event semantics.
        try {
            const allQuestions = await persistence_1.DialogPersistence.loadAllQ4HState();
            const allAnswers = await persistence_1.DialogPersistence.loadAllA2HState();
            const response = {
                type: 'q4h_state_response',
                questions: allQuestions.map((q) => ({
                    id: q.id,
                    selfId: q.selfId,
                    rootId: q.rootId,
                    agentId: q.agentId,
                    taskDocPath: q.taskDocPath,
                    tellaskContent: q.tellaskContent,
                    askedAt: q.askedAt,
                    callId: q.callId,
                    callSiteRef: q.callSiteRef,
                })),
                answers: allAnswers.map((a) => ({
                    id: a.id,
                    selfId: a.selfId,
                    rootId: a.rootId,
                    agentId: a.agentId,
                    taskDocPath: a.taskDocPath,
                    content: a.content,
                    answeredAt: a.answeredAt,
                    answerRef: a.answerRef,
                })),
            };
            ws.send(JSON.stringify(response));
        }
        catch (err) {
            log.warn(`Failed to emit Q4H state for ${dialogIdObj}:`, err);
        }
        // Proactively emit reminders for the newly active dialog.
        // Historical dialogs are display-only: do not run owner reconciliation or write reminder files
        // just because an old browser tab reconnected to a completed/archived dialog URL.
        try {
            if (requestedStatus === 'running') {
                await syncPendingTellaskReminderBestEffort(dialog, 'handleDisplayDialog');
                await dialog.processReminderUpdates();
            }
            else {
                await dialog.emitReminderSnapshot();
            }
        }
        catch (err) {
            log.warn(`Failed to emit proactive reminders for ${dialogIdObj}:`, err);
        }
    }
    catch (error) {
        log.warn('Failed to handle display_dialog', error);
    }
}
/**
 * Handle Q4H state request via WebSocket
 * Fetches Q4H questions from ALL running dialogs for global display
 */
async function handleGetQ4HState(ws, _packet) {
    try {
        // Load Q4H from all running dialogs
        const allQuestions = await persistence_1.DialogPersistence.loadAllQ4HState();
        const allAnswers = await persistence_1.DialogPersistence.loadAllA2HState();
        // Transform to wire `Q4HStateResponse` question entries.
        // `selfId` + `rootId` uniquely identify the originating dialog (including sideDialogs).
        const questions = allQuestions.map((q) => ({
            id: q.id,
            selfId: q.selfId,
            rootId: q.rootId,
            agentId: q.agentId,
            taskDocPath: q.taskDocPath,
            tellaskContent: q.tellaskContent,
            askedAt: q.askedAt,
            callId: q.callId,
            callSiteRef: q.callSiteRef,
        }));
        const answers = allAnswers.map((a) => ({
            id: a.id,
            selfId: a.selfId,
            rootId: a.rootId,
            agentId: a.agentId,
            taskDocPath: a.taskDocPath,
            content: a.content,
            answeredAt: a.answeredAt,
            answerRef: a.answerRef,
        }));
        // Send single response packet with all questions (not PubChan events)
        const response = {
            type: 'q4h_state_response',
            questions,
            answers,
        };
        ws.send(JSON.stringify(response));
    }
    catch (error) {
        log.warn('Failed to handle get_q4h_state', error);
        ws.send(JSON.stringify({
            type: 'error',
            message: error instanceof Error ? error.message : 'Unknown error getting Q4H state',
        }));
    }
}
async function handleAckA2H(ws, packet) {
    try {
        const dialogIdent = packet.dialog;
        const answerId = packet.answerId;
        if (!dialogIdent || typeof answerId !== 'string' || answerId.trim() === '') {
            ws.send(JSON.stringify({ type: 'error', message: 'dialog and answerId are required for A2H Ack' }));
            return;
        }
        if (typeof dialogIdent.selfId !== 'string' || typeof dialogIdent.rootId !== 'string') {
            ws.send(JSON.stringify({
                type: 'error',
                message: 'Invalid dialog identifiers for A2H Ack: selfId/rootId must be strings',
            }));
            return;
        }
        const dialogId = new dialog_1.DialogID(dialogIdent.selfId, dialogIdent.rootId);
        const requestedStatusInput = readOptionalPersistableDialogStatus(dialogIdent.status);
        if (requestedStatusInput.kind === 'invalid') {
            ws.send(JSON.stringify({
                type: 'error',
                message: 'A2H Ack requires status running, completed, or archived when status is provided',
            }));
            return;
        }
        const candidateStatuses = requestedStatusInput.kind === 'value'
            ? orderedDialogStatusCandidates(requestedStatusInput.status)
            : [...DIALOG_STATUS_RESOLUTION_CANDIDATES];
        let removed = null;
        let removedStatus = null;
        for (const status of candidateStatuses) {
            const attempt = await persistence_1.DialogPersistence.acknowledgeAnswerToHumanState(dialogId, answerId, status);
            if (attempt.found) {
                removed = attempt;
                removedStatus = status;
                break;
            }
        }
        if (removed === null || !removed.found) {
            ws.send(JSON.stringify({
                type: 'error',
                message: `A2H answer ${answerId} not found in dialog ${dialogIdent.selfId}`,
            }));
            return;
        }
        if (removedStatus === null) {
            throw new Error(`A2H Ack invariant violation: removed answer without status ${answerId}`);
        }
        const event = {
            type: 'a2h_acknowledged',
            answerId,
            selfId: dialogIdent.selfId,
            rootId: dialogIdent.rootId,
        };
        (0, evt_registry_1.postDialogEventById)(dialogId, event);
    }
    catch (error) {
        log.error('Error processing A2H Ack:', error);
        ws.send(JSON.stringify({
            type: 'error',
            message: `Failed to acknowledge A2H answer: ${error instanceof Error ? error.message : 'Unknown error'}`,
        }));
    }
}
async function handleDisplayReminders(ws, packet) {
    try {
        const live = wsLiveDlg.get(ws);
        if (!live) {
            log.warn('No live dialog found for display_reminders');
            return;
        }
        if (live.id.selfId !== packet.dialog.selfId) {
            log.warn(`Dialog ${packet.dialog} for reminders is not current live (live dialog is ${live.id})`);
            return;
        }
        await syncPendingTellaskReminderBestEffort(live, 'handleDisplayReminders');
        await live.processReminderUpdates();
    }
    catch (error) {
        log.warn('Failed to display reminders', error);
    }
}
async function handleDisplayCourse(ws, packet) {
    try {
        const { dialog, course } = packet;
        if (!dialog || typeof course !== 'number') {
            throw new Error('dialog and course are required');
        }
        if (!Number.isInteger(course) || course <= 0) {
            ws.send(JSON.stringify({
                type: 'error',
                message: 'display_course course must be a positive integer',
            }));
            return;
        }
        // Extract dialog ID from DialogIdent
        let dialogIdStr = dialog.selfId;
        let mainDialogIdStr = dialog.rootId;
        // Handle case where dialog properties might be objects instead of strings
        if (typeof dialogIdStr !== 'string' || typeof mainDialogIdStr !== 'string') {
            ws.send(JSON.stringify({
                type: 'error',
                message: 'Invalid dialog identifiers for display_course: selfId/rootId must be strings',
            }));
            return;
        }
        const dialogId = new dialog_1.DialogID(dialogIdStr, mainDialogIdStr);
        try {
            const requestedStatusInput = readOptionalPersistableDialogStatus(dialog.status);
            if (requestedStatusInput.kind === 'invalid') {
                ws.send(JSON.stringify({
                    type: 'error',
                    message: 'display_course requires status running, completed, or archived',
                }));
                return;
            }
            const preferredStatus = requestedStatusInput.kind === 'missing' ? 'running' : requestedStatusInput.status;
            const restoredForDisplay = await restoreDialogForDisplay(dialogId, preferredStatus);
            if (!restoredForDisplay) {
                log.warn('Metadata not found for display_course', undefined, {
                    dialogId: dialogId.selfId,
                    requestedStatus: preferredStatus,
                });
                return;
            }
            const { status: requestedStatus } = restoredForDisplay;
            if (requestedStatus !== preferredStatus) {
                log.debug('display_course: resolved stale requested status', undefined, {
                    dialogId: dialogId.valueOf(),
                    requestedStatus: preferredStatus,
                    resolvedStatus: requestedStatus,
                });
            }
            const totalCourses = (await persistence_1.DialogPersistence.getCurrentCourseNumber(dialogId, requestedStatus)) ||
                restoredForDisplay.dialogState.currentCourse ||
                course;
            if (course > totalCourses) {
                ws.send(JSON.stringify({
                    type: 'error',
                    message: `display_course course ${String(course)} exceeds latest course ${String(totalCourses)}`,
                }));
                return;
            }
            const mainDialog = await (0, dialog_instance_registry_1.getOrRestoreMainDialog)(dialogId.rootId, requestedStatus);
            if (!mainDialog)
                return;
            const restoredDialog = dialogId.selfId === dialogId.rootId
                ? mainDialog
                : await (0, dialog_instance_registry_1.ensureDialogLoaded)(mainDialog, dialogId, requestedStatus);
            if (!restoredDialog)
                return;
            const store = restoredDialog.dlgStore;
            if (!(store instanceof persistence_1.DiskFileDialogStore)) {
                throw new Error('Unexpected dialog store type for display_course');
            }
            // Send the requested course's persisted events directly to this WebSocket.
            // This is a UI navigation operation; do not emit via PubChan.
            await store.sendDialogEventsDirectly(ws, restoredDialog, course, totalCourses, requestedStatus);
        }
        catch (err) {
            log.warn('Failed to send dialog events for display_course', err);
        }
    }
    catch (error) {
        log.warn('Failed to handle display_course', error);
    }
}
/**
 * Handle message sending via WebSocket
 */
async function handleUserMsg2Dlg(ws, packet) {
    try {
        const { dialog: dialogIdent, content, msgId } = packet;
        const attachments = parseUserImageAttachments(packet.attachments);
        const rawUserLanguageCode = packet
            .userLanguageCode;
        // Basic validation
        if (!dialogIdent || !content || !msgId) {
            ws.send(JSON.stringify({
                type: 'error',
                message: 'dialog, content, and msgId are required',
            }));
            return;
        }
        // Extract dialog ID from DialogIdent
        const dialogId = dialogIdent.selfId;
        const mainDialogId = dialogIdent.rootId;
        // Validate dialog identifiers
        if (typeof dialogId !== 'string' || typeof mainDialogId !== 'string') {
            ws.send(JSON.stringify({
                type: 'error',
                message: 'Invalid dialog identifiers for drive_dlg_by_user_msg: selfId/rootId must be strings',
            }));
            return;
        }
        const dialogIdObj = new dialog_1.DialogID(dialogId, mainDialogId);
        const latest = await persistence_1.DialogPersistence.loadDialogLatest(dialogIdObj, 'running');
        if (latest?.executionMarker?.kind === 'dead') {
            ws.send(JSON.stringify({
                type: 'error',
                message: 'Dialog is dead and cannot be driven.',
            }));
            return;
        }
        let effectivePrompt = {
            content,
            msgId,
            grammar: 'markdown',
        };
        const preparedAttachments = prepareUserImageAttachments(attachments);
        // If the dialog is already active for this WebSocket, runnable (status === 'running'),
        // and has an event forwarder (subChan),
        // drive it directly to preserve in-memory state (active callee dispatches, teammate tellask tracking, etc).
        //
        // IMPORTANT: do not drive a view-only dialog instance here. When users browse a completed/archived
        // dialog, handleDisplayDialog restores it with dialog.status set to completed/archived. If that
        // dialog is later revived to running by another client, the UI may re-enable input without
        // re-issuing display_dialog. In that case, we must restore from running rather than driving the
        // cached view-only dialog (stale state, wrong hydration, etc).
        const existingDialog = wsLiveDlg.get(ws);
        const existingSub = wsSub.get(ws);
        if (existingDialog &&
            existingDialog.id.selfId === dialogId &&
            existingDialog.id.rootId === mainDialogId &&
            existingDialog.status === 'running' &&
            existingSub &&
            existingSub.dialogKey === existingDialog.id.valueOf()) {
            effectivePrompt = {
                ...effectivePrompt,
                userLanguageCode: resolveUserLanguageCode(ws, rawUserLanguageCode, existingDialog),
            };
            const contentItems = await persistPreparedUserImageAttachments({
                dialog: existingDialog,
                msgId: effectivePrompt.msgId,
                attachments: preparedAttachments,
            });
            effectivePrompt = wrapCriticalUserInterjectionPromptAtIngress(existingDialog, effectivePrompt);
            const queuedAtBoundary = await queueUserSupplementAtGenerationBoundary(existingDialog, {
                ...effectivePrompt,
                contentItems,
            });
            if (queuedAtBoundary) {
                return;
            }
            await (0, kernel_driver_1.driveDialogStream)(existingDialog, {
                content: effectivePrompt.content,
                ...(contentItems === undefined ? {} : { contentItems }),
                msgId: effectivePrompt.msgId,
                grammar: effectivePrompt.grammar,
                userLanguageCode: effectivePrompt.userLanguageCode,
                origin: 'user',
            }, true, {
                source: 'ws_user_message',
                reason: 'drive_dlg_by_user_msg',
            });
            return;
        }
        // Dialog not found in wsLiveDlg - drive using the canonical root/sideDialog instances.
        // This supports driving sideDialogs and cross-client revival without creating duplicate dialog objects.
        try {
            const mainDialog = await (0, dialog_instance_registry_1.getOrRestoreMainDialog)(dialogIdObj.rootId, 'running');
            if (!mainDialog) {
                ws.send(JSON.stringify({ type: 'error', message: `Dialog ${dialogId} not found` }));
                return;
            }
            dialog_global_registry_1.globalDialogRegistry.register(mainDialog);
            const dialog = dialogIdObj.selfId === dialogIdObj.rootId
                ? mainDialog
                : await (0, dialog_instance_registry_1.ensureDialogLoaded)(mainDialog, dialogIdObj, 'running');
            if (!dialog) {
                ws.send(JSON.stringify({ type: 'error', message: `Dialog ${dialogId} not found` }));
                return;
            }
            await setupWebSocketSubscription(ws, dialog);
            effectivePrompt = {
                ...effectivePrompt,
                userLanguageCode: resolveUserLanguageCode(ws, rawUserLanguageCode, dialog),
            };
            const contentItems = await persistPreparedUserImageAttachments({
                dialog,
                msgId: effectivePrompt.msgId,
                attachments: preparedAttachments,
            });
            effectivePrompt = wrapCriticalUserInterjectionPromptAtIngress(dialog, effectivePrompt);
            const queuedAtBoundary = await queueUserSupplementAtGenerationBoundary(dialog, {
                ...effectivePrompt,
                contentItems,
            });
            if (queuedAtBoundary) {
                return;
            }
            await (0, kernel_driver_1.driveDialogStream)(dialog, {
                content: effectivePrompt.content,
                ...(contentItems === undefined ? {} : { contentItems }),
                msgId: effectivePrompt.msgId,
                grammar: effectivePrompt.grammar,
                userLanguageCode: effectivePrompt.userLanguageCode,
                origin: 'user',
            }, true, {
                source: 'ws_user_message',
                reason: 'drive_dlg_by_user_msg',
            });
            return;
        }
        catch (restoreError) {
            log.warn('Failed to restore dialog for message:', restoreError);
            ws.send(JSON.stringify({
                type: 'error',
                message: `Cannot send message to dialog ${dialogId}: dialog is not the currently active dialog and could not be restored`,
            }));
            return;
        }
    }
    catch (error) {
        // Log the error at warning level in the backend console
        log.warn(`Failed to drive dialog ${packet?.dialog?.selfId} with user message: ${error instanceof Error ? error.message : 'Unknown error'}`, error);
        // Send error response to client
        ws.send(JSON.stringify({
            type: 'error',
            message: `Failed to process message: ${error instanceof Error ? error.message : 'Unknown error'}`,
        }));
    }
}
async function restoreDialogForDrive(dialogIdObj, status) {
    const mainDialog = await (0, dialog_instance_registry_1.getOrRestoreMainDialog)(dialogIdObj.rootId, status);
    if (!mainDialog) {
        throw new Error(`Dialog ${dialogIdObj.valueOf()} not found`);
    }
    dialog_global_registry_1.globalDialogRegistry.register(mainDialog);
    // This helper is intentionally for business operations that will mutate or continue execution
    // immediately after restore (for example resume_dialog, resume_all, or dead-sideDialog recovery).
    // Because those operations are execution-oriented, we repair pending replyTellask* delivery
    // before handing the dialog back to the tellasker.
    if (dialogIdObj.selfId === dialogIdObj.rootId) {
        await (0, reply_delivery_recovery_1.recoverPendingReplyDeliveryForDialog)(mainDialog);
        return mainDialog;
    }
    const sub = await (0, dialog_instance_registry_1.ensureDialogLoaded)(mainDialog, dialogIdObj, status);
    if (!sub) {
        throw new Error(`Dialog ${dialogIdObj.valueOf()} not found`);
    }
    await (0, reply_delivery_recovery_1.recoverPendingReplyDeliveryForDialog)(sub);
    return sub;
}
async function handleInterruptDialog(ws, packet) {
    if (packet.type !== 'interrupt_dialog') {
        throw new Error('Internal error: handleInterruptDialog called with non interrupt_dialog packet');
    }
    const dialog = packet.dialog;
    if (!dialog || typeof dialog.selfId !== 'string' || typeof dialog.rootId !== 'string') {
        ws.send(JSON.stringify({ type: 'error', message: 'interrupt_dialog requires dialog.selfId/rootId' }));
        return;
    }
    const dialogIdObj = new dialog_1.DialogID(dialog.selfId, dialog.rootId);
    const res = await (0, dialog_display_state_1.requestInterruptDialog)(dialogIdObj, 'user_stop');
    if (!res.applied) {
        // Stop should be idempotent: a double-click (or concurrent stop) must not surface as an error.
        // If the dialog is already not proceeding, treat as a no-op.
        return;
    }
}
async function handleEmergencyStop(ws, packet) {
    if (packet.type !== 'emergency_stop') {
        throw new Error('Internal error: handleEmergencyStop called with non emergency_stop packet');
    }
    await (0, dialog_display_state_1.requestEmergencyStopAll)();
    emitRunControlRefresh('emergency_stop');
}
async function handleResumeDialog(ws, packet) {
    if (packet.type !== 'resume_dialog') {
        throw new Error('Internal error: handleResumeDialog called with non resume_dialog packet');
    }
    const dialog = packet.dialog;
    if (!dialog || typeof dialog.selfId !== 'string' || typeof dialog.rootId !== 'string') {
        ws.send(JSON.stringify({ type: 'error', message: 'resume_dialog requires dialog.selfId/rootId' }));
        return;
    }
    const dialogIdObj = new dialog_1.DialogID(dialog.selfId, dialog.rootId);
    const latest = await (0, dialog_display_state_1.refreshRunControlProjectionFromPersistenceFacts)(dialogIdObj, 'resume_dialog');
    // WARNING:
    // Passing this gate only means "a Continue attempt is allowed". It does not mean the dialog is
    // guaranteed to re-enter proceeding immediately; the resumed drive may still land in true
    // `blocked` after re-reading fresh persistence facts.
    if (!(0, dialog_display_state_1.isDialogLatestResumable)(latest)) {
        const ineligible = buildResumeIneligibleMessage(latest);
        log.warn('resume_dialog rejected after fresh fact scan', undefined, {
            dialogId: dialogIdObj.valueOf(),
            displayState: latest?.displayState ?? null,
            executionMarker: latest?.executionMarker ?? null,
            resumeNotEligibleReason: ineligible.reason,
        });
        ws.send(JSON.stringify({
            type: 'error',
            code: 'resume_dialog_not_eligible',
            resumeNotEligibleReason: ineligible.reason,
            message: ineligible.message,
        }));
        emitRunControlRefresh('resume_dialog');
        return;
    }
    const restored = await restoreDialogForDrive(dialogIdObj, 'running');
    await (0, kernel_driver_1.driveDialogStream)(restored, undefined, true, {
        allowResumeFromInterrupted: true,
        source: 'ws_resume_dialog',
        reason: 'resume_dialog',
    });
    emitRunControlRefresh('resume_dialog');
}
async function handleResumeAll(ws, packet) {
    if (packet.type !== 'resume_all') {
        throw new Error('Internal error: handleResumeAll called with non resume_all packet');
    }
    const dialogIds = await persistence_1.DialogPersistence.listAllDialogIds('running');
    let resumableCount = 0;
    for (const id of dialogIds) {
        try {
            // listAllDialogIds() only gives candidate IDs. A malformed dialog can still quarantine itself
            // during lazy latest lookup here without preventing resume-all for the rest.
            const latest = await (0, dialog_display_state_1.refreshRunControlProjectionFromPersistenceFacts)(id, 'resume_all');
            if (!(0, dialog_display_state_1.isDialogLatestResumable)(latest))
                continue;
            resumableCount += 1;
            void (async () => {
                try {
                    const dlg = await restoreDialogForDrive(id, 'running');
                    await (0, kernel_driver_1.driveDialogStream)(dlg, undefined, true, {
                        allowResumeFromInterrupted: true,
                        source: 'ws_resume_all',
                        reason: 'resume_all',
                    });
                }
                catch (err) {
                    log.warn('resume_all: failed to resume dialog', err, { dialogId: id.valueOf() });
                }
            })();
        }
        catch (error) {
            if (!(0, persistence_errors_1.findDomindsPersistenceFileError)(error)) {
                throw error;
            }
            log.warn('resume_all: skipping malformed dialog during latest lookup', error, {
                dialogId: id.valueOf(),
            });
        }
    }
    if (resumableCount === 0) {
        ws.send(JSON.stringify({
            type: 'error',
            code: 'resume_all_not_eligible',
            message: 'No dialogs are currently eligible for resumption.',
        }));
    }
    emitRunControlRefresh('resume_all');
}
/**
 * Receive a human reply for a Q4H question.
 * Validates questionId, clears q4h.yaml entry, records askHuman result/carryover,
 * and optionally queues a continuation drive input carrying the answered callId.
 *
 * Important: the human answer itself is canonicalized as askHuman tellask result/carryover first.
 * For cross-course Q4H answers, the carryover is the canonical latest-course context; it is not a
 * tool-result pair for the older-course askHuman call.
 * The follow-up drive input here is only control-flow glue for the resumed round, not a separate
 * persisted "user prompt" business fact.
 */
async function handleReceiveHumanReply(ws, packet) {
    try {
        const { dialog: dialogIdent, content, msgId, questionId, continuationType } = packet;
        const attachments = parseUserImageAttachments(packet.attachments);
        const userLanguageCode = resolveUserLanguageCode(ws, packet.userLanguageCode);
        // Basic validation
        if (!dialogIdent || !content || !msgId || !questionId) {
            ws.send(JSON.stringify({
                type: 'error',
                message: 'dialog, content, msgId, and questionId are required',
            }));
            return;
        }
        // Extract dialog ID from DialogIdent
        const dialogId = dialogIdent.selfId;
        const mainDialogId = dialogIdent.rootId;
        // Validate dialog identifiers
        if (typeof dialogId !== 'string' || typeof mainDialogId !== 'string') {
            ws.send(JSON.stringify({
                type: 'error',
                message: 'Invalid dialog identifiers for receiveHumanReply: selfId/rootId must be strings',
            }));
            return;
        }
        const dialogIdObj = new dialog_1.DialogID(dialogId, mainDialogId);
        const latest = await persistence_1.DialogPersistence.loadDialogLatest(dialogIdObj, 'running');
        if (latest?.executionMarker?.kind === 'dead') {
            ws.send(JSON.stringify({
                type: 'error',
                message: 'Dialog is dead and cannot be driven.',
            }));
            return;
        }
        const effectivePrompt = {
            content,
            msgId,
            grammar: 'markdown',
            userLanguageCode,
        };
        const preparedAttachments = prepareUserImageAttachments(attachments);
        // Restore the canonical dialog instances (main dialog + sideDialogs) to avoid duplicates.
        const mainDialog = await (0, dialog_instance_registry_1.getOrRestoreMainDialog)(dialogIdObj.rootId, 'running');
        if (!mainDialog) {
            ws.send(JSON.stringify({ type: 'error', message: `Dialog ${dialogId} not found` }));
            return;
        }
        dialog_global_registry_1.globalDialogRegistry.register(mainDialog);
        const dialog = dialogIdObj.selfId === dialogIdObj.rootId
            ? mainDialog
            : await (0, dialog_instance_registry_1.ensureDialogLoaded)(mainDialog, dialogIdObj, 'running');
        if (!dialog) {
            ws.send(JSON.stringify({ type: 'error', message: `Dialog ${dialogId} not found` }));
            return;
        }
        // Ensure the requesting WebSocket receives q4h_answered and subsequent resume stream events.
        await setupWebSocketSubscription(ws, dialog);
        const removed = await persistence_1.DialogPersistence.removeQuestion4HumanState(dialogIdObj, questionId);
        if (!removed.found) {
            ws.send(JSON.stringify({
                type: 'error',
                message: `Question ${questionId} not found in dialog ${dialogId}`,
            }));
            return;
        }
        const removedQuestion = removed.removedQuestion;
        if (!removedQuestion) {
            throw new Error(`Q4H remove invariant violation: found=true but removedQuestion missing (rootId=${dialog.id.rootId} selfId=${dialog.id.selfId} questionId=${questionId})`);
        }
        const askHumanCallId = removedQuestion.callId.trim();
        if (askHumanCallId === '') {
            throw new Error(`Q4H remove invariant violation: missing callId on answered question ` +
                `(rootId=${dialog.id.rootId} selfId=${dialog.id.selfId} questionId=${questionId})`);
        }
        const contentItems = await persistPreparedUserImageAttachments({
            dialog,
            msgId: effectivePrompt.msgId,
            attachments: preparedAttachments,
        });
        const askHumanCallSiteCourse = removedQuestion.callSiteRef.course;
        const askHumanCarryoverContent = (0, inter_dialog_format_1.formatTellaskCarryoverResultContent)({
            callSiteCourse: askHumanCallSiteCourse,
            callName: 'askHuman',
            callId: removedQuestion.callId,
            responderId: 'human',
            tellaskContent: removedQuestion.tellaskContent,
            responseBody: effectivePrompt.content,
            status: 'completed',
            language: (0, work_language_1.getWorkLanguage)(),
        });
        const askHumanResultMirror = await dialog.receiveTellaskResponse('human', 'askHuman', undefined, removedQuestion.tellaskContent, 'completed', undefined, {
            response: effectivePrompt.content,
            agentId: 'human',
            callId: askHumanCallId,
            originMemberId: dialog.agentId,
            callSiteCourse: (0, storage_1.toCallSiteCourseNo)(askHumanCallSiteCourse),
            callSiteGenseq: removedQuestion.callSiteRef.callSiteGenseq,
            carryoverContent: askHumanCarryoverContent,
            contentItems,
        });
        await dialog.addChatMessages(askHumanResultMirror);
        // Emit q4h_answered event for answered question
        const answeredEvent = {
            type: 'q4h_answered',
            questionId,
            selfId: dialogId,
        };
        (0, evt_registry_1.postDialogEvent)(dialog, answeredEvent);
        // Resume the dialog after the answer has been materialized as askHuman tellask result/carryover.
        // The continuation input carries correlation only; it does not persist another user prompt fact.
        await (0, kernel_driver_1.driveDialogStream)(dialog, {
            content: effectivePrompt.content,
            ...(contentItems === undefined ? {} : { contentItems }),
            msgId: effectivePrompt.msgId,
            grammar: effectivePrompt.grammar,
            userLanguageCode: effectivePrompt.userLanguageCode,
            q4hAnswerCallId: askHumanCallId,
            origin: 'user',
        }, true, {
            source: 'ws_user_answer',
            reason: 'drive_dialog_by_user_answer',
        });
    }
    catch (error) {
        log.error('Error processing receiveHumanReply:', error);
        ws.send(JSON.stringify({
            type: 'error',
            message: `Failed to process human reply: ${error instanceof Error ? error.message : 'Unknown error'}`,
        }));
    }
}
/**
 * Setup WebSocket server with dialog handling
 */
function setupWebSocketServer(httpServers, clients, auth, serverWorkLanguage, mode) {
    const servers = Array.isArray(httpServers) ? httpServers : [httpServers];
    const webSocketServers = servers.map((server) => new ws_1.WebSocketServer({ server }));
    const runtimeStatusPubChan = (0, evt_1.createPubChan)();
    const broadcastToClients = (msg) => {
        const data = JSON.stringify(msg);
        for (const ws of clients) {
            if (ws.readyState === 1) {
                ws.send(data);
            }
        }
    };
    // Broadcast dialog display-state changes to all connected clients so multi-tab views converge.
    (0, dialog_display_state_1.setDisplayStateBroadcaster)(broadcastToClients);
    // Broadcast global dialog events to all connected clients:
    // - Q4H updates are rtws-global state in WebUI
    // - sideDialog creation must refresh hierarchy/list even when current subscription is elsewhere
    // - dlg_touched_evt keeps dialog list timestamps/reordering in sync across clients
    (0, global_dialog_event_broadcaster_1.installGlobalDialogEventBroadcaster)({
        label: 'websocket-server',
        publish: (evt) => {
            broadcastToClients(evt);
        },
    });
    // Broadcast dialog index changes (create/move/delete) so other tabs refresh their lists.
    // This ensures multi-tab/multi-browser updates stay consistent without polling.
    broadcastDialogsIndexMessage = (msg) => {
        broadcastToClients(msg);
    };
    broadcastDiligencePushUpdatedMessage = (msg) => {
        broadcastToClients(msg);
    };
    (0, persistence_1.setPrepareDialogQuarantineHook)(async ({ mainDialogId, status }) => {
        if (status !== 'running') {
            return;
        }
        (0, dialog_display_state_1.markMainDialogQuarantining)(mainDialogId);
        await (0, dialog_display_state_1.forceStopActiveRunsForMainDialog)(mainDialogId);
    });
    (0, persistence_1.setFinalizeDialogQuarantineHook)(({ mainDialogId, status, quarantined }) => {
        if (status !== 'running') {
            return;
        }
        if (quarantined) {
            (0, dialog_display_state_1.clearMainDialogQuarantiningIfIdle)(mainDialogId);
            return;
        }
        (0, dialog_display_state_1.clearMainDialogQuarantining)(mainDialogId);
    });
    (0, persistence_1.setDialogsQuarantinedBroadcaster)((msg) => {
        broadcastToClients(msg);
        if (msg.fromStatus !== 'running') {
            return;
        }
        void (0, dialog_display_state_1.createRunControlCountsMessage)()
            .then((message) => {
            broadcastToClients(message);
        })
            .catch((error) => {
            log.warn('Failed to broadcast run-control counts after dialog quarantine', error, {
                rootId: msg.rootId,
                dialogId: msg.dialogId,
            });
        });
    });
    // Broadcast global run-control refresh hints so all clients converge from persisted dialog index.
    broadcastRunControlRefreshMessage = (msg) => {
        broadcastToClients(msg);
    };
    // Broadcast rtws Problems snapshots to all connected clients.
    (0, problems_1.setProblemsBroadcaster)(broadcastToClients);
    // Broadcast `.minds/team.yaml` updates so multi-tab clients can refresh cached team config.
    (0, team_config_updates_1.setTeamConfigBroadcaster)(broadcastToClients);
    (0, tool_availability_updates_1.setToolAvailabilityBroadcaster)(broadcastToClients);
    (0, dominds_self_update_1.setDomindsSelfUpdateBroadcaster)((status) => {
        void (0, dominds_runtime_status_1.createDomindsRuntimeStatusMessage)(mode)
            .then((msg) => {
            runtimeStatusPubChan.write({
                ...msg,
                runtimeStatus: {
                    ...msg.runtimeStatus,
                    selfUpdate: status,
                },
            });
        })
            .catch((error) => {
            log.warn('Failed to broadcast Dominds runtime status update', error);
        });
    });
    (0, team_config_updates_1.startTeamConfigWatcher)();
    const cleanupRuntime = () => {
        (0, team_config_updates_1.stopTeamConfigWatcher)();
        (0, team_config_updates_1.clearTeamConfigBroadcaster)();
        (0, tool_availability_updates_1.clearToolAvailabilityBroadcaster)();
        (0, dominds_self_update_1.setDomindsSelfUpdateBroadcaster)(null);
        (0, persistence_1.setFinalizeDialogQuarantineHook)(null);
        (0, persistence_1.setPrepareDialogQuarantineHook)(null);
        (0, persistence_1.setDialogsQuarantinedBroadcaster)(null);
        broadcastDialogsIndexMessage = null;
        broadcastDiligencePushUpdatedMessage = null;
        for (const wss of webSocketServers) {
            void wss.close();
        }
    };
    servers[0]?.once('close', cleanupRuntime);
    const onConnection = (ws, req) => {
        const authCheck = (0, auth_1.getWebSocketAuthCheck)(req, auth);
        if (authCheck.kind !== 'ok') {
            ws.close(4401, 'unauthorized');
            return;
        }
        wsUiLanguage.set(ws, serverWorkLanguage);
        const runtimeStatusSubChan = (0, evt_1.createSubChan)(runtimeStatusPubChan);
        void (async () => {
            try {
                const runtimeStatus = await (0, dominds_runtime_status_1.getDomindsRuntimeStatus)(mode);
                ws.send(JSON.stringify({
                    type: 'welcome',
                    message: 'Connected to dialog server',
                    serverWorkLanguage,
                    supportedLanguageCodes: [...language_1.supportedLanguageCodes],
                    runControlCountsSnapshotEpoch: (0, dialog_display_state_1.getRunControlCountsSnapshotEpoch)(),
                    runtimeStatus,
                    timestamp: (0, time_1.formatUnifiedTimestamp)(new Date()),
                }));
            }
            catch (error) {
                log.warn('Failed to send WebSocket welcome snapshot', error);
                ws.close(1011, 'welcome_failed');
                return;
            }
            clients.add(ws);
            try {
                // Send an initial snapshot so the UI can render a stable Problems indicator immediately.
                ws.send(JSON.stringify((0, problems_1.createProblemsSnapshotMessage)()));
            }
            catch (error) {
                log.warn('Failed to send initial problems snapshot', error);
            }
            void (async () => {
                try {
                    for await (const runtimeStatusMsg of runtimeStatusSubChan.stream()) {
                        ws.send(JSON.stringify(runtimeStatusMsg));
                    }
                }
                catch (error) {
                    log.warn('Failed to forward Dominds runtime status event', error);
                }
            })();
            try {
                ws.send(JSON.stringify(await (0, dialog_display_state_1.createRunControlCountsMessage)()));
            }
            catch (error) {
                log.warn('Failed to send initial run-control counts snapshot', error);
            }
        })();
        ws.on('message', async (data) => {
            try {
                const packet = JSON.parse(data.toString());
                if (!isRecord(packet) || typeof packet.type !== 'string') {
                    throw new Error('Invalid packet format');
                }
                await handleWebSocketMessage(ws, packet);
            }
            catch (error) {
                log.error('Error handling WebSocket packet:', error);
                ws.send(JSON.stringify({
                    type: 'error',
                    message: 'Invalid packet format',
                }));
            }
        });
        ws.on('close', () => {
            runtimeStatusSubChan.cancel();
            clients.delete(ws);
            // Clean up client subscriptions
            cleanupWsClient(ws);
        });
        ws.on('error', (error) => {
            log.error('WebSocket error:', error);
            runtimeStatusSubChan.cancel();
            clients.delete(ws);
            // Clean up client subscriptions on error
            cleanupWsClient(ws);
        });
    };
    for (const wss of webSocketServers) {
        wss.on('connection', onConnection);
    }
    return webSocketServers;
}
function isRecord(value) {
    return typeof value === 'object' && value !== null;
}
/**
 * Clean up all event channels and subscriptions
 */
function cleanupEventSystems() {
    // Clear all WebSocket subscriptions (WeakMap will be garbage collected)
    // Just trigger cleanup of the event channel registry
    evt_registry_1.dialogEventRegistry.cleanup();
}
// Register cleanup on process exit
process.on('SIGINT', () => {
    cleanupEventSystems();
    void (0, runtime_1.shutdownAppsRuntime)().finally(() => process.exit(0));
});
process.on('SIGTERM', () => {
    cleanupEventSystems();
    void (0, runtime_1.shutdownAppsRuntime)().finally(() => process.exit(0));
});
