fix(agent): 超预算待处理事项先于会话终态写入

e15「超预算」偶发失败(全量 ci 负载下 decision.agent_run_id 仍是上一次运行)是产品竞态,不是测试等待不足:
finalize 先把 agent_run 写成 budget_exceeded,之后才 upsert 同根因的超预算待处理事项(和任务行)。
观察者(测试、会话页、待处理中心)在两次写入之间看到终态时,事项还指向上一次运行。

改为先写待处理事项与任务行,最后写会话终态:看到 budget_exceeded 时事项已指向这次运行。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 22:40:39 +08:00
1 parent 12bc4664b6
commit c9c6505d71
1 file changed
+26 -23
+26 -23
View File
@@ -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);