{"record":{"id":"de3a6173ab2d2cd2","repo":"paperclipai/paperclip","slug":"conversation-session-changed-this-older-turn-cannot-resume","errorCode":null,"errorMessage":"Conversation session changed; this older turn cannot resume","messagePattern":"Conversation session changed; this older turn cannot resume","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"server/src/services/agent-conversations.ts","lineNumber":150,"sourceCode":"          .select()\n          .from(issueComments)\n          .where(\n            and(\n              eq(issueComments.id, commentId),\n              eq(issueComments.issueId, issueId),\n              eq(issueComments.companyId, run.companyId),\n            ),\n          )\n      : [];\n    const reset = Boolean(\n      comment && comment.authorUserId && isConversationReset(comment.body),\n    );\n    let generation = issue.conversationSessionGeneration;\n    if (\n      typeof context.conversationSessionGeneration === \"number\" &&\n      context.conversationSessionGeneration !== generation\n    ) {\n      throw new Error(\n        \"Conversation session changed; this older turn cannot resume\",\n      );\n    }\n    // The boundary lives on the command comment. A crash/retry reuses it instead of resetting twice.\n    if (reset && comment && comment.conversationSessionGeneration == null) {\n      generation += 1;\n      await tx\n        .update(issues)\n        .set({\n          conversationSessionGeneration: generation,\n          conversationBoundaryCommentId: comment.id,\n          updatedAt: new Date(),\n        })\n        .where(eq(issues.id, issue.id));\n      await tx\n        .update(issueComments)\n        .set({ conversationSessionGeneration: generation })\n        .where(eq(issueComments.id, comment.id));","sourceCodeStart":132,"sourceCodeEnd":168,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/agent-conversations.ts#L132-L168","documentation":"prepareConversationTurn guards against stale turns: each conversation issue carries a conversationSessionGeneration that increments whenever the session is reset. If the caller passes a context whose conversationSessionGeneration is a number that no longer matches the issue's current generation, the turn is from an older session and is refused — resuming it would continue the wrong agent session state.","triggerScenarios":"Enqueueing or preparing a conversation turn after a reset occurred (e.g. a reset command comment bumped the generation) while the caller still holds the pre-reset generation; a UI or automation retrying an old turn payload after the session was regenerated.","commonSituations":"A user hits 'reset conversation' while an agent response is still pending; a queued/retried turn from before a crash-recovery reset; multiple board tabs where one reset the session and another submits a follow-up message.","solutions":["Discard the stale turn and submit a new message so it carries the current session generation.","Refresh the issue/conversation state (re-fetch) and retry with the updated conversationSessionGeneration.","Avoid resetting the session while turns are queued; wait for pending turns to drain first.","If this happens on retries, drop idempotency replays of old turn payloads after any reset."],"exampleFix":"// before\nawait prepareConversationTurn(issue.id, { conversationSessionGeneration: savedGeneration, ... });\n// after\nconst issue = await getIssue(issue.id);\nif (issue.conversationSessionGeneration !== savedGeneration) {\n  savedGeneration = issue.conversationSessionGeneration; // refresh to current session\n}\nawait prepareConversationTurn(issue.id, { conversationSessionGeneration: savedGeneration, ... });","handlingStrategy":"type-guard","validationCode":"const current = (await getIssue(issueId)).conversationSessionGeneration;\nif (typeof pendingTurn.conversationSessionGeneration === \"number\" &&\n    pendingTurn.conversationSessionGeneration !== current) {\n  pendingTurn = null; // stale turn; resubmit instead\n}","typeGuard":"function canResume(turn, issue) {\n  return typeof turn.conversationSessionGeneration !== \"number\" ||\n         turn.conversationSessionGeneration === issue.conversationSessionGeneration;\n}","tryCatchPattern":"try { await prepareConversationTurn(issueId, ctx); }\ncatch (e) {\n  if (e.message.includes(\"older turn cannot resume\")) {\n    await refreshConversation(issueId); // discard stale turn, start fresh\n    return;\n  }\n  throw e;\n}","preventionTips":["Re-fetch issue state before submitting queued turns","Do not reset the conversation while turns are pending","Drop idempotent replays of pre-reset turn payloads","Refresh UI state after any session reset in another tab"],"tags":["conversation","stale-state","session"],"backgroundTag":"invalid-state-transition","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}