paperclipai/paperclip · error

OpenCode interruption is unavailable

Error message

OpenCode interruption is unavailable

What it means

The proxy throws this for a 'turn/interrupt' request when no session is open or the open OpenCode session object exposes no `interrupt` capability. Interruption is optional in the driver contract, so the proxy fails closed rather than silently ignoring the request.

Source

Thrown at packages/paperclip-runner/src/cli/opencode-app-server-proxy.ts:375

        message: { role: "user", text: messageText },
      });
      activeTurnId = turn.turnId;
      // OpenCode normally publishes session/turn startup over SSE, but a fast
      // completion can make the synchronous prompt response the only place the
      // authoritative turn id is observed. Emit the normalized notification
      // after this request's JSON-RPC response when SSE did not already announce
      // it. runnerd buffers notifications received while awaiting a response,
      // so replaying an earlier SSE announcement would violate strict turn
      // binding at the outer driver.
      queueMicrotask(() => {
        announceTurnStarted(session!, turn.turnId);
      });
      result = { turn: { id: turn.turnId, status: "inProgress" } };
      break;
    }
    case "turn/interrupt":
      if (!session?.interrupt)
        throw new Error("OpenCode interruption is unavailable");
      await session.interrupt({
        turnId: text(params.turnId, activeTurnId ?? ""),
      });
      result = true;
      break;
    case "thread/read":
      if (!session) throw new Error("OpenCode thread is not open");
      result = {
        thread: {
          id: session.ids().driverSessionId,
          cwd,
          turns: activeTurnId
            ? [{ id: activeTurnId, status: "inProgress" }]
            : [],
        },
        transcript: await session.read?.(),
      };
      break;

View on GitHub (pinned to 01ad858492)

Solutions

  1. Ensure 'thread/start' or 'thread/resume' completed successfully before interrupting.
  2. Check whether the underlying OpenCode driver session supports interrupt; disable the stop control if `session.interrupt` is absent.
  3. Upgrade the opencode CLI/adapter to a version whose session implements cancellation.
  4. As a fallback, let the turn complete or kill the underlying process instead of calling interrupt.

Example fix

// before
await rpc({ method: 'turn/interrupt', params: { turnId } });
// after
if (capabilities.interrupt) {
  await rpc({ method: 'turn/interrupt', params: { turnId } });
}
Defensive patterns

Strategy: type-guard

Validate before calling

const canInterrupt = session !== null && typeof session.interrupt === 'function';

Type guard

function supportsInterrupt(s): s is { interrupt: (p: { turnId: string }) => Promise<unknown> } {
  return s !== null && typeof (s as any).interrupt === 'function';
}

Try / catch

try {
  await rpc({ method: 'turn/interrupt', params: { turnId } });
} catch (e) {
  if (e.message.includes('interruption is unavailable')) {
    console.warn('turn cannot be cancelled for this session kind');
  } else throw e;
}

Prevention

When it happens

Trigger: Calling 'turn/interrupt' (a) before a thread is opened (session null), or (b) against an OpenCode session implementation that does not implement interrupt() (no cancel support in that transport/version).

Common situations: Client UI always shows a stop button even when the session kind lacks cancellation; opencode CLI version or transport without interrupt support; interrupt raced with session teardown.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/b9d1015759ec6294. Report an issue: GitHub.