elsa-workflows/elsa-core · error · InvalidOperationException

Cannot overwrite an AI conversation that belongs to another…

Error message

Cannot overwrite an AI conversation that belongs to another user.

What it means

InMemoryAIConversationStore.SaveAsync rejects overwriting an existing conversation whose UserId differs from the incoming one, when the stored record has a non-empty UserId. This prevents one user from replacing another user's conversation. Thrown from ValidateOwnership during SaveAsync.

Solutions

  1. Ensure the same UserId is supplied when updating an existing conversation.
  2. Use a fresh conversation ID for a different user.
  3. Delete the existing conversation first if reassignment is intended.
  4. Check that the current-user provider returns a stable UserId across calls.

Example fix

// before
await store.SaveAsync(new AIConversation { Id = id, TenantId = tenant, UserId = "user-b" });
// after
await store.SaveAsync(new AIConversation { Id = id, TenantId = tenant, UserId = "user-a" });
Defensive patterns

Strategy: validation

Validate before calling

var existing = await store.FindAsync(conversation.Id);
if (existing is not null && !string.IsNullOrWhiteSpace(existing.UserId) && existing.UserId != conversation.UserId)
    throw new InvalidOperationException("User mismatch for conversation " + conversation.Id);

Type guard

bool SameUser(AIConversation? existing, AIConversation c) => existing is null || string.IsNullOrWhiteSpace(existing.UserId) || existing.UserId == c.UserId;

Try / catch

try { await store.SaveAsync(conversation); } catch (InvalidOperationException ex) when (ex.Message.Contains("another user")) { logger.LogWarning(ex, "Cross-user save blocked for {Id}", conversation.Id); }

Prevention

When it happens

Trigger: Calling SaveAsync with a conversation whose Id matches an existing record but whose UserId differs from the stored UserId.

Common situations: Switching authenticated users in tests without clearing the store; user ID mismatches after auth refactors; replaying conversation saves under a different identity.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13). Data as JSON: /api/errors/270d77873f06ab6f. Report an issue: GitHub.

Appendix: source

Thrown at src/modules/Elsa.AI.Host/Services/InMemoryAIConversationStore.cs:63

    private bool IsExpired(AIConversation conversation)
    {
        if (conversation.RetentionMode == AIRetentionMode.Ephemeral)
            return conversation.Status is AIConversationStatus.Completed or AIConversationStatus.Failed;

        if (conversation.RetentionMode == AIRetentionMode.Durable)
            return false;

        var expiresAt = conversation.RetentionExpiresAt;
        return expiresAt.HasValue && expiresAt <= DateTimeOffset.UtcNow;
    }

    private static void ValidateOwnership(AIConversation existing, AIConversation conversation)
    {
        if (!string.Equals(NormalizeTenantId(existing.TenantId), NormalizeTenantId(conversation.TenantId), StringComparison.Ordinal))
            throw new InvalidOperationException("Cannot overwrite an AI conversation that belongs to another tenant.");

        if (!string.IsNullOrWhiteSpace(existing.UserId) && !string.Equals(existing.UserId, conversation.UserId, StringComparison.Ordinal))
            throw new InvalidOperationException("Cannot overwrite an AI conversation that belongs to another user.");
    }

    private static string NormalizeTenantId(string? tenantId) => tenantId ?? "";

    private static void Validate(AIConversation conversation)
    {
        if (string.IsNullOrWhiteSpace(conversation.Id))
            throw new ArgumentException("A conversation ID is required.", nameof(conversation));

        if (string.IsNullOrWhiteSpace(conversation.UserId))
            throw new ArgumentException("A conversation user ID is required.", nameof(conversation));
    }
}

View on GitHub (pinned to fe9217bdfa)