diff --git a/apps/server/src/modules/agent/service.ts b/apps/server/src/modules/agent/service.ts index 43a5bf76..76ac9a08 100644 --- a/apps/server/src/modules/agent/service.ts +++ b/apps/server/src/modules/agent/service.ts @@ -315,18 +315,20 @@ export async function finalize( content_hash: sha(JSON.stringify({ runId, report })), }) .execute(); - await ctx.db - .updateTable("agent_run") - .set({ - status, - summary: summary.slice(0, 4000), - ...(status === "waiting" ? {} : { finished_at: new Date() }), - ...(res.status === "failed" - ? { error: JSON.stringify({ reason: res.reason, budget: res.budget, message: res.message }) } - : {}), - }) - .where("id", "=", runId) - .execute(); + if (status === "budget_exceeded") { + // 同根因(同一待处理事项 / 同一定时巡检 / 同一目标模板)超预算合并到已有事项,不重复新建。 + // 先写事项再写终态:观察者看到 budget_exceeded 时,事项已指向这次运行(之前顺序相反, + // 负载下读到终态时事项还指向上一次运行,e15 偶发失败) + await writeDecision(ctx, { + kind: "other", + title: "智能体运行超出预算", + summary: res.message ?? "超出预算", + subjectId: runId, + dedupeKey: await budgetDedupeKey(ctx, run), + evidence: { runId, runIds: [runId], budget: res.budget, message: res.message, goal: run.goal }, + }); + } + // 任务行同样先于会话终态写:看到会话终态时任务页已是同一结论 if (run.task_id) { const taskStatus = status === "cancelled" @@ -349,17 +351,18 @@ export async function finalize( .where("id", "=", run.task_id) .execute(); } - if (status === "budget_exceeded") { - // 同根因(同一待处理事项 / 同一定时巡检 / 同一目标模板)超预算合并到已有事项,不重复新建 - await writeDecision(ctx, { - kind: "other", - title: "智能体运行超出预算", - summary: res.message ?? "超出预算", - subjectId: runId, - dedupeKey: await budgetDedupeKey(ctx, run), - evidence: { runId, runIds: [runId], budget: res.budget, message: res.message, goal: run.goal }, - }); - } + await ctx.db + .updateTable("agent_run") + .set({ + status, + summary: summary.slice(0, 4000), + ...(status === "waiting" ? {} : { finished_at: new Date() }), + ...(res.status === "failed" + ? { error: JSON.stringify({ reason: res.reason, budget: res.budget, message: res.message }) } + : {}), + }) + .where("id", "=", runId) + .execute(); // 后台调查:结论回写待处理事项,交还人工(仍可被新证据重新唤醒) if (run.dedupe_key?.startsWith("investigate:")) { const decisionId = run.dedupe_key.slice("investigate:".length);