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
- Verify the sessionId and the full scope key (companyId, ownerUserId, adapterType) exactly match the session as created — any mismatch yields this 404.
- 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.
- Re-fetch the active session via findActive (descriptor lookup) to confirm it still exists before cancelling.
- 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
- Pass the exact scope values (companyId, ownerUserId, adapterType) used at session creation; any mismatch returns 404.
- Make cleanup idempotent: treat this 404 during cleanup as success since the code intentionally hides the specific cause.
- Re-read session state before cancelling; rows past cancellable states (promoted/completed/cancelled) can no longer be cancelled.
- Avoid caching sessionIds across process restarts; re-resolve via findActive first.
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
- session_unavailable
- Job not found
- Probe found no matching Anthropic Environment
- Probe found no matching Anthropic Agent
- Anthropic did not return a usable pinned Agent version
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/b20049757c018383.
Report an issue: GitHub.