/** * RunRegistry - 进程运行注册表 * * 参考 openclaw/src/process/supervisor/registry.ts * * 核心功能: * - 状态机追踪: starting → running → exiting → exited * - 按条件查询: byBackend, byState, active * - 自动淘汰: exited 记录超过 maxExited 时 FIFO 清除 * - 统计输出: 给 /api/runs 端点提供数据 */ /** @typedef {'starting'|'running'|'exiting'|'exited'} RunState */ /** @typedef {'manual-cancel'|'overall-timeout'|'no-output-timeout'|'spawn-error'|'signal'|'exit'|'unknown'} TerminationReason */ /** * @typedef {object} RunRecord * @property {string} runId * @property {string} agentName * @property {string} backendId - launcher / cliBackend 标识 * @property {string} [scopeKey] - 串行化 key * @property {number} [pid] * @property {RunState} state * @property {TerminationReason} [terminationReason] * @property {number} [exitCode] * @property {number} createdAtMs * @property {number} startedAtMs * @property {number} lastOutputAtMs * @property {number} [updatedAtMs] */ const DEFAULT_MAX_EXITED = 200; class RunRegistry { /** * @param {object} [opts] * @param {number} [opts.maxExitedRecords=200] */ constructor(opts = {}) { /** @type {Map} */ this._records = new Map(); this._maxExited = opts.maxExitedRecords || DEFAULT_MAX_EXITED; } /** * 注册新的 run * @param {object} params * @param {string} params.runId * @param {string} params.agentName * @param {string} params.backendId * @param {string} [params.scopeKey] * @param {number} [params.pid] * @returns {string} runId */ add(params) { const now = Date.now(); /** @type {RunRecord} */ const record = { runId: params.runId, agentName: params.agentName, backendId: params.backendId, scopeKey: params.scopeKey, pid: params.pid, state: 'starting', createdAtMs: now, startedAtMs: 0, lastOutputAtMs: 0, }; this._records.set(record.runId, record); this._pruneExited(); return record.runId; } /** * 获取记录 * @param {string} runId * @returns {RunRecord|undefined} */ get(runId) { return this._records.get(runId); } /** * 变更状态 * @param {string} runId * @param {RunState} state * @param {object} [extra] - 额外字段 (exitCode, terminationReason, pid 等) */ markState(runId, state, extra = {}) { const record = this._records.get(runId); if (!record) return; record.state = state; record.updatedAtMs = Date.now(); if (state === 'running' && !record.startedAtMs) { record.startedAtMs = Date.now(); } Object.assign(record, extra); } /** 记录输出活动 (给 ProcessGuard 联动) */ touchOutput(runId) { const record = this._records.get(runId); if (record) { record.lastOutputAtMs = Date.now(); } } /** 获取所有活跃 runs (starting + running) */ listActive() { return [...this._records.values()].filter( r => r.state === 'starting' || r.state === 'running' ); } /** 按 backendId 查询 */ listByBackend(backendId) { return [...this._records.values()].filter(r => r.backendId === backendId); } /** 按 agentName 查询 */ listByAgent(agentName) { return [...this._records.values()].filter(r => r.agentName === agentName); } /** 获取全部记录 */ list() { return [...this._records.values()]; } /** 删除指定 run */ remove(runId) { this._records.delete(runId); } /** 统计信息 */ stats() { const all = [...this._records.values()]; return { total: all.length, starting: all.filter(r => r.state === 'starting').length, running: all.filter(r => r.state === 'running').length, exiting: all.filter(r => r.state === 'exiting').length, exited: all.filter(r => r.state === 'exited').length, }; } /** 清空全部 */ clear() { this._records.clear(); } /** 淘汰过期 exited 记录 (FIFO) */ _pruneExited() { const exited = [...this._records.entries()] .filter(([, r]) => r.state === 'exited') .sort((a, b) => (a[1].createdAtMs || 0) - (b[1].createdAtMs || 0)); while (exited.length > this._maxExited) { const [id] = exited.shift(); this._records.delete(id); } } } module.exports = { RunRegistry, DEFAULT_MAX_EXITED };