paperclipai/paperclip · error

OpenCode recovery failed

Error message

OpenCode recovery failed

What it means

When opening with a resume path, the proxy asks the driver to recover a previous session from a snapshot; if the driver reports recovered=false or returns no session, the proxy throws, preferring the driver's reason string and falling back to the generic 'OpenCode recovery failed' message.

Source

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

    // runnerd gives this proxy its own process group. Keep `opencode serve` in
    // that same group so runnerd's TERM/KILL fallback cannot orphan it.
    isolateProcessGroup: false,
  });
  if (resume) {
    const threadId = text(params.threadId);
    const snapshot: PersistedHarnessSession = {
      driverKind: "opencode_server",
      driverSessionId: threadId,
      providerSessionId: threadId,
      runId: process.env.PAPERCLIP_RUN_ID ?? "runnerd-run",
      normalizedSessionId:
        process.env.PAPERCLIP_NORMALIZED_SESSION_ID ?? threadId,
      activeTurnId: null,
      lastSourceSequence: 0,
    };
    const recovered = await driver.recoverSession(snapshot);
    if (!recovered.recovered || !recovered.session)
      throw new Error(recovered.reason ?? "OpenCode recovery failed");
    session = recovered.session;
  } else {
    session = await driver.openSession({
      runId: process.env.PAPERCLIP_RUN_ID ?? "runnerd-run",
      normalizedSessionId:
        process.env.PAPERCLIP_NORMALIZED_SESSION_ID ?? `runnerd-${Date.now()}`,
      workingDirectory: cwd,
    });
  }
  eventPump = pumpEvents(session);
  void eventPump.catch((error) => failProxy(error));
  return threadResponse(
    session.ids().providerSessionId ?? session.ids().driverSessionId,
  );
}

function threadResponse(id: string): Record<string, unknown> {
  return { thread: { id, sessionId: id, cwd }, model: activeModel };

View on GitHub (pinned to 01ad858492)

Solutions

  1. Check the thrown message/recovered.reason for the specific recovery cause
  2. Start a fresh session instead of resuming (drop the resume path so openSession runs)
  3. Verify the normalized session id / thread id matches an existing runtime session

Example fix

// before
const s = await resumeSession(threadId); // throws if unrecoverable
// after
try { s = await resumeSession(threadId); }
catch { s = await openSession({ cwd, model }); } // fresh session fallback
Defensive patterns

Strategy: fallback

Validate before calling

const snapshot = buildSnapshot(threadId);
const probe = await driver.probeSession?.(snapshot.normalizedSessionId);
if (probe && !probe.exists) return openFreshSession();

Type guard

function isRecoverable(r: { recovered: boolean; session?: unknown; reason?: string }): r is { recovered: true; session: NonNullable<unknown> } { return r.recovered && r.session != null; }

Try / catch

try { await openWithRecovery(snapshot); }
catch (e) { if (String(e.message).includes('OpenCode recovery failed')) { log.warn('recovery failed, opening fresh session', e.message); await openFreshSession(); } else throw e; }

Prevention

When it happens

Trigger: driver.recoverSession(snapshot) returns { recovered: false } or { recovered: true, session: null } for the snapshot built from PAPERCLIP_NORMALIZED_SESSION_ID / threadId and lastSourceSequence 0.

Common situations: Resuming a session whose persisted runtime data was deleted; a stale or mismatched session id after a runtime reset; the underlying OpenCode app server no longer recognizes the thread.

Understand the failure class

Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.

Related errors


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