diff --git a/src/Netclaw.Actors.Tests/Channels/DiscordRoutingPolicyTests.cs b/src/Netclaw.Actors.Tests/Channels/DiscordRoutingPolicyTests.cs index 7d49de808..118a4be40 100644 --- a/src/Netclaw.Actors.Tests/Channels/DiscordRoutingPolicyTests.cs +++ b/src/Netclaw.Actors.Tests/Channels/DiscordRoutingPolicyTests.cs @@ -57,7 +57,7 @@ public void ExistingThread_ContinuesWithoutMention() } [Fact] - public void ThreadReply_RequiresMention_WhenNoExistingActor() + public void ThreadReply_RehydratesSession_WhenNoExistingActor() { var message = CreateMessage(text: "follow up", rootMessageId: null, isThread: true); @@ -69,8 +69,8 @@ public void ThreadReply_RequiresMention_WhenNoExistingActor() threadExists: false, containsBotMention: false); - Assert.Equal(DiscordRoutingDecisionKind.Ignore, decision.Kind); - Assert.Equal(DiscordRoutingIgnoreReason.ChannelMentionRequired, decision.IgnoreReason); + Assert.Equal(DiscordRoutingDecisionKind.StartOrContinue, decision.Kind); + Assert.Null(decision.IgnoreReason); } [Fact] @@ -227,6 +227,7 @@ private static DiscordGatewayMessage CreateMessage( IsDirectMessage: isDirectMessage, ContainsBotMention: false, Text: text, - ReceivedAt: DateTimeOffset.UtcNow); + ReceivedAt: DateTimeOffset.UtcNow, + IsInThread: isThread); } } diff --git a/src/Netclaw.Channels.Discord/DiscordRoutingPolicy.cs b/src/Netclaw.Channels.Discord/DiscordRoutingPolicy.cs index da1c92016..f6ca901ab 100644 --- a/src/Netclaw.Channels.Discord/DiscordRoutingPolicy.cs +++ b/src/Netclaw.Channels.Discord/DiscordRoutingPolicy.cs @@ -26,6 +26,12 @@ public static DiscordRoutingDecision Evaluate( if (threadExists) return DiscordRoutingDecision.ContinueOnly; + // Thread reply where the actor was lost (e.g. daemon restart): + // the message is in a Discord thread, so re-create the session + // binding and continue the persisted session. + if (message.IsInThread) + return DiscordRoutingDecision.StartOrContinue; + if (!mentionOnly) return DiscordRoutingDecision.StartOrContinue; diff --git a/src/Netclaw.Channels.Discord/DiscordTransportContracts.cs b/src/Netclaw.Channels.Discord/DiscordTransportContracts.cs index 687eb0a59..f9d2baac0 100644 --- a/src/Netclaw.Channels.Discord/DiscordTransportContracts.cs +++ b/src/Netclaw.Channels.Discord/DiscordTransportContracts.cs @@ -16,7 +16,8 @@ public sealed record DiscordGatewayMessage( bool ContainsBotMention, string Text, DateTimeOffset ReceivedAt, - IReadOnlyList? Attachments = null); + IReadOnlyList? Attachments = null, + bool IsInThread = false); /// /// Normalized Discord interaction response payload emitted by the transport client. diff --git a/src/Netclaw.Channels.Discord/Transport/DiscordNetGatewayClient.cs b/src/Netclaw.Channels.Discord/Transport/DiscordNetGatewayClient.cs index 8e5fcc541..d4d185819 100644 --- a/src/Netclaw.Channels.Discord/Transport/DiscordNetGatewayClient.cs +++ b/src/Netclaw.Channels.Discord/Transport/DiscordNetGatewayClient.cs @@ -119,7 +119,8 @@ private async Task OnMessageReceivedAsync(SocketMessage message) ContainsBotMention: containsMention, Text: message.Content, ReceivedAt: _timeProvider.GetUtcNow(), - Attachments: attachments); + Attachments: attachments, + IsInThread: isThread); try {