mastra-ai/mastra · error · Error

Thread with id ${newThreadId} already exists

Error message

Thread with id ${newThreadId} already exists

What it means

cloneThread throws this error when the target thread id (the provided newThreadId, or a generated one in the astronomically unlikely UUID collision case) already exists in the thread map. Cloning must produce a new unique thread, so an existing target id is treated as a conflict. Only applies when newThreadId is explicitly supplied or a UUID collides.

Source

Thrown at packages/core/src/storage/domains/memory/inmemory.ts:666

    this.db.resources.set(resourceId, resource);
    return resource;
  }

  async cloneThread(args: StorageCloneThreadInput): Promise<StorageCloneThreadOutput> {
    const { sourceThreadId, newThreadId: providedThreadId, resourceId, title, metadata, options } = args;

    // Get the source thread
    const sourceThread = this.db.threads.get(sourceThreadId);
    if (!sourceThread) {
      throw new Error(`Source thread with id ${sourceThreadId} not found`);
    }

    // Use provided ID or generate a new one
    const newThreadId = providedThreadId || crypto.randomUUID();

    // Check if the new thread ID already exists
    if (this.db.threads.has(newThreadId)) {
      throw new Error(`Thread with id ${newThreadId} already exists`);
    }

    // Get messages from the source thread
    let sourceMessages = Array.from(this.db.messages.values())
      .filter((msg: StorageMessageType) => msg.thread_id === sourceThreadId)
      .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());

    // Apply message filters if provided
    if (options?.messageFilter) {
      const { startDate, endDate, messageIds } = options.messageFilter;

      if (messageIds && messageIds.length > 0) {
        const messageIdSet = new Set(messageIds);
        sourceMessages = sourceMessages.filter(msg => messageIdSet.has(msg.id));
      }

      if (startDate) {
        sourceMessages = sourceMessages.filter(msg => new Date(msg.createdAt) >= startDate);

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Omit newThreadId and let cloneThread generate a fresh crypto.randomUUID().
  2. Check existence first: if (await storage.getThreadById({ threadId: newThreadId })) pick another id.
  3. Treat the error as an idempotent success if the prior clone is known to have completed, or catch and retry with a new id.

Example fix

// before
await storage.cloneThread({ sourceThreadId, newThreadId: fixedId });
// after
const exists = await storage.getThreadById({ threadId: fixedId });
if (!exists) {
  await storage.cloneThread({ sourceThreadId });
}
Defensive patterns

Strategy: try-catch

Validate before calling

const existing = await storage.getThreadById({ threadId: newThreadId });
if (existing) {
  throw new Error(`Thread ${newThreadId} already exists`);
}

Try / catch

try {
  await storage.cloneThread({ sourceThreadId, newThreadId });
} catch (e) {
  if (e instanceof Error && e.message.includes('already exists')) {
    // treat as idempotent success or retry with a generated id
    await storage.cloneThread({ sourceThreadId });
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling cloneThread({ sourceThreadId, newThreadId: 'existing-id' }) where existing-id already names a thread in the store; retrying a clone that already succeeded with the same explicit id; concurrent clones racing with the same newThreadId.

Common situations: Re-running an idempotency-broken retry after a timeout; two users/requests cloning into the same target id; seeding scripts executed twice with fixed ids; omitting newThreadId intentionally to auto-generate (the fix for most cases).

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/905a69d740d8041e. Report an issue: GitHub.