diff --git a/dotnet/src/Microsoft.Agents.AI.CosmosNoSql/CosmosChatHistoryProvider.cs b/dotnet/src/Microsoft.Agents.AI.CosmosNoSql/CosmosChatHistoryProvider.cs index 82f6ab1c282..4e7574af698 100644 --- a/dotnet/src/Microsoft.Agents.AI.CosmosNoSql/CosmosChatHistoryProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI.CosmosNoSql/CosmosChatHistoryProvider.cs @@ -607,7 +607,10 @@ private sealed class CosmosMessageDocument [Newtonsoft.Json.JsonProperty("type")] public string Type { get; set; } = string.Empty; - [Newtonsoft.Json.JsonProperty("ttl")] + // Omit "ttl" from the document when null so Cosmos DB leaves TTL unset (disabled) instead of + // rejecting the write. Cosmos requires ttl to be a positive integer or -1; a literal null is + // invalid, so serializing MessageTtlSeconds = null must drop the property entirely. + [Newtonsoft.Json.JsonProperty("ttl", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public int? Ttl { get; set; } /// diff --git a/dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/CosmosChatHistoryProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/CosmosChatHistoryProviderTests.cs index 4b62e549c06..23a3a81014d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/CosmosChatHistoryProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/CosmosChatHistoryProviderTests.cs @@ -288,6 +288,52 @@ public async Task InvokedAsync_WithSingleMessage_ShouldAddMessageAsync() Assert.Equal(ChatRole.User, messageList[0].Role); } + [Fact] + [Trait("Category", "CosmosDB")] + public async Task InvokedAsync_WithNullMessageTtl_ShouldPersistWithoutTtlPropertyAsync() + { + // Arrange + this.SkipIfEmulatorNotAvailable(); + var session = CreateMockSession(); + var conversationId = Guid.NewGuid().ToString(); + using var provider = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, + _ => new CosmosChatHistoryProvider.State(conversationId)) + { + MessageTtlSeconds = null // Disable TTL. Previously this serialized ttl=null and Cosmos rejected the write. + }; + var message = new ChatMessage(ChatRole.User, "No TTL message"); + var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, session, [message], []); + + // Act - must not throw "The input ttl 'null' is invalid ..." + await provider.InvokedAsync(context); + + // Wait a moment for eventual consistency + await Task.Delay(100); + + // Assert - message persisted and the stored document omits the "ttl" property entirely. + var invokingContext = new ChatHistoryProvider.InvokingContext(s_mockAgent, session, []); + var messageList = (await provider.InvokingAsync(invokingContext)).ToList(); + Assert.Single(messageList); + Assert.Equal("No TTL message", messageList[0].Text); + + var rawQuery = new QueryDefinition("SELECT * FROM c WHERE c.conversationId = @conversationId") + .WithParameter("@conversationId", conversationId); + var rawIterator = this._setupClient!.GetDatabase(s_testDatabaseId).GetContainer(TestContainerId) + .GetItemQueryIterator(rawQuery, requestOptions: new QueryRequestOptions + { + PartitionKey = new PartitionKey(conversationId) + }); + + List rawDocs = []; + while (rawIterator.HasMoreResults) + { + rawDocs.AddRange(await rawIterator.ReadNextAsync()); + } + + Assert.Single(rawDocs); + Assert.False(rawDocs[0].ContainsKey("ttl"), "The 'ttl' property must be omitted when MessageTtlSeconds is null."); + } + [Fact] [Trait("Category", "CosmosDB")] public async Task InvokedAsync_WithMultipleMessages_ShouldAddAllMessagesAsync()