ACP: session rejects a new prompt as busy after answering the cancelled prompt with 'cancelled'

オープン
#569 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
53/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
javascript, node.js, typescript

調査の方向性

Start at the mcode acp session prompt and cancellation handling, then trace how an in-flight bash tool call is torn down and when the session's active-turn state is cleared. Compare the tool-call reproduction with the control run without a tool call in flight. Add or locate a regression test for sending a prompt immediately after a cancelled response; done when that prompt is accepted without waiting for a delay.

索引モデルが issue の本文から書いたものです。

説明

Product or interface

CLI — ACP (mcode acp), version 0.7.1 (macOS arm64)

Use case and problem

An ACP client that cancels a prompt turn and then sends a new prompt on the same session gets the new prompt rejected as busy, even though the agent already answered the cancelled prompt with stopReason: "cancelled".

Sequence that reproduces it:

  1. session/prompt starts a turn that runs a bash tool call (sleep 45).
  2. While the tool call is in_progress, the client sends session/cancel.
  3. The agent answers the original session/prompt with stopReason: "cancelled" (~40 ms later).
  4. The client immediately sends another session/prompt on the same session.
  5. The agent rejects it:
Internal error: MiniMax Code Runtime failed: Session already has an active Turn. Use queue send to deliver the message after it.
code: -32603

Sending the same prompt ~110 ms later succeeds.

Expected: once the agent has answered the cancelled prompt with the cancelled stop reason, the completed turn no longer prevents a new prompt on that session. Per the ACP cancellation spec the response marks "Agent completes prompt cancellation", so a client that waits for that response — as the spec explicitly allows — still cannot use the session.

Reproduction

Raw log (timestamps relative to process start, mcode acp over stdio via @agentclientprotocol/sdk 0.17.1):

[  8707ms] tool_call: bash
[  8808ms] tool call is running - cancelling
[ 10312ms] session/cancel sent (returned after 2 ms - it is a notification)
[ 10351ms] prompt#1: stopReason=cancelled after 41 ms
[ 10357ms] prompt#2 (sent 0ms after the cancelled answer): ERROR -32603 Internal error: MiniMax Code Runtime failed: Session already has an active Turn. Use queue send to deliver the message after it. after 6 ms
[ 10412ms] update: session_info_update {"updatedAt":"2026-10-30T15:39:56.835Z","sessionUpdate":"session_info_update"}
[ 10676ms] update: session_info_update {"updatedAt":"2026-10-30T15:39:57.099Z","sessionUpdate":"session_info_update"}
[ 11293ms] update: session_info_update {"updatedAt":"2026-10-30T15:39:57.715Z","sessionUpdate":"session_info_update"}
[ 11299ms] update: usage_update {"cost":{"amount":0,"currency":"USD"},"size":512000,"used":16743,"sessionUpdate":"usage_update"}
[ 11302ms] update: usage_update {"cost":{"amount":0,"currency":"USD"},"size":512000,"used":16743,"sessionUpdate":"usage_update"}
[ 11302ms] prompt#2 (sent 109ms after the cancelled answer): OK stopReason=end_turn after 842 ms
[ 12334ms] prompt#2 (sent 1253ms after the cancelled answer): OK stopReason=end_turn after 730 ms
[ 14017ms] prompt#2 (sent 2985ms after the cancelled answer): OK stopReason=end_turn after 681 ms

Control run without a tool call in flight — there the next prompt succeeds immediately after the cancelled answer, so the problem is tied to the tool call being torn down:

[  7931ms] session/cancel sent (returned after 0 ms - it is a notification)
[  8204ms] prompt#1 answered: {"stopReason":"cancelled"} after 273 ms
[ 10731ms] prompt#2: OK ({"stopReason":"end_turn"}) after 2527 ms

Note also that no terminal tool_call_update is emitted for the cancelled tool call — the only signal that the turn ended is the cancelled answer to the prompt.

