You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In a real session on a packaged build, a task that the model had actually completed was left pending in the ledger forever — because the refusal that blocked it named the rule it broke and not the one call that would have satisfied it. The model read the refusal, could not derive the exit, and abandoned the task.
This is the same shape as #2234, one layer up. #2234 was about a refusal never reaching the model at all. This is about a refusal reaching the model and being useless when it arrives.
What happened
Session a34d6343, run 86204787 (packaged macOS arm64 build, DeepSeek V4 Pro). The model created an 8-task plan and worked it. Task T7 = 第5步:五清扫sweep.
It did the work — ran the sweep script, got a clean result — and then:
seq 223 function_call task_update {"id":"T7","status":"completed",
"completionEvidence":"五清扫通过,零残留"}
seq 225 function_response task_update isError: true
"Invalid task status transition from pending to completed"
seq 228 function_call task_update {"id":"T8","status":"in_progress"}
That is the entire refusal text the model received. Not a truncation — the full result.text.
T7 was never started, so pending → completed is genuinely not allowed. The fix is one call: task_update {id: "T7", status: "in_progress"} first. The message does not say that. It does not name in_progress, does not name the tool, does not say a transition exists that would work.
The model's response was to skip T7 and move on. Replaying workflow_task_ledger_events to final state:
第0步:标的定性与中立数据包 in_progress
第1步:三框架盲测 completed
第2步:crux定向核实 completed
第3步:裁决与定价 completed
第3.5步:红队自攻 completed
第4步:撰写并生成.docx completed
第5步:五清扫sweep pending ← the work was done
第6步:交付与台账登记 completed
The ledger now asserts step 5 never happened. It did happen; the evidence string the model tried to attach describes it. The ledger's single job is to hold the plan's true state across a volatile context, and a refusal message caused it to hold a false one.
This model was not confused about the protocol in general — it used in_progress → completed correctly for T1, T2, T3, T4, T5, T6 and T8. It slipped once on T7 and the refusal gave it nothing to recover with.
The same session, the same shape, one turn earlier
This one the model recovered from immediately — because the message names the field. That contrast is the whole point: same tool, same session, same model. The recoverable refusal named a thing to do; the unrecoverable one named only a thing that was wrong.
Note also that neither message mentions the other rule. The model learned "completion needs evidence" at seq 150, applied it correctly at seq 223, and walked straight into the second rule it had no way to know about.
Still-unfixed instance of the message #2240 replaces
packages/runtime/src/subagent-tools.ts:142 — agent_spawn carries verbatim the message that #2240 replaces in agent_swarm:
if(Boolean(input.profile)===Boolean(input.subagent_id)){ctx.addIssue({code: z.ZodIssueCode.custom,message: 'Provide exactly one of subagent_id or legacy profile.',});
Three separate dead ends in one sentence: it does not distinguish "you set neither" from "you set both", it does not list the legal profile values, and it does not say where a subagent_id comes from (agent_list). #2240 fixed exactly this in agent-swarm-tools.ts and deliberately scoped agent_spawn out; this is that follow-up. It is a small, self-contained change and the agent_swarm version can be lifted directly.
Suggested rule
A refusal the model is expected to recover from should name a next action, not only a violated constraint. Concretely, for these three:
Site
Now
Should also say
packages/core/src/task-ledger.ts:460
Invalid task status transition from pending to completed
that pending → in_progress → completed is the path, i.e. call task_update with in_progress first
packages/runtime/src/subagent-tools.ts:142
Provide exactly one of subagent_id or legacy profile.
which mistake was made, the legal profile values, and that subagent_id comes from agent_list
packages/runtime/src/task-ledger-tools.ts:214
completionEvidence is required when status is completed.
— already fine; kept here as the control case
The transition message is the one that cost a task, so it is the one worth fixing first.
#2290 proposes cutting the transition whitelist and completionEvidence outright ("Transition whitelist + explicitReopen + evidence rules — cut, validate enum membership only"). If that lands as specified, the task-ledger.ts:460 message disappears rather than improves, and this issue's first row becomes moot.
I am not proposing to pre-empt that decision. Two things are worth saying either way:
This is a data point for RFC: Radically slim the Session Task Ledger #2290's open question 4, not against it. A rule the authority enforces and cannot explain is a rule that costs real state. That is an argument for cutting or for explaining — but not for leaving it as is.
If #2290's P1 is close, fixing only subagent-tools.ts and letting the transition message die with the whitelist is the cheaper path. If P1 is not close, the transition message is a one-line change that stops a live loss.
What I verified
Everything above is read from a packaged build's runtime.sqlite: runtime_events for the exact sequence numbers quoted, workflow_task_ledger_events replayed for the final state table.
The refusal texts are the complete result.text values the model received, not summaries.
In a real session on a packaged build, a task that the model had actually completed was left
pendingin the ledger forever — because the refusal that blocked it named the rule it broke and not the one call that would have satisfied it. The model read the refusal, could not derive the exit, and abandoned the task.This is the same shape as #2234, one layer up. #2234 was about a refusal never reaching the model at all. This is about a refusal reaching the model and being useless when it arrives.
What happened
Session
a34d6343, run86204787(packaged macOS arm64 build, DeepSeek V4 Pro). The model created an 8-task plan and worked it. TaskT7= 第5步:五清扫sweep.It did the work — ran the sweep script, got a clean result — and then:
That is the entire refusal text the model received. Not a truncation — the full
result.text.T7was never started, sopending → completedis genuinely not allowed. The fix is one call:task_update {id: "T7", status: "in_progress"}first. The message does not say that. It does not namein_progress, does not name the tool, does not say a transition exists that would work.The model's response was to skip
T7and move on. Replayingworkflow_task_ledger_eventsto final state:The ledger now asserts step 5 never happened. It did happen; the evidence string the model tried to attach describes it. The ledger's single job is to hold the plan's true state across a volatile context, and a refusal message caused it to hold a false one.
This model was not confused about the protocol in general — it used
in_progress → completedcorrectly for T1, T2, T3, T4, T5, T6 and T8. It slipped once on T7 and the refusal gave it nothing to recover with.The same session, the same shape, one turn earlier
This one the model recovered from immediately — because the message names the field. That contrast is the whole point: same tool, same session, same model. The recoverable refusal named a thing to do; the unrecoverable one named only a thing that was wrong.
Note also that neither message mentions the other rule. The model learned "completion needs evidence" at seq 150, applied it correctly at seq 223, and walked straight into the second rule it had no way to know about.
Still-unfixed instance of the message #2240 replaces
packages/runtime/src/subagent-tools.ts:142—agent_spawncarries verbatim the message that #2240 replaces inagent_swarm:Three separate dead ends in one sentence: it does not distinguish "you set neither" from "you set both", it does not list the legal
profilevalues, and it does not say where asubagent_idcomes from (agent_list). #2240 fixed exactly this inagent-swarm-tools.tsand deliberately scopedagent_spawnout; this is that follow-up. It is a small, self-contained change and theagent_swarmversion can be lifted directly.Suggested rule
A refusal the model is expected to recover from should name a next action, not only a violated constraint. Concretely, for these three:
packages/core/src/task-ledger.ts:460Invalid task status transition from pending to completedpending → in_progress → completedis the path, i.e. calltask_updatewithin_progressfirstpackages/runtime/src/subagent-tools.ts:142Provide exactly one of subagent_id or legacy profile.profilevalues, and thatsubagent_idcomes fromagent_listpackages/runtime/src/task-ledger-tools.ts:214completionEvidence is required when status is completed.The transition message is the one that cost a task, so it is the one worth fixing first.
Interaction with #2290
#2290 proposes cutting the transition whitelist and
completionEvidenceoutright ("Transition whitelist +explicitReopen+ evidence rules — cut, validate enum membership only"). If that lands as specified, thetask-ledger.ts:460message disappears rather than improves, and this issue's first row becomes moot.I am not proposing to pre-empt that decision. Two things are worth saying either way:
agent_spawnis outside RFC: Radically slim the Session Task Ledger #2290 entirely and can be fixed today regardless of how the RFC resolves.If #2290's P1 is close, fixing only
subagent-tools.tsand letting the transition message die with the whitelist is the cheaper path. If P1 is not close, the transition message is a one-line change that stops a live loss.What I verified
runtime.sqlite:runtime_eventsfor the exact sequence numbers quoted,workflow_task_ledger_eventsreplayed for the final state table.result.textvalues the model received, not summaries.agent_spawnmessage is a static read of currentmain, not an observed failure.简体中文要点
在打包版本的一次真实会话里,一个模型确实做完了的任务被永久留在账本的
pending状态——因为挡住它的那句拒绝只说了违反哪条规则,没说哪一个调用能满足它。模型读完拒绝,推不出出路,直接放弃了这个任务。这和 #2234 是同一个形状,只是高了一层:#2234 是拒绝根本到不了模型;这条是拒绝到了模型手里,但没有用。
经过:会话
a34d6343,run86204787。模型建了 8 步计划,T7 = 第5步:五清扫sweep。它跑完了 sweep 脚本,拿到干净结果,然后:那就是模型收到的全文,没有截断。T7 确实没 start 过,所以这个跃迁本来就不合法,正确解法只需要一次调用:先
task_update {id:"T7", status:"in_progress"}。而这句话里没有in_progress,没有工具名,也没说存在一条走得通的路径。模型的反应是跳过 T7 直接做 T8。重放
workflow_task_ledger_events的终态:第5步pending,而它前后的第4步、第6步都completed。账本现在断言第5步没做过——它做过,模型试图附上的那段证据就是在描述它。账本唯一的职责是在易失上下文之外持有计划的真实状态,而一句拒绝文案让它持有了假状态。这个模型并不是不懂协议:T1–T6、T8 它都规规矩矩走了
in_progress → completed。它在 T7 上失手一次,而拒绝没给它任何可恢复的东西。同一会话、前一轮的对照组:seq 149 漏了
completionEvidence,拒绝里点了字段名,模型 seq 153 立刻补上改对。同一个工具、同一个会话、同一个模型——能恢复的那句说了"该做什么",不能恢复的那句只说了"什么错了"。另外两条规则互不提及:模型在 seq 150 学会"完成要证据",在 seq 223 正确用上,然后一头撞进它无从得知的第二条规则。#2240 换掉的那句文案,
agent_spawn还原样留着:subagent-tools.ts:142的Provide exactly one of subagent_id or legacy profile.——一句话里三个死胡同:不区分"一个都没给"和"两个都给了"、不列出合法的profile值、不说subagent_id从哪来(agent_list)。#2240 在agent-swarm-tools.ts里修的就是这一条,并明确把agent_spawn划到范围外,这就是那个后续项,改动很小,可以直接照搬。建议的规则:期望模型自行恢复的拒绝,应当指出下一个动作,而不只是被违反的约束。三处对照见上表——
task-ledger.ts:460是真的赔掉了一个任务的那条,优先级最高。与 #2290 的关系:#2290 提议直接砍掉迁移白名单和
completionEvidence。若按原案落地,task-ledger.ts:460这句是消失而不是改进,本 issue 的第一行就作废。我不打算抢在那个决定前面,但两点无论如何成立:(1) 这是 #2290 开放问题 4 的一个数据点,不是反对意见——一条权威在执行却解释不了的规则会赔掉真实状态,这既可以论证"砍"也可以论证"讲清楚",但唯独不能论证"就这样放着";(2)agent_spawn完全在 #2290 范围之外,今天就能修。验证边界:以上全部从打包版
runtime.sqlite读出;拒绝文案是模型收到的完整result.text,不是摘要;不是刻意复现的,是在打包验证 #2240 时撞见的,放弃任务这个行为 n=1,换个模型未必如此;agent_spawn那条是对当前main的静态阅读,不是观察到的失败。