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