Skip to content

bug(runtime): ACP Session reports ready but stays busy after its result fails Runtime validation #5822

Description

@Aaron-Ben

What happened

When an external executor turn settles but the Runtime rejects its result before publishing it, the Plugin is never told to acknowledge or abandon that turn. The ACP Session then refuses every later message with acp_busy, while the executor picker keeps showing it as ready.

Chain on 2f3220552:

  1. AcpExecutor.execute sets awaitingAck and leaves its continuity record at prompt_pending once the prompt settles (packages/acp-executor-plugin/src/index.ts).
  2. PluginExecutorService.executeEntry runs the result through normalizeResult, which throws for a completion over MAX_EXECUTOR_COMPLETION_TEXT_BYTES (256 KiB) (packages/runtime/src/plugin-executor-service.ts).
  3. PluginExecutorBackend.#produce publishes a failure and returns false. The finally block in send only calls abandonExecution when returnedResult is true, so neither acknowledgeExecution nor abandonExecution runs (packages/runtime/src/plugin-executor-backend.ts:119-126).
  4. awaitingAck is never cleared. execute rejects every later prompt as busy, but inspectConversation does not check awaitingAck and still reports ready. After a Host restart, the stored prompt_pending record surfaces as history_gap.

Expected: the turn is abandoned and the Session shows history_gap right away, as it already does when the terminal event is not consumed or the acknowledgement fails (P2 #1 in Astro-Han's review on #5670). The task still cannot continue after that, which is intended because the transcript really is missing the reply. The defect is the contradictory ready/busy state, which leaves the user retrying with no guidance until they restart.

How to reproduce

Reproduced end to end in Desktop (npm run dev:worktree). A fake ACP Agent stands in for Antigravity, so no Google sign-in is needed (script below).

  1. Save the fake Agent as ~/maka-fake-acp/agent. Next to it, create an executable localharness_external that just runs exit 0, then chmod +x both files.
  2. Settings → External Agents → Antigravity: select ~/maka-fake-acp/agent.
  3. New task, executor Antigravity, model Fake Model. Send hello: you get a normal reply.
  4. Send MAKA_BIG_REPLY: the Agent streams about 300 KiB, and the turn fails with Executor completion text exceeds the size limit.
  5. Open the executor picker: Antigravity still shows no status and lists Fake Model.
  6. Send hello: it fails with Antigravity ACP Session is busy. Every later message fails the same way.
Fake ACP Agent (~/maka-fake-acp/agent)
#!/usr/bin/env node
// Fake ACP Agent for end-to-end testing Maka's Antigravity executor path.
// A prompt starting with "MAKA_BIG_REPLY" replies with ~300 KiB of text (over Maka's 256 KiB limit);
// any other prompt gets a short echo reply.
const { createInterface } = require('node:readline');

const BIG_REPLY_KIB = 300;
let model = 'fake-model';
let promptId;

const modelOption = () => ({
  id: 'model',
  name: 'Model',
  category: 'model',
  type: 'select',
  currentValue: model,
  options: [{ value: 'fake-model', name: 'Fake Model' }],
});
const send = (value) => process.stdout.write(JSON.stringify({ jsonrpc: '2.0', ...value }) + '\n');
const respond = (id, result) => send({ id, result });
const text = (sessionId, chunk) =>
  send({
    method: 'session/update',
    params: { sessionId, update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: chunk } } },
  });

createInterface({ input: process.stdin }).on('line', (line) => {
  const m = JSON.parse(line);
  if (m.method === undefined) return;
  switch (m.method) {
    case 'initialize':
      return respond(m.id, {
        protocolVersion: 1,
        agentCapabilities: { loadSession: true, sessionCapabilities: { resume: {} } },
      });
    case 'session/new':
      return respond(m.id, { sessionId: `fake-${Date.now()}`, configOptions: [modelOption()] });
    case 'session/resume':
    case 'session/load':
      return respond(m.id, { configOptions: [modelOption()] });
    case 'session/set_config_option':
      if (m.params.configId === 'model') model = m.params.value;
      return respond(m.id, { configOptions: [modelOption()] });
    case 'session/cancel':
      if (promptId !== undefined) respond(promptId, { stopReason: 'cancelled' });
      promptId = undefined;
      return;
    case 'session/prompt': {
      const { sessionId, prompt } = m.params;
      const input = prompt.map((part) => part.text ?? '').join('');
      if (input.startsWith('MAKA_BIG_REPLY')) {
        const chunk = 'x'.repeat(1023) + '\n';
        for (let i = 0; i < BIG_REPLY_KIB; i++) text(sessionId, chunk);
      } else {
        text(sessionId, `fake reply (${input.length} chars received)`);
      }
      return respond(m.id, { stopReason: 'end_turn' });
    }
    default:
      if (m.id !== undefined) send({ id: m.id, error: { code: -32601, message: `Method not found: ${m.method}` } });
  }
});

The same chain also reproduces with the existing test harnesses. For an oversize completion, PluginExecutorBackend records neither acknowledgeExecution nor abandonExecution. AcpExecutor then reports ready from inspectConversation, while execute returns acp_busy.

Environment

  • Maka commit: 2f32205 (source, npm run dev:worktree)
  • OS: macOS 15 (Darwin 24.6.0), arm64
  • Surface: Desktop + Runtime Host, Antigravity ACP executor (fake Agent)
  • Node.js: v24.15.0

Logs, screenshots, or additional context

Plugin continuity record after step 6 (plugin storage under ~/Library/Caches/Maka/runtime-hosts/<rootId>/plugin-data/):

{"phase":"prompt_pending","pendingTurnId":"1d446523-…","committedPrompts":1}

committedPrompts: 1 shows the first hello was acknowledged normally. pendingTurnId is the MAKA_BIG_REPLY turn, whose stored terminal event is the size-limit error. An abandoned turn would have moved the record to history_gap and closed the Agent process, but the fake Agent process is still running.

Screenshots: expanded turn errors (size limit, then busy); executor picker after the failure still showing Antigravity ready.

Proposed fix: in PluginExecutorBackend.send, call abandonExecution whenever the execution was not acknowledged, not only when a result was published. AcpExecutor.abandonExecution ignores turns that are not awaiting acknowledgement, so the extra call does nothing for failures that happen before the prompt settles. Add a regression test in plugin-executor-backend.test.ts asserting the abandonment for an oversize completion.

Refs #5103, #5670

This issue was drafted with Claude Code. I reproduced the behavior and reviewed the text.

Image Image

Activity

  1. Aaron-Ben commented on Sep 29, 2026

    @Aaron-Ben
    ContributorAuthor

    take

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions