merge: planner runs — 编排器自动拆解 hard / 自动写方案 medium [planner; tsk_1dkswD5Ub7gT]
部署 planner 功能(此前实现在 planner 分支、测试绿,未合):编排器把 analyzing/speccing 也当可执行,派 planner run 自动拆解(hard→子任务+plan_review)/写方案(medium→spec+spec_review);in-flight 从 status 泛化为'有 started run';reap 区分 executor/planner。与 main 干净合并,typecheck + 201 测试全绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+61
-3
@@ -660,11 +660,67 @@ export class Store {
|
||||
return this.getTask(taskId)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* planner(拆解 Hard / 写方案 Medium)失败的落点:收尾 planner run + 退避,任务【留在 analyzing/speccing】
|
||||
* 等退避到期重新被领取;累计 planner 失败超 project.maxRetries → 升级 needs_attention(清退避)。
|
||||
* 与 failTaskAttempt 的区别:planner 失败不把任务转 failed/queued(那是 executor 语义),只退避或升级。
|
||||
*/
|
||||
failPlanAttempt(taskId: string, runId: string | null, error: string): Task {
|
||||
const row = this.getTaskRow(taskId);
|
||||
if (!row) throw new StoreError(`任务不存在: ${taskId}`);
|
||||
const project = this.getProject(row.project_id);
|
||||
if (!project) throw new StoreError(`项目不存在: ${row.project_id}`);
|
||||
|
||||
if (runId) {
|
||||
const rr = this.db.prepare(`SELECT status FROM runs WHERE id = ?`).get(runId) as { status: string } | undefined;
|
||||
if (rr && rr.status === 'started') this.finishRun(runId, 'failed', { error });
|
||||
} else {
|
||||
const r = this.startRun(taskId, 'planner');
|
||||
this.finishRun(r.id, 'failed', { error });
|
||||
}
|
||||
|
||||
const allFailed = (this.db.prepare(
|
||||
`SELECT COUNT(*) AS n FROM runs WHERE task_id = ? AND kind = 'planner' AND status = 'failed'`,
|
||||
).get(taskId) as { n: number }).n;
|
||||
const priorNetFailed = Math.max(0, allFailed - 1); // 不含本次
|
||||
if (priorNetFailed < project.maxRetries) {
|
||||
const attempt = priorNetFailed + 1;
|
||||
const backoffMs = Math.min(30_000 * 2 ** (attempt - 1), 10 * 60_000);
|
||||
this.setNextEligibleAt(taskId, new Date(Date.now() + backoffMs).toISOString());
|
||||
// 任务保持 analyzing/speccing(不转状态)——退避到期后重新可领,再起 planner run
|
||||
this.emit(row.project_id, taskId, 'task.updated', { field: 'plan-retry', attempt, backoffMs, error });
|
||||
} else {
|
||||
this.setNextEligibleAt(taskId, null);
|
||||
this.transition(taskId, 'needs_attention', { by: 'failPlanAttempt', allFailed });
|
||||
}
|
||||
return this.getTask(taskId)!;
|
||||
}
|
||||
|
||||
/** 持久化退避:早于 next_eligible_at 不被领取(null=即刻可领)。 */
|
||||
setNextEligibleAt(taskId: string, iso: string | null): void {
|
||||
this.db.prepare(`UPDATE tasks SET next_eligible_at = ?, updated_at = ? WHERE id = ?`).run(iso, now(), taskId);
|
||||
}
|
||||
|
||||
/** 项目内"有 started run 的任务" id 集合(多进程并发闸 + claimable 排除在途;executor 与 planner 通用)。 */
|
||||
inflightTaskIds(projectId: string): Set<string> {
|
||||
const rows = this.db.prepare(
|
||||
`SELECT DISTINCT r.task_id AS tid FROM runs r JOIN tasks t ON t.id = r.task_id
|
||||
WHERE r.status = 'started' AND t.project_id = ?`,
|
||||
).all(projectId) as Array<{ tid: string }>;
|
||||
return new Set(rows.map((r) => r.tid));
|
||||
}
|
||||
|
||||
/** 所有 started run + 其任务(reap / ingest / reconcile 通用,覆盖 executor + planner + 残留复审 run)。 */
|
||||
liveRunsWithTask(): Array<{ task: Task; run: Run }> {
|
||||
const runs = this.db.prepare(`SELECT * FROM runs WHERE status = 'started' ORDER BY started_at`).all() as RunRow[];
|
||||
const out: Array<{ task: Task; run: Run }> = [];
|
||||
for (const r of runs) {
|
||||
const t = this.getTaskRow(r.task_id);
|
||||
if (t) out.push({ task: rowToTask(t), run: rowToRun(r) });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 项目内 executing 任务数(多进程执行的并发闸:每个 executing 对应一个活 worker)。 */
|
||||
countExecuting(projectId: string): number {
|
||||
return (this.db.prepare(
|
||||
@@ -691,9 +747,11 @@ export class Store {
|
||||
*/
|
||||
reconcileInterrupted(isAlive: (run: Run | null) => boolean = () => false): { readopted: number; reclaimed: number } {
|
||||
let readopted = 0, reclaimed = 0;
|
||||
for (const { task, run } of this.executingWithLatestExecutorRun()) {
|
||||
if (run && isAlive(run)) { readopted++; continue; }
|
||||
this.failTaskAttempt(task.id, run?.id ?? null, 'daemon 重启时发现 worker 已退出');
|
||||
for (const { task, run } of this.liveRunsWithTask()) {
|
||||
if (isAlive(run)) { readopted++; continue; } // worker 仍活 → re-adopt,daemon 续 ingest
|
||||
if (run.kind === 'executor') this.failTaskAttempt(task.id, run.id, 'daemon 重启时发现 worker 已退出');
|
||||
else if (run.kind === 'planner') this.failPlanAttempt(task.id, run.id, 'daemon 重启时发现 planner worker 已退出');
|
||||
else this.finishRun(run.id, 'failed', { error: 'daemon 重启中断' }); // 残留复审 run:仅收尾
|
||||
reclaimed++;
|
||||
}
|
||||
return { readopted, reclaimed };
|
||||
|
||||
Reference in New Issue
Block a user