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 tenant.
What it means
InMemoryAIConversationStore.SaveAsync refuses to overwrite an existing AI conversation whose TenantId differs from the incoming one (after normalizing null to empty string). This guard prevents a caller from replacing another tenant's conversation record. It is thrown as an InvalidOperationException from ValidateOwnership, invoked during SaveAsync.
Solutions
- Ensure the conversation carries the same TenantId as the stored record.
- Use a unique conversation ID per tenant instead of reusing one.
- If the record truly must move tenants, delete the old record first and save as new.
- Verify tenant resolution middleware supplies the correct TenantId before saving.
Example fix
// before
await store.SaveAsync(new AIConversation { Id = id, TenantId = otherTenant, UserId = user });
// after
await store.SaveAsync(new AIConversation { Id = id, TenantId = existingTenantId, UserId = user }); Defensive patterns
Strategy: validation
Validate before calling
var existing = await store.FindAsync(conversation.Id);
if (existing is not null && !string.Equals(existing.TenantId ?? "", conversation.TenantId ?? "", StringComparison.Ordinal))
throw new InvalidOperationException("Tenant mismatch for conversation " + conversation.Id); Type guard
bool SameTenant(AIConversation? existing, AIConversation c) => existing is null || string.Equals(existing.TenantId ?? "", c.TenantId ?? "", StringComparison.Ordinal);
Try / catch
try { await store.SaveAsync(conversation); } catch (InvalidOperationException ex) when (ex.Message.Contains("another tenant")) { logger.LogWarning(ex, "Cross-tenant save blocked for {Id}", conversation.Id); } Prevention
- Always propagate TenantId from ambient tenant context when building conversations.
- Never reuse conversation IDs across tenants.
- Add integration tests for cross-tenant save rejection.
When it happens
Trigger: Calling SaveAsync with a conversation whose Id matches an existing record but whose TenantId differs (including one being null/empty and the other set).
Common situations: Multi-tenant apps where the tenant context is lost or defaulted between calls; reusing conversation IDs across tenants in tests or seeded data; migrating conversations without preserving TenantId.
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
- Cannot overwrite an AI conversation that belongs to another…
- Cannot overwrite an AI conversation that belongs to another…
- ' ' is not a well-formed permission. Expected ' : '.
- A conversation ID is required. (Parameter 'conversation')
- A conversation user ID is required. (Parameter…
AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13).
Data as JSON: /api/errors/97cf21991d46245d.
Report an issue: GitHub.
Appendix: source
Thrown at src/modules/Elsa.AI.Host/Services/InMemoryAIConversationStore.cs:60
_conversations.TryRemove(conversation.Id, out _);
}
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)