paperclipai/paperclip · error

opencode_run_attach_busy

opencode_run_attach_busy

Error message

opencode_run_attach_busy

What it means

attachRun binds a new runId to an existing driver session and resets result bookkeeping. It is a synchronous, single-tenant operation: it throws opencode_run_attach_busy if a turn is still active on the session (#activeTurnId !== null), preventing two runs from interleaving on one OpenCode session.

Source

Thrown at packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.ts:508

          provider: input.model.split("/", 1)[0],
          opencodeVersion: input.runtime.version,
        },
      },
      { itemId: `${input.providerSessionId}:model` },
    );
  }

  ids() {
    return {
      driverSessionId: this.#providerSessionId,
      providerSessionId: this.#providerSessionId,
      displayId: this.#providerSessionId,
    };
  }

  attachRun(input: { runId: string }): void {
    if (this.#activeTurnId !== null)
      throw new Error("opencode_run_attach_busy");
    if (!input.runId) throw new Error("opencode_run_attach_invalid");
    this.#runId = input.runId;
    this.#result = null;
    this.#resultFingerprint = null;
    this.#resultCallId = null;
    this.#resultTurnId = null;
    this.#semanticResultTextBoundary = null;
    this.#semanticResultProviderMessageId = null;
    this.#lastNonTerminalToolSourceSeq = 0;
    this.#completedTextPartIds.clear();
    this.#completedReasoningPartIds.clear();
    this.#completedTextParts.length = 0;
    this.#terminalTurns.clear();
    this.#sendFullContext = false;
    this.#emit("run.attached", { runId: input.runId, sameSession: true });
  }

  events(): AsyncIterable<PrpEvent> {

View on GitHub (pinned to 01ad858492)

Solutions

  1. Await completion of the active turn (consume the run result / stream end) before calling attachRun.
  2. Use a fresh driver session for the new run instead of reattaching to a busy one.
  3. Serialize attachRun calls through a mutex/queue per session.
  4. If the previous turn is wedged, abort/cancel it so #activeTurnId clears, then retry attachRun.

Example fix

// before
driver.attachRun({ runId: nextRunId }); // previous turn still active -> throws
// after
await driver.waitForTurnCompletion(); // or await previousRun.result
driver.attachRun({ runId: nextRunId });
Defensive patterns

Strategy: retry

Validate before calling

if (driver.activeTurnId !== null && driver.activeTurnId !== undefined) await driver.waitForIdle();
driver.attachRun({ runId });

Type guard

const canAttach = (d: { activeTurnId: string | null }): boolean => d.activeTurnId === null;

Try / catch

try { driver.attachRun({ runId }); } catch (e) { if ((e as Error).message === 'opencode_run_attach_busy') { await backoff(); await driver.waitForIdle(); driver.attachRun({ runId }); } else throw e; }

Prevention

When it happens

Trigger: Calling attachRun while the previous run's turn is still in flight (stream not drained, result not consumed), or attaching a second run concurrently to the same driver instance.

Common situations: Runner restart logic reattaching before the prior run finished, missing await on the previous run promise, retry handlers attaching immediately after a failure without awaiting turn teardown, queue code reusing one driver for parallel tasks.

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/059b5e420f341de9. Report an issue: GitHub.