paperclipai/paperclip · error · SetupTokenSessionError

SETUP_TOKEN_SESSION_NOT_FOUND

SETUP_TOKEN_SESSION_NOT_FOUND

Error message

SETUP_TOKEN_SESSION_NOT_FOUND

What it means

SetupTokenSessionService.cancelByScope throws SetupTokenSessionError(404, SETUP_TOKEN_SESSION_NOT_FOUND) when neither a live in-memory session matching (sessionId, companyId, ownerUserId, adapterType) nor a durable row in a cancellable pre-promotion state exists. The code path deliberately collapses missing row, foreign owner, foreign company, foreign adapter, and non-cancellable state into this single 404 so callers cannot distinguish them.

Source

Thrown at server/src/services/setup-token-session.ts:1231

   * these apart.
   */
  async cancelByScope(
    sessionId: string,
    key: Pick<SetupTokenSessionScope, "companyId" | "ownerUserId" | "adapterType">,
  ): Promise<{ state: SetupTokenSessionState }> {
    const session = this.sessions.get(sessionId);
    if (
      session &&
      session.scope.companyId === key.companyId &&
      session.scope.ownerUserId === key.ownerUserId &&
      session.scope.adapterType === key.adapterType
    ) {
      return this.cancel(sessionId, session.scope);
    }
    const identity: SetupTokenCleanupIdentity = { sessionId, ...key };
    const cancelled = await this.store.cancelDurable(identity, SETUP_TOKEN_CANCELLABLE_STATES);
    if (!cancelled) {
      throw new SetupTokenSessionError(404, SETUP_TOKEN_SESSION_NOT_FOUND);
    }
    return { state: "cancelled" };
  }

  /**
   * Expires a session on a timeout. It stops the direct child before it releases
   * the lease. The harness can call it, and the deadline timer calls the same
   * path internally.
   */
  async expire(sessionId: string, scope: SetupTokenSessionScope): Promise<{ state: SetupTokenSessionState }> {
    const session = this.resolveOwned(sessionId, scope);
    if (isTerminalSessionState(session.state)) {
      return { state: session.state };
    }
    await this.terminate(session, "timed_out");
    return { state: session.state };
  }

View on GitHub (pinned to 01ad858492)

Solutions

  1. Verify the sessionId and the full scope key (companyId, ownerUserId, adapterType) exactly match the session as created — any mismatch yields this 404.
  2. Check the session's current durable state: if it already left the cancellable states (promoted/completed/cancelled), the 404 is expected; treat it as idempotent success in cleanup code.
  3. Re-fetch the active session via findActive (descriptor lookup) to confirm it still exists before cancelling.
  4. If a restart dropped the live session, rely on the durable fallback and ensure the row is still in a pre-promotion cancellable state; otherwise no cancel is needed.

Example fix

// before
await setupTokenSessions.cancelByScope(sessionId, key); // throws on already-cancelled
// after
try {
  await setupTokenSessions.cancelByScope(sessionId, key);
} catch (e) {
  if (e?.code === "SETUP_TOKEN_SESSION_NOT_FOUND") return { state: "cancelled" }; // idempotent cleanup
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

const active = await setupTokenSessions.findActive({ companyId, ownerUserId, adapterType });
if (!active || active.sessionId !== sessionId) {
  throw new Error(`No active setup-token session ${sessionId} for scope`);
}

Type guard

function isSetupTokenNotFoundError(e: unknown): e is SetupTokenSessionError {
  return e instanceof SetupTokenSessionError && e.code === "SETUP_TOKEN_SESSION_NOT_FOUND" && e.status === 404;
}

Try / catch

try {
  await setupTokenSessions.cancelByScope(sessionId, { companyId, ownerUserId, adapterType });
} catch (e) {
  if (isSetupTokenNotFoundError(e)) return { state: "cancelled" }; // already gone/non-cancellable: idempotent
  throw e;
}

Prevention

When it happens

Trigger: Calling cancelByScope(sessionId, {companyId, ownerUserId, adapterType}) when: the sessionId is wrong/already deleted; the scope key (companyId, ownerUserId, or adapterType) does not match the session's actual scope; the durable row is already past the cancellable states (e.g. already promoted, completed, or cancelled); or a restart dropped the live session and the durable fallback finds no cancellable row.

Common situations: A cleanup job cancels a session twice (second call hits the already-cancelled row); the harness passes an ownerUserId or adapterType that differs from the one the session was created with; the server restarted so the live session is gone and the durable row already transitioned out of SETUP_TOKEN_CANCELLABLE_STATES; stale sessionId cached from a previous login flow.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


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