Skip to content

bug(runtime): a refusal that names the violation but not the exit left a completed task pending forever #2310

Description

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

seq 149 function_call task_update {"id":"T4","status":"completed"}
seq 150 function_response "Tool \"task_update\" arguments failed validation: [
{ \"code\": \"custom\",
\"message\": \"completionEvidence is required when status is completed.\",
\"path\": [\"completionEvidence\"] } ]
task_update takes `id`, `status`, `subject`, `blockedReason`,
`failureReason`, `completionEvidence`, `explicitReopen`."
seq 153 function_call task_update {"id":"T4","status":"completed",
"completionEvidence":"裁决完成:…"}

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:142agent_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:

SiteNowShould also say
packages/core/src/task-ledger.ts:460Invalid task status transition from pending to completedthat pending → in_progress → completed is the path, i.e. call task_update with in_progress first
packages/runtime/src/subagent-tools.ts:142Provide 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:214completionEvidence 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.

Interaction with #2290

#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:

  1. 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.
  2. agent_spawn is 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.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.
  • I did not reproduce this deliberately — it surfaced while verifying fix(runtime): decide the tool call event's ledger lane at push time #2240 against a packaged build. n=1 for the abandonment behaviour specifically; a different model might have recovered.
  • The agent_spawn message is a static read of current main, not an observed failure.
简体中文要点

在打包版本的一次真实会话里,一个模型确实做完了的任务被永久留在账本的 pending 状态——因为挡住它的那句拒绝只说了违反哪条规则,没说哪一个调用能满足它。模型读完拒绝,推不出出路,直接放弃了这个任务。

这和 #2234 是同一个形状,只是高了一层:#2234 是拒绝根本到不了模型;这条是拒绝到了模型手里,但没有用。

经过:会话 a34d6343,run 86204787。模型建了 8 步计划,T7 = 第5步:五清扫sweep。它跑完了 sweep 脚本,拿到干净结果,然后:

seq 223 task_update {"id":"T7","status":"completed","completionEvidence":"五清扫通过,零残留"}
seq 225 → isError: "Invalid task status transition from pending to completed"
seq 228 task_update {"id":"T8","status":"in_progress"}

那就是模型收到的全文,没有截断。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:142Provide exactly one of subagent_id or legacy profile.——一句话里三个死胡同:不区分"一个都没给"和"两个都给了"、不列出合法的 profile 值、不说 subagent_id 从哪来(agent_list)。#2240agent-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 的静态阅读,不是观察到的失败。

Metadata

Metadata

Assignees

No one assigned

    Labels

    help wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
       blocks
      (function() {
      function addCopyButtons() {
      document.querySelectorAll('pre code').forEach(function(codeBlock) {
      if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
      codeBlock.parentElement.setAttribute('data-copy-added', 'true');
      var btn = document.createElement('button');
      btn.textContent = 'Copy';
      btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
      btn.onmouseover = function() { this.style.opacity = '1'; };
      btn.onmouseout = function() { this.style.opacity = '0.7'; };
      btn.onclick = function() {
      navigator.clipboard.writeText(codeBlock.textContent).then(function() {
      btn.textContent = 'Copied!';
      setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
      });
      };
      codeBlock.parentElement.style.position = 'relative';
      codeBlock.parentElement.appendChild(btn);
      });
      }
      addCopyButtons();
      // Re-run on dynamic content
      var observer = new MutationObserver(addCopyButtons);
      observer.observe(document.body, { childList: true, subtree: true });
      })();
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      bug(runtime): a refusal that names the violation but not the exit left a completed task `pending` forever · Issue #2310 · apache/maka · GitHub
      Skip to content

      bug(runtime): a refusal that names the violation but not the exit left a completed task pending forever #2310

      Description

      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

      seq 149 function_call task_update {"id":"T4","status":"completed"}
      seq 150 function_response "Tool \"task_update\" arguments failed validation: [
      { \"code\": \"custom\",
      \"message\": \"completionEvidence is required when status is completed.\",
      \"path\": [\"completionEvidence\"] } ]
      task_update takes `id`, `status`, `subject`, `blockedReason`,
      `failureReason`, `completionEvidence`, `explicitReopen`."
      seq 153 function_call task_update {"id":"T4","status":"completed",
      "completionEvidence":"裁决完成:…"}
      

      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:142agent_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:

      SiteNowShould also say
      packages/core/src/task-ledger.ts:460Invalid task status transition from pending to completedthat pending → in_progress → completed is the path, i.e. call task_update with in_progress first
      packages/runtime/src/subagent-tools.ts:142Provide 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:214completionEvidence 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.

      Interaction with #2290

      #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:

      1. 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.
      2. agent_spawn is 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.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.
      • I did not reproduce this deliberately — it surfaced while verifying fix(runtime): decide the tool call event's ledger lane at push time #2240 against a packaged build. n=1 for the abandonment behaviour specifically; a different model might have recovered.
      • The agent_spawn message is a static read of current main, not an observed failure.
      简体中文要点

      在打包版本的一次真实会话里,一个模型确实做完了的任务被永久留在账本的 pending 状态——因为挡住它的那句拒绝只说了违反哪条规则,没说哪一个调用能满足它。模型读完拒绝,推不出出路,直接放弃了这个任务。

      这和 #2234 是同一个形状,只是高了一层:#2234 是拒绝根本到不了模型;这条是拒绝到了模型手里,但没有用。

      经过:会话 a34d6343,run 86204787。模型建了 8 步计划,T7 = 第5步:五清扫sweep。它跑完了 sweep 脚本,拿到干净结果,然后:

      seq 223 task_update {"id":"T7","status":"completed","completionEvidence":"五清扫通过,零残留"}
      seq 225 → isError: "Invalid task status transition from pending to completed"
      seq 228 task_update {"id":"T8","status":"in_progress"}
      

      那就是模型收到的全文,没有截断。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:142Provide exactly one of subagent_id or legacy profile.——一句话里三个死胡同:不区分"一个都没给"和"两个都给了"、不列出合法的 profile 值、不说 subagent_id 从哪来(agent_list)。#2240agent-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 的静态阅读,不是观察到的失败。

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        help wantedExtra attention is needed

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' bug(runtime): a refusal that names the violation but not the exit left a completed task `pending` forever · Issue #2310 · apache/maka · GitHub
          Skip to content

          bug(runtime): a refusal that names the violation but not the exit left a completed task pending forever #2310

          Description

          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

          seq 149 function_call task_update {"id":"T4","status":"completed"}
          seq 150 function_response "Tool \"task_update\" arguments failed validation: [
          { \"code\": \"custom\",
          \"message\": \"completionEvidence is required when status is completed.\",
          \"path\": [\"completionEvidence\"] } ]
          task_update takes `id`, `status`, `subject`, `blockedReason`,
          `failureReason`, `completionEvidence`, `explicitReopen`."
          seq 153 function_call task_update {"id":"T4","status":"completed",
          "completionEvidence":"裁决完成:…"}
          

          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:142agent_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:

          SiteNowShould also say
          packages/core/src/task-ledger.ts:460Invalid task status transition from pending to completedthat pending → in_progress → completed is the path, i.e. call task_update with in_progress first
          packages/runtime/src/subagent-tools.ts:142Provide 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:214completionEvidence 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.

          Interaction with #2290

          #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:

          1. 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.
          2. agent_spawn is 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.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.
          • I did not reproduce this deliberately — it surfaced while verifying fix(runtime): decide the tool call event's ledger lane at push time #2240 against a packaged build. n=1 for the abandonment behaviour specifically; a different model might have recovered.
          • The agent_spawn message is a static read of current main, not an observed failure.
          简体中文要点

          在打包版本的一次真实会话里,一个模型确实做完了的任务被永久留在账本的 pending 状态——因为挡住它的那句拒绝只说了违反哪条规则,没说哪一个调用能满足它。模型读完拒绝,推不出出路,直接放弃了这个任务。

          这和 #2234 是同一个形状,只是高了一层:#2234 是拒绝根本到不了模型;这条是拒绝到了模型手里,但没有用。

          经过:会话 a34d6343,run 86204787。模型建了 8 步计划,T7 = 第5步:五清扫sweep。它跑完了 sweep 脚本,拿到干净结果,然后:

          seq 223 task_update {"id":"T7","status":"completed","completionEvidence":"五清扫通过,零残留"}
          seq 225 → isError: "Invalid task status transition from pending to completed"
          seq 228 task_update {"id":"T8","status":"in_progress"}
          

          那就是模型收到的全文,没有截断。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:142Provide exactly one of subagent_id or legacy profile.——一句话里三个死胡同:不区分"一个都没给"和"两个都给了"、不列出合法的 profile 值、不说 subagent_id 从哪来(agent_list)。#2240agent-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 的静态阅读,不是观察到的失败。

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            help wantedExtra attention is needed

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' bug(runtime): a refusal that names the violation but not the exit left a completed task `pending` forever · Issue #2310 · apache/maka · GitHub
              Skip to content

              bug(runtime): a refusal that names the violation but not the exit left a completed task pending forever #2310

              Description

              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

              seq 149 function_call task_update {"id":"T4","status":"completed"}
              seq 150 function_response "Tool \"task_update\" arguments failed validation: [
              { \"code\": \"custom\",
              \"message\": \"completionEvidence is required when status is completed.\",
              \"path\": [\"completionEvidence\"] } ]
              task_update takes `id`, `status`, `subject`, `blockedReason`,
              `failureReason`, `completionEvidence`, `explicitReopen`."
              seq 153 function_call task_update {"id":"T4","status":"completed",
              "completionEvidence":"裁决完成:…"}
              

              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:142agent_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:

              SiteNowShould also say
              packages/core/src/task-ledger.ts:460Invalid task status transition from pending to completedthat pending → in_progress → completed is the path, i.e. call task_update with in_progress first
              packages/runtime/src/subagent-tools.ts:142Provide 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:214completionEvidence 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.

              Interaction with #2290

              #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:

              1. 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.
              2. agent_spawn is 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.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.
              • I did not reproduce this deliberately — it surfaced while verifying fix(runtime): decide the tool call event's ledger lane at push time #2240 against a packaged build. n=1 for the abandonment behaviour specifically; a different model might have recovered.
              • The agent_spawn message is a static read of current main, not an observed failure.
              简体中文要点

              在打包版本的一次真实会话里,一个模型确实做完了的任务被永久留在账本的 pending 状态——因为挡住它的那句拒绝只说了违反哪条规则,没说哪一个调用能满足它。模型读完拒绝,推不出出路,直接放弃了这个任务。

              这和 #2234 是同一个形状,只是高了一层:#2234 是拒绝根本到不了模型;这条是拒绝到了模型手里,但没有用。

              经过:会话 a34d6343,run 86204787。模型建了 8 步计划,T7 = 第5步:五清扫sweep。它跑完了 sweep 脚本,拿到干净结果,然后:

              seq 223 task_update {"id":"T7","status":"completed","completionEvidence":"五清扫通过,零残留"}
              seq 225 → isError: "Invalid task status transition from pending to completed"
              seq 228 task_update {"id":"T8","status":"in_progress"}
              

              那就是模型收到的全文,没有截断。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:142Provide exactly one of subagent_id or legacy profile.——一句话里三个死胡同:不区分"一个都没给"和"两个都给了"、不列出合法的 profile 值、不说 subagent_id 从哪来(agent_list)。#2240agent-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 的静态阅读,不是观察到的失败。

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                help wantedExtra attention is needed

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' bug(runtime): a refusal that names the violation but not the exit left a completed task `pending` forever · Issue #2310 · apache/maka · GitHub
                  Skip to content

                  bug(runtime): a refusal that names the violation but not the exit left a completed task pending forever #2310

                  Description

                  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

                  seq 149 function_call task_update {"id":"T4","status":"completed"}
                  seq 150 function_response "Tool \"task_update\" arguments failed validation: [
                  { \"code\": \"custom\",
                  \"message\": \"completionEvidence is required when status is completed.\",
                  \"path\": [\"completionEvidence\"] } ]
                  task_update takes `id`, `status`, `subject`, `blockedReason`,
                  `failureReason`, `completionEvidence`, `explicitReopen`."
                  seq 153 function_call task_update {"id":"T4","status":"completed",
                  "completionEvidence":"裁决完成:…"}
                  

                  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:142agent_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:

                  SiteNowShould also say
                  packages/core/src/task-ledger.ts:460Invalid task status transition from pending to completedthat pending → in_progress → completed is the path, i.e. call task_update with in_progress first
                  packages/runtime/src/subagent-tools.ts:142Provide 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:214completionEvidence 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.

                  Interaction with #2290

                  #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:

                  1. 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.
                  2. agent_spawn is 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.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.
                  • I did not reproduce this deliberately — it surfaced while verifying fix(runtime): decide the tool call event's ledger lane at push time #2240 against a packaged build. n=1 for the abandonment behaviour specifically; a different model might have recovered.
                  • The agent_spawn message is a static read of current main, not an observed failure.
                  简体中文要点

                  在打包版本的一次真实会话里,一个模型确实做完了的任务被永久留在账本的 pending 状态——因为挡住它的那句拒绝只说了违反哪条规则,没说哪一个调用能满足它。模型读完拒绝,推不出出路,直接放弃了这个任务。

                  这和 #2234 是同一个形状,只是高了一层:#2234 是拒绝根本到不了模型;这条是拒绝到了模型手里,但没有用。

                  经过:会话 a34d6343,run 86204787。模型建了 8 步计划,T7 = 第5步:五清扫sweep。它跑完了 sweep 脚本,拿到干净结果,然后:

                  seq 223 task_update {"id":"T7","status":"completed","completionEvidence":"五清扫通过,零残留"}
                  seq 225 → isError: "Invalid task status transition from pending to completed"
                  seq 228 task_update {"id":"T8","status":"in_progress"}
                  

                  那就是模型收到的全文,没有截断。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:142Provide exactly one of subagent_id or legacy profile.——一句话里三个死胡同:不区分"一个都没给"和"两个都给了"、不列出合法的 profile 值、不说 subagent_id 从哪来(agent_list)。#2240agent-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 的静态阅读,不是观察到的失败。

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    help wantedExtra attention is needed

                    Type

                    No type

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' bug(runtime): a refusal that names the violation but not the exit left a completed task `pending` forever · Issue #2310 · apache/maka · GitHub
                      Skip to content

                      bug(runtime): a refusal that names the violation but not the exit left a completed task pending forever #2310

                      Description

                      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

                      seq 149 function_call task_update {"id":"T4","status":"completed"}
                      seq 150 function_response "Tool \"task_update\" arguments failed validation: [
                      { \"code\": \"custom\",
                      \"message\": \"completionEvidence is required when status is completed.\",
                      \"path\": [\"completionEvidence\"] } ]
                      task_update takes `id`, `status`, `subject`, `blockedReason`,
                      `failureReason`, `completionEvidence`, `explicitReopen`."
                      seq 153 function_call task_update {"id":"T4","status":"completed",
                      "completionEvidence":"裁决完成:…"}
                      

                      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:142agent_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:

                      SiteNowShould also say
                      packages/core/src/task-ledger.ts:460Invalid task status transition from pending to completedthat pending → in_progress → completed is the path, i.e. call task_update with in_progress first
                      packages/runtime/src/subagent-tools.ts:142Provide 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:214completionEvidence 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.

                      Interaction with #2290

                      #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:

                      1. 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.
                      2. agent_spawn is 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.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.
                      • I did not reproduce this deliberately — it surfaced while verifying fix(runtime): decide the tool call event's ledger lane at push time #2240 against a packaged build. n=1 for the abandonment behaviour specifically; a different model might have recovered.
                      • The agent_spawn message is a static read of current main, not an observed failure.
                      简体中文要点

                      在打包版本的一次真实会话里,一个模型确实做完了的任务被永久留在账本的 pending 状态——因为挡住它的那句拒绝只说了违反哪条规则,没说哪一个调用能满足它。模型读完拒绝,推不出出路,直接放弃了这个任务。

                      这和 #2234 是同一个形状,只是高了一层:#2234 是拒绝根本到不了模型;这条是拒绝到了模型手里,但没有用。

                      经过:会话 a34d6343,run 86204787。模型建了 8 步计划,T7 = 第5步:五清扫sweep。它跑完了 sweep 脚本,拿到干净结果,然后:

                      seq 223 task_update {"id":"T7","status":"completed","completionEvidence":"五清扫通过,零残留"}
                      seq 225 → isError: "Invalid task status transition from pending to completed"
                      seq 228 task_update {"id":"T8","status":"in_progress"}
                      

                      那就是模型收到的全文,没有截断。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:142Provide exactly one of subagent_id or legacy profile.——一句话里三个死胡同:不区分"一个都没给"和"两个都给了"、不列出合法的 profile 值、不说 subagent_id 从哪来(agent_list)。#2240agent-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 的静态阅读,不是观察到的失败。

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        help wantedExtra attention is needed

                        Type

                        No type

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' bug(runtime): a refusal that names the violation but not the exit left a completed task `pending` forever · Issue #2310 · apache/maka · GitHub
                          Skip to content

                          bug(runtime): a refusal that names the violation but not the exit left a completed task pending forever #2310

                          Description

                          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

                          seq 149 function_call task_update {"id":"T4","status":"completed"}
                          seq 150 function_response "Tool \"task_update\" arguments failed validation: [
                          { \"code\": \"custom\",
                          \"message\": \"completionEvidence is required when status is completed.\",
                          \"path\": [\"completionEvidence\"] } ]
                          task_update takes `id`, `status`, `subject`, `blockedReason`,
                          `failureReason`, `completionEvidence`, `explicitReopen`."
                          seq 153 function_call task_update {"id":"T4","status":"completed",
                          "completionEvidence":"裁决完成:…"}
                          

                          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:142agent_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:

                          SiteNowShould also say
                          packages/core/src/task-ledger.ts:460Invalid task status transition from pending to completedthat pending → in_progress → completed is the path, i.e. call task_update with in_progress first
                          packages/runtime/src/subagent-tools.ts:142Provide 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:214completionEvidence 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.

                          Interaction with #2290

                          #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:

                          1. 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.
                          2. agent_spawn is 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.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.
                          • I did not reproduce this deliberately — it surfaced while verifying fix(runtime): decide the tool call event's ledger lane at push time #2240 against a packaged build. n=1 for the abandonment behaviour specifically; a different model might have recovered.
                          • The agent_spawn message is a static read of current main, not an observed failure.
                          简体中文要点

                          在打包版本的一次真实会话里,一个模型确实做完了的任务被永久留在账本的 pending 状态——因为挡住它的那句拒绝只说了违反哪条规则,没说哪一个调用能满足它。模型读完拒绝,推不出出路,直接放弃了这个任务。

                          这和 #2234 是同一个形状,只是高了一层:#2234 是拒绝根本到不了模型;这条是拒绝到了模型手里,但没有用。

                          经过:会话 a34d6343,run 86204787。模型建了 8 步计划,T7 = 第5步:五清扫sweep。它跑完了 sweep 脚本,拿到干净结果,然后:

                          seq 223 task_update {"id":"T7","status":"completed","completionEvidence":"五清扫通过,零残留"}
                          seq 225 → isError: "Invalid task status transition from pending to completed"
                          seq 228 task_update {"id":"T8","status":"in_progress"}
                          

                          那就是模型收到的全文,没有截断。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:142Provide exactly one of subagent_id or legacy profile.——一句话里三个死胡同:不区分"一个都没给"和"两个都给了"、不列出合法的 profile 值、不说 subagent_id 从哪来(agent_list)。#2240agent-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 的静态阅读,不是观察到的失败。

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            help wantedExtra attention is needed

                            Type

                            No type

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); bug(runtime): a refusal that names the violation but not the exit left a completed task `pending` forever · Issue #2310 · apache/maka · GitHub
                              Skip to content

                              bug(runtime): a refusal that names the violation but not the exit left a completed task pending forever #2310

                              Description

                              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

                              seq 149 function_call task_update {"id":"T4","status":"completed"}
                              seq 150 function_response "Tool \"task_update\" arguments failed validation: [
                              { \"code\": \"custom\",
                              \"message\": \"completionEvidence is required when status is completed.\",
                              \"path\": [\"completionEvidence\"] } ]
                              task_update takes `id`, `status`, `subject`, `blockedReason`,
                              `failureReason`, `completionEvidence`, `explicitReopen`."
                              seq 153 function_call task_update {"id":"T4","status":"completed",
                              "completionEvidence":"裁决完成:…"}
                              

                              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:142agent_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:

                              SiteNowShould also say
                              packages/core/src/task-ledger.ts:460Invalid task status transition from pending to completedthat pending → in_progress → completed is the path, i.e. call task_update with in_progress first
                              packages/runtime/src/subagent-tools.ts:142Provide 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:214completionEvidence 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.

                              Interaction with #2290

                              #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:

                              1. 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.
                              2. agent_spawn is 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.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.
                              • I did not reproduce this deliberately — it surfaced while verifying fix(runtime): decide the tool call event's ledger lane at push time #2240 against a packaged build. n=1 for the abandonment behaviour specifically; a different model might have recovered.
                              • The agent_spawn message is a static read of current main, not an observed failure.
                              简体中文要点

                              在打包版本的一次真实会话里,一个模型确实做完了的任务被永久留在账本的 pending 状态——因为挡住它的那句拒绝只说了违反哪条规则,没说哪一个调用能满足它。模型读完拒绝,推不出出路,直接放弃了这个任务。

                              这和 #2234 是同一个形状,只是高了一层:#2234 是拒绝根本到不了模型;这条是拒绝到了模型手里,但没有用。

                              经过:会话 a34d6343,run 86204787。模型建了 8 步计划,T7 = 第5步:五清扫sweep。它跑完了 sweep 脚本,拿到干净结果,然后:

                              seq 223 task_update {"id":"T7","status":"completed","completionEvidence":"五清扫通过,零残留"}
                              seq 225 → isError: "Invalid task status transition from pending to completed"
                              seq 228 task_update {"id":"T8","status":"in_progress"}
                              

                              那就是模型收到的全文,没有截断。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:142Provide exactly one of subagent_id or legacy profile.——一句话里三个死胡同:不区分"一个都没给"和"两个都给了"、不列出合法的 profile 值、不说 subagent_id 从哪来(agent_list)。#2240agent-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 的静态阅读,不是观察到的失败。

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                help wantedExtra attention is needed

                                Type

                                No type

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions