"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.flowRunRegistry = exports.FlowRunRegistry = void 0;
exports.cleanupStaleCwdFiles = cleanupStaleCwdFiles;
const train_json_sync_1 = require("./train-json-sync");
const DEFAULT_QUOTA = {
    maxIterPerNode: 50,
    maxLlmCalls: 100,
    maxRunDurationMs: 2 * 60 * 60 * 1000,
};
/**
 * In-memory registry of active flow runs.
 *
 * Lock semantics（v-h，原 spec §8.3 修订）：**同一 projectId 同时只允许 1 个 active run**。
 * 原本是 (projectId, basename) 级，但 cwd 文件（`.ccweb-flow-train.json`）是 project
 * 根级一份，不带 basename 区分；如果不同 basename 可并发，两个 flow 会互相覆盖
 * cwd 文件 → `filterByWhitelist` 用本 flow 白名单去过滤别人写的数据 → snapshot
 * 错乱 / outputs 检测假阳。锁升到 projectId 级是最 surgical 的解决（替代方案是
 * cwd 文件名带 basename，但 prompt-translator 系统指令也得改、LLM 不知道是哪个轨）。
 *
 * 重复 register 抛 FLOW_ALREADY_RUNNING（路由层映射为 409 + existingRunId）。
 */
class FlowRunRegistry {
    constructor() {
        this.byRunId = new Map();
        this.activeByProject = new Map(); // projectId → runId
    }
    start(opts) {
        const existing = this.activeByProject.get(opts.projectId);
        if (existing) {
            const err = new Error(`FLOW_ALREADY_RUNNING`);
            err.existingRunId = existing;
            throw err;
        }
        const info = {
            runId: opts.runId,
            projectId: opts.projectId,
            basename: opts.basename,
            status: 'pending',
            startedAt: Date.now(),
            quota: { ...DEFAULT_QUOTA, ...(opts.quotaOverride ?? {}) },
            iterCounts: new Map(),
            llmCallsCount: 0,
            cancelAbortController: new AbortController(),
            snapshot: {},
            nodeStates: new Map(),
            currentNodeId: null,
        };
        this.byRunId.set(opts.runId, info);
        this.activeByProject.set(opts.projectId, opts.runId);
        return info;
    }
    get(runId) {
        return this.byRunId.get(runId);
    }
    findActive(projectId, basename) {
        // basename 参数仅用于 caller 表意；锁是 projectId 级 (v-h)，basename 不再参与查找。
        const runId = this.activeByProject.get(projectId);
        if (!runId)
            return undefined;
        const info = this.byRunId.get(runId);
        if (basename && info && info.basename !== basename)
            return undefined;
        return info;
    }
    listActive(projectId) {
        return [...this.byRunId.values()].filter((r) => r.projectId === projectId &&
            (r.status === 'pending' || r.status === 'running' || r.status === 'waiting_user_input'));
    }
    /**
     * Check & increment quotas. Returns null if all pass, or error message if exceeded.
     * 调用方在每节点循环开始前调一次。
     */
    checkQuotaForNode(runId, nodeId) {
        const info = this.byRunId.get(runId);
        if (!info)
            return 'run not found';
        const newIter = (info.iterCounts.get(nodeId) ?? 0) + 1;
        if (newIter > info.quota.maxIterPerNode) {
            return `node ${nodeId} exceeded maxIterPerNode (${info.quota.maxIterPerNode})`;
        }
        info.iterCounts.set(nodeId, newIter);
        if (Date.now() - info.startedAt > info.quota.maxRunDurationMs) {
            return `run exceeded maxRunDurationMs (${info.quota.maxRunDurationMs}ms)`;
        }
        return null;
    }
    checkQuotaBeforeLlmCall(runId) {
        const info = this.byRunId.get(runId);
        if (!info)
            return 'run not found';
        if (info.llmCallsCount + 1 > info.quota.maxLlmCalls) {
            return `run exceeded maxLlmCalls (${info.quota.maxLlmCalls})`;
        }
        info.llmCallsCount += 1;
        return null;
    }
    /** Return remaining quotas for WS payload (spec §9.5 last bullet). */
    remainingQuota(runId, currentNodeId) {
        const info = this.byRunId.get(runId);
        if (!info)
            return { llmCallsRemaining: 0, durationRemainingMs: 0 };
        const iterRemaining = currentNodeId !== undefined
            ? Math.max(0, info.quota.maxIterPerNode - (info.iterCounts.get(currentNodeId) ?? 0))
            : undefined;
        return {
            iterRemaining,
            llmCallsRemaining: Math.max(0, info.quota.maxLlmCalls - info.llmCallsCount),
            durationRemainingMs: Math.max(0, info.quota.maxRunDurationMs - (Date.now() - info.startedAt)),
        };
    }
    updateStatus(runId, status, error) {
        const info = this.byRunId.get(runId);
        if (!info)
            return;
        info.status = status;
        if (error)
            info.error = error;
        if (status === 'completed' || status === 'failed' || status === 'cancelled') {
            this.activeByProject.delete(info.projectId);
        }
    }
    setPendingUserInput(runId, payload) {
        const info = this.byRunId.get(runId);
        if (info)
            info.pendingUserInput = payload;
    }
    clearPendingUserInput(runId) {
        const info = this.byRunId.get(runId);
        if (info)
            info.pendingUserInput = undefined;
    }
    // v-h rehydrate 字段写入入口（runtime 调用）
    setSnapshot(runId, snapshot) {
        const info = this.byRunId.get(runId);
        if (info)
            info.snapshot = { ...snapshot };
    }
    setCurrentNode(runId, nodeId) {
        const info = this.byRunId.get(runId);
        if (info)
            info.currentNodeId = nodeId;
    }
    setNodeState(runId, nodeId, state) {
        const info = this.byRunId.get(runId);
        if (info)
            info.nodeStates.set(nodeId, state);
    }
    cancel(runId) {
        const info = this.byRunId.get(runId);
        if (!info)
            return false;
        info.cancelAbortController.abort();
        this.updateStatus(runId, 'cancelled');
        return true;
    }
}
exports.FlowRunRegistry = FlowRunRegistry;
/**
 * Singleton registry instance used by routes / runtime.
 */
exports.flowRunRegistry = new FlowRunRegistry();
/**
 * Cleanup stale cwd train.json / workflow_data.json on daemon startup.
 * spec §9.6：daemon 重启检测到 cwd 文件存在视为"上次 run 异常中断"，删除。
 *
 * Pass a list of project folders to clean.
 */
function cleanupStaleCwdFiles(projectFolders) {
    for (const folder of projectFolders) {
        (0, train_json_sync_1.cleanupProjectCwd)(folder);
    }
}
//# sourceMappingURL=run-registry.js.map