{"record":{"id":"b20049757c018383","repo":"paperclipai/paperclip","slug":"setup-token-session-not-found-b20049","errorCode":"SETUP_TOKEN_SESSION_NOT_FOUND","errorMessage":"SETUP_TOKEN_SESSION_NOT_FOUND","messagePattern":"SETUP_TOKEN_SESSION_NOT_FOUND","errorType":"http","errorClass":"SetupTokenSessionError","httpStatus":404,"severity":"error","filePath":"server/src/services/setup-token-session.ts","lineNumber":1231,"sourceCode":"   * these apart.\n   */\n  async cancelByScope(\n    sessionId: string,\n    key: Pick<SetupTokenSessionScope, \"companyId\" | \"ownerUserId\" | \"adapterType\">,\n  ): Promise<{ state: SetupTokenSessionState }> {\n    const session = this.sessions.get(sessionId);\n    if (\n      session &&\n      session.scope.companyId === key.companyId &&\n      session.scope.ownerUserId === key.ownerUserId &&\n      session.scope.adapterType === key.adapterType\n    ) {\n      return this.cancel(sessionId, session.scope);\n    }\n    const identity: SetupTokenCleanupIdentity = { sessionId, ...key };\n    const cancelled = await this.store.cancelDurable(identity, SETUP_TOKEN_CANCELLABLE_STATES);\n    if (!cancelled) {\n      throw new SetupTokenSessionError(404, SETUP_TOKEN_SESSION_NOT_FOUND);\n    }\n    return { state: \"cancelled\" };\n  }\n\n  /**\n   * Expires a session on a timeout. It stops the direct child before it releases\n   * the lease. The harness can call it, and the deadline timer calls the same\n   * path internally.\n   */\n  async expire(sessionId: string, scope: SetupTokenSessionScope): Promise<{ state: SetupTokenSessionState }> {\n    const session = this.resolveOwned(sessionId, scope);\n    if (isTerminalSessionState(session.state)) {\n      return { state: session.state };\n    }\n    await this.terminate(session, \"timed_out\");\n    return { state: session.state };\n  }\n","sourceCodeStart":1213,"sourceCodeEnd":1249,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/setup-token-session.ts#L1213-L1249","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait setupTokenSessions.cancelByScope(sessionId, key); // throws on already-cancelled\n// after\ntry {\n  await setupTokenSessions.cancelByScope(sessionId, key);\n} catch (e) {\n  if (e?.code === \"SETUP_TOKEN_SESSION_NOT_FOUND\") return { state: \"cancelled\" }; // idempotent cleanup\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"const active = await setupTokenSessions.findActive({ companyId, ownerUserId, adapterType });\nif (!active || active.sessionId !== sessionId) {\n  throw new Error(`No active setup-token session ${sessionId} for scope`);\n}","typeGuard":"function isSetupTokenNotFoundError(e: unknown): e is SetupTokenSessionError {\n  return e instanceof SetupTokenSessionError && e.code === \"SETUP_TOKEN_SESSION_NOT_FOUND\" && e.status === 404;\n}","tryCatchPattern":"try {\n  await setupTokenSessions.cancelByScope(sessionId, { companyId, ownerUserId, adapterType });\n} catch (e) {\n  if (isSetupTokenNotFoundError(e)) return { state: \"cancelled\" }; // already gone/non-cancellable: idempotent\n  throw e;\n}","preventionTips":["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."],"tags":["session","not-found","setup-token"],"backgroundTag":"record-not-found","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}