paperclipai/paperclip · error

OpenCode thread is not open

Error message

OpenCode thread is not open

What it means

The OpenCode app-server proxy translates JSON-RPC requests (runnerd protocol) into calls against an OpenCode driver session. 'turn/start' requires an open thread/session; the proxy throws this error when a turn is requested before 'thread/start' or 'thread/resume' succeeded (or after the session was never opened in this process).

Source

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

  let result: unknown;
  switch (message.method) {
    case "initialize":
      result = {
        user: { sessionId: "opencode" },
        serverInfo: { name: "opencode", version: "1.18.29" },
      };
      break;
    case "thread/start":
      result = await open(params, false);
      break;
    case "thread/resume":
      result = await open(params, true);
      break;
    case "collaborationMode/list":
      result = openCodeProxyCollaborationModes(activeModel);
      break;
    case "turn/start": {
      if (!session) throw new Error("OpenCode thread is not open");
      assertOpenCodeProxyCollaborationMode(params);
      const inputItems = Array.isArray(params.input)
        ? params.input.map(record)
        : [];
      const messageText = inputItems
        .map((entry) => text(entry.text))
        .filter(Boolean)
        .join("\n");
      const turn = await session.startTurn({
        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

View on GitHub (pinned to 01ad858492)

Solutions

  1. Send 'thread/start' (or 'thread/resume' with the existing thread id) before any 'turn/start'.
  2. Check proxy stderr for bootstrap failures (missing/misconfigured opencode CLI) that prevented the session from opening.
  3. Retry thread creation first, then re-issue the turn request.
  4. If resuming an existing conversation, use 'thread/resume' with the prior driver session id instead of assuming state persists.

Example fix

// before
await rpc({ method: 'turn/start', params: { input: [{ text: 'hi' }] } });
// after
await rpc({ method: 'thread/start', params: { cwd } });
await rpc({ method: 'turn/start', params: { input: [{ text: 'hi' }] } });
Defensive patterns

Strategy: try-catch

Validate before calling

let threadOpen = false;
async function ensureThread(params) {
  if (!threadOpen) {
    await rpc({ method: 'thread/start', params });
    threadOpen = true;
  }
}

Try / catch

try {
  await rpc({ method: 'turn/start', params });
} catch (e) {
  if (e.message.includes('thread is not open')) {
    await rpc({ method: 'thread/start', params: threadParams });
    await rpc({ method: 'turn/start', params });
  } else throw e;
}

Prevention

When it happens

Trigger: Sending a JSON-RPC 'turn/start' request to the proxy without first sending 'thread/start' or 'thread/resume', or after a prior 'thread/start' failed and left `session` null.

Common situations: Client reconnects and skips the initialize/thread handshake; a bootstrap failure (e.g. missing opencode binary) means the thread never opened but the client retries turns; test harness drives turn/start directly.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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