paperclipai/paperclip · warning

Conversation session changed; this older turn cannot resume

Error message

Conversation session changed; this older turn cannot resume

What it means

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.

Solutions

  1. Discard the stale turn and submit a new message so it carries the current session generation.
  2. Refresh the issue/conversation state (re-fetch) and retry with the updated conversationSessionGeneration.
  3. Avoid resetting the session while turns are queued; wait for pending turns to drain first.
  4. If this happens on retries, drop idempotency replays of old turn payloads after any reset.

Example fix

// before
await prepareConversationTurn(issue.id, { conversationSessionGeneration: savedGeneration, ... });
// after
const issue = await getIssue(issue.id);
if (issue.conversationSessionGeneration !== savedGeneration) {
  savedGeneration = issue.conversationSessionGeneration; // refresh to current session
}
await prepareConversationTurn(issue.id, { conversationSessionGeneration: savedGeneration, ... });
Defensive patterns

Strategy: type-guard

Validate before calling

const current = (await getIssue(issueId)).conversationSessionGeneration;
if (typeof pendingTurn.conversationSessionGeneration === "number" &&
    pendingTurn.conversationSessionGeneration !== current) {
  pendingTurn = null; // stale turn; resubmit instead
}

Type guard

function canResume(turn, issue) {
  return typeof turn.conversationSessionGeneration !== "number" ||
         turn.conversationSessionGeneration === issue.conversationSessionGeneration;
}

Try / catch

try { await prepareConversationTurn(issueId, ctx); }
catch (e) {
  if (e.message.includes("older turn cannot resume")) {
    await refreshConversation(issueId); // discard stale turn, start fresh
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/de3a6173ab2d2cd2. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/agent-conversations.ts:150

          .select()
          .from(issueComments)
          .where(
            and(
              eq(issueComments.id, commentId),
              eq(issueComments.issueId, issueId),
              eq(issueComments.companyId, run.companyId),
            ),
          )
      : [];
    const reset = Boolean(
      comment && comment.authorUserId && isConversationReset(comment.body),
    );
    let generation = issue.conversationSessionGeneration;
    if (
      typeof context.conversationSessionGeneration === "number" &&
      context.conversationSessionGeneration !== generation
    ) {
      throw new Error(
        "Conversation session changed; this older turn cannot resume",
      );
    }
    // The boundary lives on the command comment. A crash/retry reuses it instead of resetting twice.
    if (reset && comment && comment.conversationSessionGeneration == null) {
      generation += 1;
      await tx
        .update(issues)
        .set({
          conversationSessionGeneration: generation,
          conversationBoundaryCommentId: comment.id,
          updatedAt: new Date(),
        })
        .where(eq(issues.id, issue.id));
      await tx
        .update(issueComments)
        .set({ conversationSessionGeneration: generation })
        .where(eq(issueComments.id, comment.id));

View on GitHub (pinned to 3f1d897a7c)