修:点了通过却以 exhausted 收场 —— 放行落地失败是终态,不是可重试的中间态

用户实测报的:任务起起来了,点通过就卡住,测试台也看不到日志。查下来是一条真事故:

**机理**(证据链完整):`land()` 抛错(草稿与正式版并发冲突)→ 异常穿到推进器 →
推进器写一行 stderr 就 break;而这一拍**已经把 runtime 落回 running、已经扣了
一步预算** —— 两秒后又来一次,空转到 steps 触顶。实测:3 步的活跑了 40 步,
终态 `exhausted`,而真正的原因只在服务端 stderr 里刷屏
(`草稿与正式版并发冲突:2 处字段已被他单改动` 刷了几十行),
任务记忆、界面、trace 上一个字都没有 —— 这正是「看不到日志」。

冲突检测本身**是对的**(多单并发写同一行,基线过期即冲突,不该静默覆盖);
坏的是失败处理。三处修:

① **落地失败落终态**:`land` 抛错 → 就地 `blocked` + 原因写进任务记忆(人读得到),
   推进器再也不会来推它 —— 空转到预算耗尽那条路断了
② **失败在时间轴上标红**:非放行终态的节点 span 走 `fail()` 并带原因,
   不再收成一条无害的「已结束」(从前失败的单在观测面上与走完的单长得一模一样)
③ 顺带修:`listInstances` 没读回 `assignee_id`(写了不读等于没写)—— 上一提交已含

实测复核(真库 + HTTP 全链):正常路径 A1 四步 released、流转到 A4(修复前是 40 步
exhausted);单测三例锁死失败路径(blocked 终态 / 只调一次 land / 不误伤正常路)。
五闸全过,测试台六例端到端照旧绿。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
2026-07-29 03:27:43 +08:00
co-authored by Claude Fable 5
parent 48bc664c7e
commit d98cdddca3
6 changed files with 198 additions and 10 deletions
@@ -169,12 +169,32 @@ export function createTaskService(deps: TaskServiceDeps): TaskService {
await deps.interaction.setPhase(taskId, 'awaiting-event');
return { task: waiting, verdict, progressed: false };
}
const landed = await deps.landing.land({
taskId,
ownerId: task.ownerId,
humanCallCap: task.caps.humanCalls,
selectedPorts: outcome.selectedPorts ?? {},
});
/*
* 落地会**抛**(草稿与正式版冲突、动作意向执行失败…)。抛出去就是灾难:
* 谱已经走完(status=finished),runtime 却还停在 running —— 推进器每两秒
* 再来一次,每次白烧一步预算,最后以 `exhausted` 收场,而真正的原因只在
* stderr 里刷屏(实测:一条试跑单空转 37 拍,界面上什么都看不到)。
* 落地失败是**终态**,不是可重试的中间态:就地落 blocked,把原因写进任务记忆。
*/
let landed;
try {
landed = await deps.landing.land({
taskId,
ownerId: task.ownerId,
humanCallCap: task.caps.humanCalls,
selectedPorts: outcome.selectedPorts ?? {},
});
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
await deps.memory.append({
taskId,
kind: 'observation',
title: `放行落地失败:${detail}`,
detail: { reason: detail },
});
const blocked = await this.finish(taskId, { kind: 'blocked', reason: `放行落地失败:${detail}` });
return { task: blocked, verdict, progressed: true };
}
if (!landed.converged) {
// 落地未收敛:输出口不得发射,任务受阻待人(落地失败卡已发)
const blocked = await this.finish(taskId, { kind: 'blocked', reason: '落地未收敛,输出口未发射' });