Repository navigation
fix(twilio): keep the lines a relay session says on its own in the history - #1491
Conversation
…story A dev call's last turn failed, the relay apologized and hung up, but the conversation ends with the caller's "Bye" and shows nothing the caller heard after it. What the session says on its own went out without being stored: - The apology of a failed or timed-out turn, and the fillers said before it: fillers were stored only with a reply, so a turn that threw or ran past AiResponseTimeoutSeconds lost them - Idle prompts (say-it-again-question, still-there, say-it-again-N) and the idle goodbye - A line said again after a cut-in nothing followed (ResumeAfterCutInSeconds) The session now takes a keepSaidLine callback, and the middleware gives it one that appends an assistant dialog out of the agent's context, at the time it was said, in a scope of its own, logging storage errors. Only a line that went out is kept, with its text rather than its prompt name, and a hang-up's lines only after the end frame, so the storage never holds up the hang-up. Fillers are noted on the turn's callbacks as they go out and taken once: by the turn with its reply, or by the session for a turn that failed, timed out or stored no reply. Tool-call indications stay the flow's to keep. The postback a keypad entry answers is read from the last dialog in the agent's context, as a kept line may now come after the reply that asked for it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
…up lines play to the end - Each line kept only as a record of what the caller heard carries what it was in its MessageLabel (ConversationRelayLabels: filler, idle prompt, idle goodbye, apology, said again), so a flow can tell an idle prompt from a line of the agent's: asked "what?" after "Are you still there?", it should say its question again, not the prompt - The apology of a failed turn and the idle goodbye go out uninterruptible: the call ends after them and nothing the caller says over them is answered, so a cut-in only cost the caller the end of a line the history keeps whole. Idle prompts and a line said again stay interruptible, as they wait for the caller to speak Co-Authored-By: Claude Opus 5.5 <[email protected]>
✅ AegisCode Code Review Report: PassCounted: 2 Medium, 23 Low. None of them blocks this change. Medium · .../BotSharp.Plugin.Twilio/Services/ConversationRelaySession.cs:61 · performance: When fillers remain after a turn completes, this finally block waits for every persistence callback…When fillers remain after a turn completes, this finally block waits for every persistence callback before reaching CloseUtteranceAsync and subsequent reply handling. KeepFillersLeftAsync awaits the callbacks sequentially, so their combined latency is added directly to the live conversation path. Unlike the failure path, which ends the call before persistence, this path makes speech processing wait for history storage. Suggested fix: [Severity capped to Medium: performance claim not confirmed by static analysis; reviewer rated High] [Needs Review - Inconclusive] Take ownership of the remaining fillers during cleanup, but persist them after closing the utterance and completing any reply delivery. Alternatively, use an ordered session-owned persistence queue that is drained during shutdown without delaying speech delivery. Medium · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:122 · logic: The hang-up test assumes that 300 ms is enough for the session to process the close frame.The hang-up test assumes that 300 ms is enough for the session to process the close frame. FakeWebSocket.Close() only enqueues null; it does not acknowledge that the session has observed the close or marked itself ended. On a delayed scheduler, fail.SetResult() can release the failing turn before close processing completes, so the test does not reliably establish the 'failure after hang-up' ordering it intends to verify. Suggested fix: [Not confirmed by a second review; reviewer rated High, needs a human decision] [Needs Review - Inconclusive] Replace the fixed delay with an explicit acknowledgement that the session has processed the close and entered its ended state before completing fail. Use a deterministic lifecycle signal rather than elapsed time. Low · .../BotSharp.Plugin.Twilio/Models/ConversationRelayLabels.cs:12 · Constant naming: Constant 'Filler' (line 12) is not UPPER_CASEConstant 'Filler' (line 12) is not UPPER_CASE Suggested fix: [Validated by static analysis] Rename 'Filler' to 'FILLER' Low · .../BotSharp.Plugin.Twilio/Models/ConversationRelayLabels.cs:19 · Constant naming: Constant 'IdlePrompt' (line 19) is not UPPER_CASEConstant 'IdlePrompt' (line 19) is not UPPER_CASE Suggested fix: [Validated by static analysis] Rename 'IdlePrompt' to 'IDLE_PROMPT' Low · .../BotSharp.Plugin.Twilio/Models/ConversationRelayLabels.cs:24 · Constant naming: Constant 'IdleGoodbye' (line 24) is not UPPER_CASEConstant 'IdleGoodbye' (line 24) is not UPPER_CASE Suggested fix: [Validated by static analysis] Rename 'IdleGoodbye' to 'IDLE_GOODBYE' Low · .../BotSharp.Plugin.Twilio/Models/ConversationRelayLabels.cs:29 · Constant naming: Constant 'Apology' (line 29) is not UPPER_CASEConstant 'Apology' (line 29) is not UPPER_CASE Suggested fix: [Validated by static analysis] Rename 'Apology' to 'APOLOGY' Low · .../BotSharp.Plugin.Twilio/Models/ConversationRelayLabels.cs:34 · Constant naming: Constant 'SaidAgain' (line 34) is not UPPER_CASEConstant 'SaidAgain' (line 34) is not UPPER_CASE Suggested fix: [Validated by static analysis] Rename 'SaidAgain' to 'SAID_AGAIN' Low · .../BotSharp.Plugin.Twilio/Services/ConversationRelaySession.cs:861 · Line length: This line is 137 characters long; the maximum is 125This line is 137 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Plugin.Twilio/Services/TwilioConversationProcessor.cs:51 · style: The new public asynchronous method KeepSaidLines lacks the Async suffix required by the C# naming…The new public asynchronous method KeepSaidLines lacks the Async suffix required by the C# naming standard. Its behavior is acceptable, but the name does not clearly communicate its asynchronous contract. Suggested fix: [Needs Review - Inconclusive] Rename the method to KeepSaidLinesAsync and update its callers and documentation references. Low · .../Plugins/BotSharp.Plugin.Twilio/TwilioConversationRelayMiddleware.cs:19 · style: KeepSaidLineAsync declares six parameters, exceeding the C# guideline of at most three.KeepSaidLineAsync declares six parameters, exceeding the C# guideline of at most three. The current forwarding code is acceptable functionally, but the adjacent string parameters make argument ordering harder to maintain. Suggested fix: [Needs Review - Inconclusive] Group the session identifiers and the said-line data into appropriately named parameter objects so the helper takes at most three parameters. Low · .../Plugins/BotSharp.Plugin.Twilio/TwilioConversationRelayMiddleware.cs:69 · Line length: This line is 126 characters long; the maximum is 125This line is 126 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:27 · Constant naming: Constant 'AgentId' (line 27) is not UPPER_CASEConstant 'AgentId' (line 27) is not UPPER_CASE Suggested fix: [Validated by static analysis] Rename 'AgentId' to 'AGENT_ID' Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:28 · Constant naming: Constant 'Filler' (line 28) is not UPPER_CASEConstant 'Filler' (line 28) is not UPPER_CASE Suggested fix: [Validated by static analysis] Rename 'Filler' to 'FILLER' Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:29 · Constant naming: Constant 'Apology' (line 29) is not UPPER_CASEConstant 'Apology' (line 29) is not UPPER_CASE Suggested fix: [Validated by static analysis] Rename 'Apology' to 'APOLOGY' Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:45 · Line length: This line is 134 characters long; the maximum is 125This line is 134 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:159 · Line length: This line is 139 characters long; the maximum is 125This line is 139 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:181 · Line length: This line is 139 characters long; the maximum is 125This line is 139 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:197 · Line length: This line is 126 characters long; the maximum is 125This line is 126 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:224 · Line length: This line is 134 characters long; the maximum is 125This line is 134 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:246 · Line length: This line is 128 characters long; the maximum is 125This line is 128 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:315 · Line length: This line is 134 characters long; the maximum is 125This line is 134 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:369 · Line length: This line is 135 characters long; the maximum is 125This line is 135 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:388 · Line length: This line is 143 characters long; the maximum is 125This line is 143 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:394 · Line length: This line is 137 characters long; the maximum is 125This line is 137 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:400 · Line length: This line is 137 characters long; the maximum is 125This line is 137 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters |
…nd a caller who said nothing isn't asked to say it again Found running calls against a local onebrain: - A turn's reply that comes back after the call ended (the caller hung up while the agent worked, or the turn ran past AiResponseTimeoutSeconds and the session apologized and hung up) was stored as a line the agent said, after the apology. With nothing of it sent, it is now stored as "[the call ended before this was said; the caller did not hear: "..."]", as a reply dropped for the caller going on talking is. RewriteReply is set for every relay turn, not only streamed ones; the other marks only ever apply to a streamed reply - Silence after the greeting got "Sorry, can you say it again?" though the caller had said nothing. Until they say or key something, an idle prompt only asks if they are still there Co-Authored-By: Claude Opus 5.5 <[email protected]>
✅ AegisCode Code Review Report: PassCounted: 2 Medium, 23 Low. None of them blocks this change. Not counted: 1 new since the baseline. Medium · .../BotSharp.Plugin.Twilio/Services/ConversationRelaySession.cs:71 · performance: When fillers remain, this finally block awaits KeepFillersLeftAsync, which sequentially awaits the…When fillers remain, this finally block awaits KeepFillersLeftAsync, which sequentially awaits the configured history callback for each filler. This happens before CloseUtteranceAsync and subsequent reply handling. Consequently, persistence latency is added to the live response path even though recording fillers does not need to precede finishing the outgoing utterance. The impact is conditional on remaining fillers and a configured persistence callback. Suggested fix: [Severity capped to Medium: performance claim not confirmed by static analysis; reviewer rated High] [Needs Review - Inconclusive] Take ownership of the remaining fillers after stopping the watchdog, but defer awaiting their persistence until after outgoing speech and reply handling are complete. Alternatively, use an ordered persistence queue that is drained during session shutdown without delaying speech or hang-up. Medium · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:96 · logic: The caller-hang-up tests assume that waiting 300 ms means the session has processed the close frame.The caller-hang-up tests assume that waiting 300 ms means the session has processed the close frame. FakeWebSocket.Close() only enqueues null; it does not acknowledge that the session has read it or marked the call ended. Under delayed scheduling, hungUp.SetResult() can release the reply before that transition, producing a spurious failure. The same race exists before fail.SetResult() at lines 154-155. Suggested fix: [Not confirmed by a second review; reviewer rated High, needs a human decision] [Needs Review - Inconclusive] Replace both fixed delays with an explicit completion signal for the session's ended-state transition. Release the pending turn only after that signal, with a bounded timeout. A signal that merely acknowledges enqueueing the close frame is insufficient. Low · .../BotSharp.Plugin.Twilio/Models/ConversationRelayLabels.cs:12 · Constant naming: Constant 'Filler' (line 12) is not UPPER_CASEConstant 'Filler' (line 12) is not UPPER_CASE Suggested fix: [Validated by static analysis] Rename 'Filler' to 'FILLER' Low · .../BotSharp.Plugin.Twilio/Models/ConversationRelayLabels.cs:19 · Constant naming: Constant 'IdlePrompt' (line 19) is not UPPER_CASEConstant 'IdlePrompt' (line 19) is not UPPER_CASE Suggested fix: [Validated by static analysis] Rename 'IdlePrompt' to 'IDLE_PROMPT' Low · .../BotSharp.Plugin.Twilio/Models/ConversationRelayLabels.cs:24 · Constant naming: Constant 'IdleGoodbye' (line 24) is not UPPER_CASEConstant 'IdleGoodbye' (line 24) is not UPPER_CASE Suggested fix: [Validated by static analysis] Rename 'IdleGoodbye' to 'IDLE_GOODBYE' Low · .../BotSharp.Plugin.Twilio/Models/ConversationRelayLabels.cs:29 · Constant naming: Constant 'Apology' (line 29) is not UPPER_CASEConstant 'Apology' (line 29) is not UPPER_CASE Suggested fix: [Validated by static analysis] Rename 'Apology' to 'APOLOGY' Low · .../BotSharp.Plugin.Twilio/Models/ConversationRelayLabels.cs:34 · Constant naming: Constant 'SaidAgain' (line 34) is not UPPER_CASEConstant 'SaidAgain' (line 34) is not UPPER_CASE Suggested fix: [Validated by static analysis] Rename 'SaidAgain' to 'SAID_AGAIN' Low · .../BotSharp.Plugin.Twilio/Services/ConversationRelaySession.cs:864 · Line length: This line is 137 characters long; the maximum is 125This line is 137 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Plugin.Twilio/Services/TwilioConversationProcessor.cs:51 · style: The new public asynchronous method KeepSaidLines lacks the Async suffix required by the C# naming…The new public asynchronous method KeepSaidLines lacks the Async suffix required by the C# naming standard. The implementation remains usable, but the suffix would make its asynchronous contract clearer to callers. Suggested fix: [Needs Review - Inconclusive] Rename the method to KeepSaidLinesAsync and update its callers and documentation references. Low · .../Plugins/BotSharp.Plugin.Twilio/TwilioConversationRelayMiddleware.cs:69 · Line length: This line is 126 characters long; the maximum is 125This line is 126 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../Plugins/BotSharp.Plugin.Twilio/TwilioConversationRelayMiddleware.cs:69 · style: KeepSaidLineAsync declares six parameters, exceeding the C# guideline of at most three.KeepSaidLineAsync declares six parameters, exceeding the C# guideline of at most three. The current forwarding code is acceptable functionally, but the adjacent string parameters make argument ordering harder to maintain. Suggested fix: [Carried from baseline review #11484: the code is unchanged and the finding was not re-reported] [Needs Review - Inconclusive] Group the session identifiers and the said-line data into appropriately named parameter objects so the helper takes at most three parameters. Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:27 · Constant naming: Constant 'AgentId' (line 27) is not UPPER_CASEConstant 'AgentId' (line 27) is not UPPER_CASE Suggested fix: [Validated by static analysis] Rename 'AgentId' to 'AGENT_ID' Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:28 · Constant naming: Constant 'Filler' (line 28) is not UPPER_CASEConstant 'Filler' (line 28) is not UPPER_CASE Suggested fix: [Validated by static analysis] Rename 'Filler' to 'FILLER' Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:29 · Constant naming: Constant 'Apology' (line 29) is not UPPER_CASEConstant 'Apology' (line 29) is not UPPER_CASE Suggested fix: [Validated by static analysis] Rename 'Apology' to 'APOLOGY' Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:45 · Line length: This line is 134 characters long; the maximum is 125This line is 134 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:192 · Line length: This line is 139 characters long; the maximum is 125This line is 139 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:214 · Line length: This line is 139 characters long; the maximum is 125This line is 139 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:251 · Line length: This line is 126 characters long; the maximum is 125This line is 126 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:278 · Line length: This line is 134 characters long; the maximum is 125This line is 134 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:300 · Line length: This line is 128 characters long; the maximum is 125This line is 128 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:369 · Line length: This line is 134 characters long; the maximum is 125This line is 134 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:423 · Line length: This line is 135 characters long; the maximum is 125This line is 135 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:442 · Line length: This line is 143 characters long; the maximum is 125This line is 143 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:448 · Line length: This line is 137 characters long; the maximum is 125This line is 137 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:454 · Line length: This line is 137 characters long; the maximum is 125This line is 137 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters 1 new since the baseline, not countedFound in this check but not in its baseline; they did not decide the result. Low · .../BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs:235 · Line length: This line is 138 characters long; the maximum is 125This line is 138 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters |
There was a problem hiding this comment.
🟡 Changes recommended
Active moderate issues remain in send-success reporting, dialog persistence ordering, and postback lookup, which can cause undelivered lines, chronologically incorrect history, or missed replies.
3 open issues
What changed in this PR
This PR updates Twilio ConversationRelay history persistence so conversation records include independently spoken agent/session lines—including fillers, idle prompts, apologies, goodbyes, and repeated lines—while excluding them from future agent context.
Changes:
- Adds labeled persistence and timestamps for session-generated speech.
- Tracks fillers across successful, failed, timed-out, and interrupted turns, and marks late replies that were not heard.
- Updates idle, interruption, timeout, and postback handling with comprehensive fake-WebSocket test coverage.
| File | Description |
|---|---|
| tests/BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs | Adds tests covering kept relay lines, fillers, failures, timeouts, idle behavior, interruptions, labels, and storage errors. |
| tests/BotSharp.Core.UnitTests/BotSharp.Core.UnitTests.csproj | Adds the Twilio plugin reference required by the new tests. |
| src/Plugins/BotSharp.Plugin.Twilio/TwilioConversationRelayMiddleware.cs | Provides scoped persistence callbacks for lines spoken by relay sessions. |
| src/Plugins/BotSharp.Plugin.Twilio/Settings/ConversationRelaySetting.cs | Documents the updated initial idle-prompt behavior when the caller has not spoken. |
| src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioConversationProcessor.cs | Persists labeled kept lines, tracks fillers, marks unheard late replies, and adjusts postback history lookup. |
| src/Plugins/BotSharp.Plugin.Twilio/Services/ConversationRelaySession.cs | Tracks and persists fillers and session-generated lines across relay lifecycle, timeout, interruption, and idle events. |
| src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationRelayLabels.cs | Defines labels for independently spoken relay lines. |
| src/Plugins/BotSharp.Plugin.Twilio/Models/AgentTurnCallbacks.cs | Replaces filler callbacks with thread-safe note-and-take tracking. |
Review effort: Lite
|
|
||
| try | ||
| { | ||
| await _keepSaidLine(text.Trim(), label, saidUtc); |
| /// <param name="keepUninterrupted">The caller must hear all of it, so it plays to the end whoever speaks over it</param> | ||
| private async Task SpeakAsync(string? text, Func<bool>? canSpeak = null, bool keepUninterrupted = false) | ||
| /// <returns>Whether it went out</returns> | ||
| private async Task<bool> SpeakAsync(string? text, Func<bool>? canSpeak = null, bool keepUninterrupted = false) |
| var lastMessage = messages[0]; | ||
| // A line kept only as a record of what the caller heard (KeepSaidLines) may have been said after the reply that | ||
| // asked for the postback | ||
| var lastMessage = (await conv.GetDialogHistory()).LastOrDefault(x => !x.ExcludeFromContext); |

Problem
On a ConversationRelay call, the conversation history should show what the caller heard. On a dev call the agent's last turn failed: the relay apologized and hung up, but the conversation page ends with the caller's "No. That's all. Thanks. Bye." and nothing the agent said after it.
A turn stores its own reply, but the session stored nothing it said on its own:
AiResponseTimeoutSeconds, and the fillers said before it. Fillers were stored only together with a reply, so a failed turn lost them.say-it-again-question,still-there,say-it-again-N) and theidle-goodbyebefore the hang-upResumeAfterCutInSeconds)Change
ConversationRelaySessiontakes an optionalkeepSaidLine(text, label, saidUtc)callback. Existing callers still compile.TwilioConversationRelayMiddlewaregives it one that creates its own service scope and callsTwilioConversationProcessor.KeepSaidLines. That method appends an assistantRoleDialogModelwithExcludeFromContext = true,CreatedAtset to when the line was said, and the label inMessageLabel. It logs storage errors instead of throwing.ConversationRelayLabels: filler, idle prompt, idle goodbye, apology, said again). Otherwise these lines all look alike, and a flow may need to tell them apart. For example, a caller who asks "what?" after "Are you still there?" should hear the agent's question again, not the idle prompt.interruptible: false. The call ends right after them and nothing the caller says over them gets answered, so being cut off only meant the caller missed the end of a line that is stored in full. Idle prompts and a line said again stay interruptible, because they wait for the caller to speak.canSpeakpassed and the session hadn't ended. The configured prompt text is stored, not the prompt name. Lines said around a hang-up are stored after theendframe, so storage never delays the hang-up.AgentTurnCallbacksnow holds the fillers:NoteFillerSaid/TakeFillersreplaceOnFillerSaid. The session notes each filler under the send lock as it goes out. Each filler is taken once, either by the turn as it stores its reply (as before) or by the session for a turn that failed, timed out or stored no reply. A turn that times out and later replies does not store them again.BuildPostbackMessageModelnow reads the last dialog that is notExcludeFromContext, because a stored idle prompt or repeated line can now come after the reply that asked for a postback.Two more fixes, found by running simulated calls against a local onebrain:
AiResponseTimeoutSecondsand the session apologizes and hangs up. If none of the reply was sent, it is now stored as[the call ended before this was said; the caller did not hear: "..."], the same way a reply dropped because the caller kept talking is marked.RewriteReplyis now set for every relay turn, not only streamed ones. The other marks only ever apply to streamed replies, so non-streamed turns are otherwise unchanged.still-there).Tests
tests/BotSharp.Core.UnitTests/Plugins/ConversationRelayKeptLinesTests.cs(the test project now references the Twilio plugin) runs a session on a fake WebSocket and checks:IdleAsksAgainon or offkeepSaidLinedoes not stop the hang-upKeepSaidLineswrites (includingMessageLabel), and that it swallows a storage failuredotnet test tests/BotSharp.Core.UnitTests: the 14 new tests pass, three runs in a row. One other test,SharpCacheAgentMemoryTests.GetAgent_caches_per_scope_without_adding_shared_cache_entries, fails onmasterwithout this change too.Local simulated calls
I ran onebrain locally with this branch and drove calls through
/twilio/inboundand the relay WebSocket from a script. Then I read the stored dialogs and checked the conversation page (with SciSharp/BotSharp-UI#497):twilio_relay_idle_promptinterruptible: false),endtwilio_relay_idle_goodbyeResumeAfterCutInSecondstwilio_relay_said_againtwilio_relay_filler) before the replyAiResponseTimeoutSecondsinterruptible: false),endtwilio_relay_apology), late reply marked never saidNot covered: a turn that throws (only the unit tests cover it), and whether Twilio finishes playing a line before it acts on
end.🤖 Generated with Claude Code