feat(cleanup): exec_review 合并后回收 runs/<runId>/ 工作目录 + 定期扫尾 [tsk_6uziW1Dg-ftQ]
每个 run 在 ~/.maestro/runs/<runId>/ 留下 job.json/outbox.ndjson/heartbeat,
合并(exec_review accept)后不再需要,需回收避免无限堆积。
- protocol.ts:新增 removeRunDir / isPlainRunId(幂等删 runDir,runId 白名单防越界)
- cleanup.ts(新):
- cleanupTaskRunArtifacts:合并/接受后即时回收某任务全部 run 工作目录
- sweepRunArtifacts:周期扫尾终态/孤儿 runDir 与(可选)转录,按保留天数
- isRunCleanable:仅终态(done/cancelled)/孤儿可回收,活跃/在途一律保留
- 双保险:任务状态判定 + heartbeat 新鲜则跳过,绝不误删活跃 run
- 保留策略可配:MAESTRO_RUN_RETENTION_DAYS(默认3) /
MAESTRO_TRANSCRIPT_RETENTION_DAYS(默认0=永久保留,便于排查)
- server.ts:exec_review accept 合并成功后即时清 runDir(与 removeWorktree 同处)
- index.ts:startCleanupLoop 定时扫尾(MAESTRO_CLEANUP_INTERVAL 秒,默认3600,0=关闭)
- test/cleanup.test.ts:终态清/在途留/孤儿清/保留期/heartbeat 兜底/转录开关/配置
测试:npm test → 164 passed;npm run typecheck → clean
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ 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';
|
||||
|
||||
/**
|
||||
@@ -49,6 +50,36 @@ function startSyncLoop(store: Store, app: FastifyInstance): NodeJS.Timeout | nul
|
||||
return timer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时回收 run 产物:周期扫尾 runs/<runId>/ 与转录(终态/孤儿、过保留期者)。
|
||||
* 间隔由 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<void> {
|
||||
const cfg = loadConfig();
|
||||
@@ -75,12 +106,14 @@ async function main(): Promise<void> {
|
||||
|
||||
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<void> => {
|
||||
app.log.info('收到退出信号,关闭中…');
|
||||
if (syncTimer) clearInterval(syncTimer);
|
||||
if (orchTimer) clearInterval(orchTimer);
|
||||
if (cleanupTimer) clearInterval(cleanupTimer);
|
||||
if (stopNotifier) stopNotifier();
|
||||
await app.close();
|
||||
store.close();
|
||||
|
||||
Reference in New Issue
Block a user