{"record":{"id":"05345f11c15c15f3","repo":"danny-avila/LibreChat","slug":"run-cancelled","errorCode":null,"errorMessage":"Run cancelled","messagePattern":"Run cancelled","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"info","filePath":"api/server/services/Runs/handle.js","lineNumber":135,"sourceCode":"\n    if (run.status !== lastSeenStatus) {\n      logger.debug(`[${run.status}] ${runInfo}`);\n      lastSeenStatus = run.status;\n    }\n\n    logger.debug(`[heartbeat ${i}] ${runStatus}`);\n\n    let cancelStatus;\n    try {\n      const timeoutMessage = `[heartbeat ${i}] ${runIdLog} | Cancel Status check operation timed out.`;\n      cancelStatus = await withTimeout(cache.get(cacheKey), raceTimeoutMs, timeoutMessage);\n    } catch (error) {\n      logger.warn(`Error retrieving cancel status: ${error}`);\n    }\n\n    if (cancelStatus === 'cancelled') {\n      logger.warn(`[waitForRun] ${runStatus} | RUN CANCELLED`);\n      throw new Error('Run cancelled');\n    }\n\n    if (![RunStatus.IN_PROGRESS, RunStatus.QUEUED].includes(run.status)) {\n      logger.debug(`[FINAL] ${runInfo} | status: ${run.status}`);\n      await runManager.fetchRunSteps({\n        openai,\n        thread_id: thread_id,\n        run_id: run_id,\n        runStatus: run.status,\n        final: true,\n      });\n      break;\n    }\n\n    // may use in future; for now, just fetch from the final status\n    await runManager.fetchRunSteps({\n      openai,\n      thread_id: thread_id,","sourceCodeStart":117,"sourceCodeEnd":153,"githubUrl":"https://github.com/danny-avila/LibreChat/blob/5ff282f9006c436e561de1afd39a481bea1ef0d8/api/server/services/Runs/handle.js#L117-L153","documentation":"Thrown by waitForRun when the ABORT_KEYS cache entry for the conversation is set to 'cancelled'. This is intentional control flow: a user (or system) marked the run for cancellation, so polling stops and the cancellation propagates up as an exception. It is a signal, not an infrastructure fault.","triggerScenarios":"The user clicks stop/abort on an in-progress assistant run; a parent flow sets cache.set(`${userId}:${conversationId}`, 'cancelled') under ABORT_KEYS; a stale cancel flag from a previous run on the same conversation key is still present.","commonSituations":"UI stop button; automated test that aborts; an earlier run on the same conversation was cancelled and the cache key was never cleared.","solutions":["Treat this exception as expected: catch it and report a 'cancelled' state to the client rather than logging it as an error.","If the cancellation is spurious, clear the ABORT_KEYS entry for the conversation key before starting a new run.","Ensure the cancel flag is written under the same key format `${userId}:${conversationId}` that waitForRun reads."],"exampleFix":"// before\nawait waitForRun({ openai, run_id, thread_id, runManager });\n\n// after\ntry {\n  await waitForRun({ openai, run_id, thread_id, runManager });\n} catch (err) {\n  if (err.message === 'Run cancelled') {\n    return { status: 'cancelled' };\n  }\n  throw err;\n}","handlingStrategy":"try-catch","validationCode":"const cancelStatus = await cache.get(`${userId}:${conversationId}`);\nif (cancelStatus === 'cancelled') {\n  return { status: 'cancelled' };\n}","typeGuard":"const isCancelled = async (cache, key) => (await cache.get(key)) === 'cancelled';","tryCatchPattern":"try {\n  await waitForRun({ openai, run_id, thread_id, runManager });\n} catch (err) {\n  if (err.message === 'Run cancelled') {\n    return { status: 'cancelled' };\n  }\n  throw err;\n}","preventionTips":["Treat 'Run cancelled' as expected control flow, not an error.","Clear the ABORT_KEYS entry for a conversation before starting a fresh run.","Write cancel flags with the exact `${userId}:${conversationId}` key format."],"tags":["openai","assistants","cancellation","control-flow"],"backgroundTag":null,"analyzedSha":"5ff282f9006c436e561de1afd39a481bea1ef0d8","analyzedAt":"2026-08-12T21:38:08.145Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}