Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,38 @@ await AwaitAssertAsync(() =>
ClearReplyClientThrows();
}

[Fact]
public async Task Feedback_send_failure_faults_the_actor()
{
// Contract: when the session feedback pipe itself fails, the binding
// actor must fail loudly. A swallowed failure leaves a zombie session
// that waits on a delivery report that will never arrive. The loud
// path is a supervised restart, which re-creates the pipeline.
var ct = TestContext.Current.CancellationToken;
var detector = new ConfigurablePromptInjectionDetector(PromptInjectionResult.Safe());
var sid = new SessionId("session-feedback-fail");
var pipeline = new RecordingSessionPipeline(_ =>
[
new TextOutput("this will fail to post") { SessionId = sid },
new TurnCompleted { SessionId = sid, TurnNumber = new Netclaw.Actors.Protocol.TurnNumber(1) }
])
{
FeedbackException = new InvalidOperationException("feedback pipe down")
};

SetReplyClientThrows(new InvalidOperationException("channel API down"));
CreateBindingActor(sid, pipeline, detector);

// A supervised restart shows up as a second pipeline CreateAsync call.
await AwaitAssertAsync(
() => Assert.True(
pipeline.CreateCount >= 2,
$"expected a supervised restart to re-create the pipeline; CreateCount={pipeline.CreateCount}"),
cancellationToken: ct);

ClearReplyClientThrows();
}

// --- Pipeline Lifecycle ---

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,26 @@ public IReadOnlyList<IWithSessionId> RecordedFeedback
public ConcurrentQueue<ChannelInput> CapturedInputs { get; } = new();
public Func<IWithSessionId, CancellationToken, Task<ISessionResponse>>? ResponseFactory { get; set; }

/// <summary>
/// Number of <see cref="CreateAsync"/> calls. A supervised actor restart
/// re-creates the pipeline, so tests observe a restart as a second call.
/// </summary>
public int CreateCount => Volatile.Read(ref _createCount);
private int _createCount;

/// <summary>
/// When set, <see cref="SendFeedbackAsync"/> throws this exception and
/// does not record the feedback. This models a dead session feedback pipe.
/// </summary>
public Exception? FeedbackException { get; set; }

public Task<MaterializedSession> CreateAsync(
SessionId sessionId,
SessionPipelineOptions options,
IMaterializer? materializer = null,
CancellationToken cancellationToken = default)
{
Interlocked.Increment(ref _createCount);
Volatile.Write(ref _capturedOptions, options);
_created.TrySetResult(options);

Expand Down Expand Up @@ -131,6 +145,8 @@ public Task<MaterializedSession> CreateAsync(

public Task SendFeedbackAsync(IWithSessionId feedback, CancellationToken ct = default)
{
if (FeedbackException is { } feedbackException)
throw feedbackException;
lock (_feedbackLock) _recordedFeedback.Add(feedback);
return Task.CompletedTask;
}
Expand Down
6 changes: 5 additions & 1 deletion src/Netclaw.Channels.Discord/DiscordSessionBindingActor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1477,7 +1477,11 @@ await _dependencies.Pipeline.SendFeedbackAsync(new DeliveryFailed
}
catch (Exception ex)
{
_log.Error(ex, "Failed to send delivery feedback to session");
// A dead feedback pipe means the session never learns the turn
// failed. Rethrow so supervision restarts the actor and
// re-creates the pipeline, same as the Slack binding actor.
_log.Error(ex, "Failed to send delivery feedback to session; propagating to trigger pipeline reinit");
throw;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1417,7 +1417,11 @@ await _dependencies.Pipeline.SendFeedbackAsync(new DeliveryFailed
}
catch (Exception ex)
{
_log.Error(ex, "Failed to send delivery feedback to session");
// A dead feedback pipe means the session never learns the turn
// failed. Rethrow so supervision restarts the actor and
// re-creates the pipeline, same as the Slack binding actor.
_log.Error(ex, "Failed to send delivery feedback to session; propagating to trigger pipeline reinit");
throw;
}
}

Expand Down
Loading