import { statSync } from 'node:fs'; import type { FastifyInstance } from 'fastify'; import { Store } from '../store/index.js'; import { buildServer, attachWebSocket } from '../api/server.js'; import { registerStatic } from '../api/static.js'; import { loadConfig } from './config.js'; import { startOrchestrator } from './orchestrator.js'; import { startNotifier } from './notify.js'; import { isWorkerAlive, heartbeatAgeMs } from '../executor/protocol.js'; import { sweepRunArtifacts, loadCleanupConfig } from '../executor/cleanup.js'; import { syncProject, todoJsonPath } from '../sync/todo-sync.js'; /** * 定时同步:每轮对 active 项目检查 todo.json 的 mtime,比 last_sync_at 新才 sync。 * 间隔由 MAESTRO_SYNC_INTERVAL(秒)控制,默认 300,0=关闭。错误只记日志,不崩 daemon。 */ function startSyncLoop(store: Store, app: FastifyInstance): NodeJS.Timeout | null { const intervalSec = Number(process.env.MAESTRO_SYNC_INTERVAL ?? 300); if (!Number.isFinite(intervalSec) || intervalSec <= 0) { app.log.info('定时 todo.json 同步已关闭(MAESTRO_SYNC_INTERVAL=0)'); return null; } const tick = (): void => { try { for (const p of store.listProjects()) { if (p.status !== 'active') continue; let mtimeIso: string; try { mtimeIso = statSync(todoJsonPath(p.repoPath)).mtime.toISOString(); } catch { continue; // 无 todo.json:跳过 } if (p.lastSyncAt && mtimeIso <= p.lastSyncAt) continue; try { const r = syncProject(store, p.id); app.log.info( `定时同步「${p.name}」:created=${r.created} doneAdvanced=${r.doneAdvanced} skipped=${r.skipped} warnings=${r.warnings.length}`, ); } catch (e) { app.log.error(`定时同步「${p.name}」失败:${(e as Error).message}`); } } } catch (e) { app.log.error(`定时同步轮询失败:${(e as Error).message}`); } }; const timer = setInterval(tick, intervalSec * 1000); timer.unref(); app.log.info(`定时 todo.json 同步已启用:每 ${intervalSec}s 一轮`); return timer; } /** * 定时回收 run 产物:周期扫尾 runs// 与转录(终态/孤儿、过保留期者)。 * 间隔由 MAESTRO_CLEANUP_INTERVAL(秒)控制,默认 3600,0=关闭;保留策略见 loadCleanupConfig。 * 错误只记日志,不崩 daemon。 */ function startCleanupLoop(store: Store, app: FastifyInstance): NodeJS.Timeout | null { const intervalSec = Number(process.env.MAESTRO_CLEANUP_INTERVAL ?? 3600); if (!Number.isFinite(intervalSec) || intervalSec <= 0) { app.log.info('定时 run 产物回收已关闭(MAESTRO_CLEANUP_INTERVAL=0)'); return null; } const cfg = loadCleanupConfig(); const tick = (): void => { try { const r = sweepRunArtifacts(store, cfg, app.log); if (r.runDirs || r.transcripts) { app.log.info(`run 产物回收:runDir=${r.runDirs} 转录=${r.transcripts}`); } } catch (e) { app.log.error(`run 产物回收轮失败:${(e as Error).message}`); } }; const timer = setInterval(tick, intervalSec * 1000); timer.unref(); app.log.info( `定时 run 产物回收已启用:每 ${intervalSec}s 一轮(runDir 保留 ${cfg.runRetentionDays}d,转录保留 ${cfg.transcriptRetentionDays > 0 ? cfg.transcriptRetentionDays + 'd' : '永久'})`, ); return timer; } /** maestrod:核心 daemon。Store + REST/WS API + 定时同步 + 编排器(Phase 2:自动领取可执行任务并在 worktree 起 headless CC)。 */ async function main(): Promise { const cfg = loadConfig(); const store = new Store(cfg.dbFile); const rec = store.reconcileDeps(); // 启动对账:ready↔blocked 按依赖纠正存量数据 // 中断恢复(多进程执行):用 worker_pid + 心跳真判活——worker 仍活则 re-adopt(daemon 续 ingest 其 outbox), // 死则回收(failTaskAttempt 收尾 + 重试/needs_attention)。 const itr = store.reconcileInterrupted((run) => run !== null && isWorkerAlive({ pid: run.workerPid, heartbeatAgeMs: heartbeatAgeMs(run.id), startedAgeMs: Date.now() - Date.parse(run.startedAt), }), ); const app = buildServer({ store, logger: true }); registerStatic(app); // Web 看板(web/ 静态文件) attachWebSocket(app, store); await app.listen({ host: cfg.host, port: cfg.port }); app.log.info(`maestrod 就绪 · db=${cfg.dbFile} · http://${cfg.host}:${cfg.port} · ws ${cfg.host}:${cfg.port}/ws`); if (rec.blocked || rec.released) app.log.info(`依赖对账:转入等依赖 ${rec.blocked} · 放行可执行 ${rec.released}`); if (itr.readopted || itr.reclaimed) app.log.info(`中断恢复:re-adopt ${itr.readopted} · 回收 ${itr.reclaimed}`); const syncTimer = startSyncLoop(store, app); const orchTimer = startOrchestrator(store, app); // 编排器:自动领取可执行任务(MAESTRO_ORCH_INTERVAL 秒,0=关闭) const cleanupTimer = startCleanupLoop(store, app); // 定时回收 run 产物(MAESTRO_CLEANUP_INTERVAL 秒,0=关闭) const stopNotifier = startNotifier(store, app); // macOS 原生通知:审核闸/需人工/合并完成(MAESTRO_NOTIFY=0 关闭) const shutdown = async (): Promise => { app.log.info('收到退出信号,关闭中…'); if (syncTimer) clearInterval(syncTimer); if (orchTimer) clearInterval(orchTimer); if (cleanupTimer) clearInterval(cleanupTimer); if (stopNotifier) stopNotifier(); await app.close(); store.close(); process.exit(0); }; process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); } main().catch((err) => { console.error('maestrod 启动失败:', err); process.exit(1); });