Reproduction script
#!/usr/bin/env node
// Repro: mcode acp rejects the next prompt as busy even AFTER it answered the
// cancelled prompt with stopReason: "cancelled".
import { spawn } from "node:child_process";
import { ClientSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
import { Writable, Readable } from "node:stream";

const t0 = Date.now();
const ts = () => String(Date.now() - t0).padStart(6, " ");
const log = (...a) => console.log(`[${ts()}ms]`, ...a);

const child = spawn("mcode", ["acp"], { stdio: ["pipe", "pipe", "pipe"] });
child.stderr.on("data", (d) => process.env.DEBUG_ACP && log("stderr:", d.toString().trim()));

const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));

let toolStarted = false;
const client = {
  sessionUpdate: async (params) => {
    const u = params.update ?? {};
    if (u.sessionUpdate === "tool_call") {
      toolStarted = true;
      log("tool_call:", u.title ?? u.toolCallId);
      return;
    }
    if (u.sessionUpdate === "tool_call_update") {
      if (u.status === "in_progress") log("tool_call_update in_progress:", u.title ?? "");
      return;
    }
    if (u.sessionUpdate === "agent_message_chunk") return;
    log("update:", u.sessionUpdate, JSON.stringify(u).slice(0, 200));
  },
  requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
};

const conn = new ClientSideConnection(() => client, stream);
await conn.initialize({ protocolVersion: 1, clientCapabilities: { fs: {} } });
const { sessionId } = await conn.newSession({ cwd: process.cwd(), mcpServers: [] });
log("session:", sessionId);

const prompt1 = conn
  .prompt({
    sessionId,
    messageId: "m1",
    prompt: [{ type: "text", text: "Выполни в bash команду `sleep 45` и дождись её завершения." }],
  })
  .then((r) => ({ ok: true, r }), (e) => ({ ok: false, e }));

const deadline = Date.now() + 30000;
while (!toolStarted && Date.now() < deadline) await new Promise((r) => setTimeout(r, 100));
log(toolStarted ? "tool call is running - cancelling" : "tool call did not start - cancelling anyway");
await new Promise((r) => setTimeout(r, 1500));

const tCancel = Date.now();
await conn.cancel({ sessionId });
log("session/cancel sent (returned after", Date.now() - tCancel, "ms - it is a notification)");

const res1 = await prompt1;
const tResponse = Date.now();
log(
  "prompt#1:",
  res1.ok ? `stopReason=${res1.r.stopReason}` : `ERROR ${res1.e?.code} ${String(res1.e?.message).slice(0, 150)}`,
  "after",
  tResponse - tCancel,
  "ms",
);

for (const waitMs of [0, 100, 300, 1000]) {
  if (waitMs) await new Promise((r) => setTimeout(r, waitMs));
  const sentAt = Date.now();
  const res = await conn
    .prompt({
      sessionId,
      messageId: `m2-${waitMs}`,
      prompt: [{ type: "text", text: "Reply with one word: ok" }],
    })
    .then((r) => ({ ok: true, r }), (e) => ({ ok: false, e }));
  log(
    `prompt#2 (sent ${sentAt - tResponse}ms after the cancelled answer): ${
      res.ok
        ? `OK stopReason=${res.r.stopReason}`
        : `ERROR ${res.e?.code} ${String(res.e?.message).slice(0, 180)}`
    }`,
    "after",
    Date.now() - sentAt,
    "ms",
  );
}

child.kill("SIGTERM");
process.exit(0);
Additional context

This is the provider side of a busy-session problem reported against the Paseo ACP client: https://github.com/getpaseo/paseo/issues/3964

The related request to queue prompts for a busy session instead of rejecting them is #567.

主要言語
TypeScript
スター
2.2k
フォーク
281
平均マージ
16時間 32分
マージ済み PR(30日)
32

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

MiniMax-AI/minimax-code のほかの issue

MiniMax-AI/minimax-code の issue をすべて見る

似ている issue

TypeScript の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。