Skip to content

fix(twilio): keep the lines a relay session says on its own in the history - #1491

Merged
yileicn merged 3 commits into
SciSharp:masterfrom
yuyixg:fix/relay-store-own-lines
Nov 13, 2026
Merged

yileicn merged 3 commits into
SciSharp:masterfrom
yuyixg:fix/relay-store-own-lines

Conversation

@yuyixg

@yuyixg yuyixg commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

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:

  • The apology of a turn that threw or ran past AiResponseTimeoutSeconds, and the fillers said before it. Fillers were stored only together with a reply, so a failed turn lost them.
  • Idle prompts (say-it-again-question, still-there, say-it-again-N) and the idle-goodbye before the hang-up
  • A line said again after a cut-in with nothing after it (ResumeAfterCutInSeconds)

Change

  • ConversationRelaySession takes an optional keepSaidLine(text, label, saidUtc) callback. Existing callers still compile. TwilioConversationRelayMiddleware gives it one that creates its own service scope and calls TwilioConversationProcessor.KeepSaidLines. That method appends an assistant RoleDialogModel with ExcludeFromContext = true, CreatedAt set to when the line was said, and the label in MessageLabel. It logs storage errors instead of throwing.
  • Each stored line is labeled with what it was (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.
  • The apology and the idle goodbye are now sent with 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.
  • Only a line that went out is kept: its canSpeak passed 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 the end frame, so storage never delays the hang-up.
  • AgentTurnCallbacks now holds the fillers: NoteFillerSaid / TakeFillers replace OnFillerSaid. 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.
  • Tool-call indications are not stored here; the flow that sends one can store it.
  • BuildPostbackMessageModel now reads the last dialog that is not ExcludeFromContext, 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:

  • A reply that comes back after the call has ended was still stored as if the agent had said it, listed after the apology. This happens when the caller hangs up while the agent is working, or when the turn runs past AiResponseTimeoutSeconds and 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. RewriteReply is 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.
  • If the caller stayed silent after the greeting, they heard "Sorry, can you say it again?" even though they had said nothing. Until the caller says something or presses a key, an idle prompt now only asks whether they are still there (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:

  • a failed turn stores its fillers, then the apology, and the apology is uninterruptible
  • a timed-out turn does not hand its fillers back when it finally replies, and its reply is stored as never said
  • a reply that comes back after the caller hung up is stored as never said
  • a turn that stored its fillers with the reply leaves none for the session, and its reply is stored unchanged
  • a caller who has said nothing yet is asked whether they are still there, with IdleAsksAgain on or off
  • a turn with no reply leaves its fillers to the session
  • a turn that fails after the caller hung up stores its fillers but no apology
  • a failing keepSaidLine does not stop the hang-up
  • idle prompts and the goodbye are stored with their text and labels; the prompts stay interruptible and the goodbye is not
  • a line said again after a cut-in is stored with its label
  • the dialog fields KeepSaidLines writes (including MessageLabel), and that it swallows a storage failure

dotnet 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 on master without this change too.

Local simulated calls

I ran onebrain locally with this branch and drove calls through /twilio/inbound and the relay WebSocket from a script. Then I read the stored dialogs and checked the conversation page (with SciSharp/BotSharp-UI#497):

Scenario On the line Stored
Caller describes a problem, answers reply, next question caller line, then reply, in time order
Caller silent after a question "Sorry, I didn't catch that. {question}", then "Are you still there?" (caller's "what?" then gets the question again; that part is onebrain) both prompts, twilio_relay_idle_prompt
Caller silent to the end two prompts, goodbye (interruptible: false), end prompts plus twilio_relay_idle_goodbye
Caller silent from the start "Are you still there?" twice, goodbye (was "Sorry, can you say it again?")
Caller cuts in and says nothing line said again after ResumeAfterCutInSeconds twilio_relay_said_again
Turn slower than the filler interval (an agent with fillers) filler, reply filler (twilio_relay_filler) before the reply
Turn past AiResponseTimeoutSeconds filler, apology (interruptible: false), end filler, apology (twilio_relay_apology), late reply marked never said
Caller hangs up while the agent works — reply marked never said

Not 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

…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-code-review

Copy link
Copy Markdown
Contributor

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

…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]>
@yileicn

yileicn commented Nov 13, 2026

Copy link
Copy Markdown
Member

✅ AegisCode Code Review Report: Pass

Counted: 2 Medium, 23 Low. None of them blocks this change.
Full report · review 11484 · commit d64dd5d9

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_CASE

Constant '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_CASE

Constant '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_CASE

Constant '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_CASE

Constant '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_CASE

Constant '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 125

This 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 125

This 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_CASE

Constant '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_CASE

Constant '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_CASE

Constant '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 125

This 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 125

This 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 125

This 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 125

This 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 125

This 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 125

This 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 125

This 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 125

This 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 125

This 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 125

This 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 125

This 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]>
@yileicn

yileicn commented Nov 13, 2026

Copy link
Copy Markdown
Member

✅ AegisCode Code Review Report: Pass

Counted: 2 Medium, 23 Low. None of them blocks this change. Not counted: 1 new since the baseline.
Full report · review 11491 · commit c39a619e

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_CASE

Constant '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_CASE

Constant '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_CASE

Constant '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_CASE

Constant '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_CASE

Constant '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 125

This 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 125

This 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_CASE

Constant '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_CASE

Constant '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_CASE

Constant '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 125

This 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 125

This 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 125

This 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 125

This 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 125

This 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 125

This 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 125

This 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 125

This 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 125

This 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 125

This 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 125

This 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 counted

Found 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 125

This 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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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);
Comment on lines 1297 to +1299
/// <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);
@yileicn
yileicn merged commit 8af0733 into SciSharp:master Nov 13, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants