From a60da139aa5c9ad2e8498b5f040ddfeae5b30dd7 Mon Sep 17 00:00:00 2001 From: "nick.yi" Date: Wed, 15 Jul 2026 11:01:47 +0800 Subject: [PATCH 1/4] update Gpt4xModelConstants --- .../BotSharp.Abstraction/Models/Gpt4xModelConstants.cs | 1 - .../Providers/Realtime/RealTimeCompletionProvider.cs | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Models/Gpt4xModelConstants.cs b/src/Infrastructure/BotSharp.Abstraction/Models/Gpt4xModelConstants.cs index a812ee02d..8919b743c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Models/Gpt4xModelConstants.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Models/Gpt4xModelConstants.cs @@ -4,7 +4,6 @@ public static class Gpt4xModelConstants { public const string GPT_4o = "gpt-4o"; public const string GPT_4o_Mini = "gpt-4o-mini"; - public const string GPT_4o_Mini_Realtime_Preview = "gpt-4o-mini-realtime-preview"; public const string GPT_4o_Mini_Search_Preview = "gpt-4o-mini-search-preview"; public const string GPT_4o_Mini_Transcribe = "gpt-4o-mini-transcribe"; public const string GPT_4o_Transcribe = "gpt-4o-transcribe"; diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs index 30119236b..40d9a0633 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs @@ -41,8 +41,6 @@ public RealTimeCompletionProvider( _logger = logger; _services = services; _botsharpOptions = botsharpOptions; - - var settingService = _services.GetRequiredService(); } public async Task Connect( From 1aa6dc831b9f950b70094ed71f48f023c42d3e56 Mon Sep 17 00:00:00 2001 From: "nick.yi" Date: Thu, 16 Jul 2026 16:58:37 +0800 Subject: [PATCH 2/4] =?UTF-8?q?Add=20OnAuthenticate=20to=20ITwilioSessionH?= =?UTF-8?q?ook=EF=BC=8CITwilioCallStatusHook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/TwilioInboundController.cs | 7 ++++++- .../Controllers/TwilioOutboundController.cs | 4 +++- .../Controllers/TwilioRecordController.cs | 1 + .../Controllers/TwilioVoiceController.cs | 4 ++++ .../Interfaces/ITwilioCallStatusHook.cs | 1 + .../Interfaces/ITwilioSessionHook.cs | 7 +++++++ .../BotSharp.Plugin.Twilio/TwilioStreamMiddleware.cs | 11 +++++++++++ 7 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs index 40ba7d80d..5006a26f0 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs @@ -53,6 +53,9 @@ public async Task InitiateStreamConversation(ConversationalVoiceReq instruction.SpeechPaths.Add(request.InitAudioFile); } + // Authenticate (synchronous, so identity writes survive into the async session hooks) + HookEmitter.Emit(_services, hook => hook.OnAuthenticate(request), request.AgentId); + // Before creating session await HookEmitter.Emit(_services, async hook => { @@ -76,7 +79,9 @@ await HookEmitter.Emit(_services, async hook => if (twilio.MachineDetected(request)) { response = new VoiceResponse(); - await HookEmitter.Emit(_services, + // Authenticate (synchronous, so identity writes survive into the async status hook) + HookEmitter.Emit(_services, hook => hook.OnAuthenticate(request), request.AgentId); + await HookEmitter.Emit(_services, async hook => await hook.OnVoicemailStarting(request), request.AgentId); var url = twilio.GetSpeechPath(request.ConversationId, "voicemail.mp3"); diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioOutboundController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioOutboundController.cs index 5543af38a..f355d77ce 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioOutboundController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioOutboundController.cs @@ -33,7 +33,9 @@ public async Task InitiateOutboundCall(ConversationalVoiceRequest r if (twilio.MachineDetected(request)) { response = new VoiceResponse(); - await HookEmitter.Emit(_services, + // Authenticate (synchronous, so identity writes survive into the async status hook) + HookEmitter.Emit(_services, hook => hook.OnAuthenticate(request), request.AgentId); + await HookEmitter.Emit(_services, async hook => await hook.OnVoicemailStarting(request), request.AgentId); var url = twilio.GetSpeechPath(request.ConversationId, "voicemail.mp3"); diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioRecordController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioRecordController.cs index c6690d84c..bb96d9d8e 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioRecordController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioRecordController.cs @@ -36,6 +36,7 @@ public async Task PhoneRecordingStatus(ConversationalVoiceRequest }); await convService.SaveStates(); + HookEmitter.Emit(_services, x => x.OnAuthenticate(request), request.AgentId); // recording completed await HookEmitter.Emit(_services, x => x.OnRecordingCompleted(request), request.AgentId); } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs index 786f116cd..12e45a8c8 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs @@ -62,6 +62,9 @@ public async Task InitiateConversation(ConversationalVoiceRequest r ActionOnEmptyResult = true }; + // Authenticate (synchronous, so identity writes survive into the async session hooks) + HookEmitter.Emit(_services, hook => hook.OnAuthenticate(request), request.AgentId); + await HookEmitter.Emit(_services, async hook => { await hook.OnSessionCreating(request, instruction); @@ -343,6 +346,7 @@ public async Task PhoneCallStatus(ConversationalVoiceRequest reque { var twilio = _services.GetRequiredService(); + HookEmitter.Emit(_services, hook => hook.OnAuthenticate(request), request.AgentId); switch (request.CallStatus) { case "completed": diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Interfaces/ITwilioCallStatusHook.cs b/src/Plugins/BotSharp.Plugin.Twilio/Interfaces/ITwilioCallStatusHook.cs index e7ce09325..b54ce490c 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Interfaces/ITwilioCallStatusHook.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Interfaces/ITwilioCallStatusHook.cs @@ -6,6 +6,7 @@ namespace BotSharp.Plugin.Twilio.Interfaces; public interface ITwilioCallStatusHook : IHookBase { + void OnAuthenticate(ConversationalVoiceRequest request); Task OnVoicemailLeft(ConversationalVoiceRequest request) => Task.CompletedTask; Task OnUserDisconnected(ConversationalVoiceRequest request) => Task.CompletedTask; Task OnRecordingCompleted(ConversationalVoiceRequest request) => Task.CompletedTask; diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Interfaces/ITwilioSessionHook.cs b/src/Plugins/BotSharp.Plugin.Twilio/Interfaces/ITwilioSessionHook.cs index 00837b6a4..3fd1c9c62 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Interfaces/ITwilioSessionHook.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Interfaces/ITwilioSessionHook.cs @@ -7,6 +7,13 @@ namespace BotSharp.Plugin.Twilio.Interfaces; public interface ITwilioSessionHook : IHookBase { + /// + /// Runs synchronously before any session hook, so identity writes (AsyncLocal) + /// set here survive into the subsequent async hooks. + /// + /// + void OnAuthenticate(ConversationalVoiceRequest request); + /// /// Before session creating /// diff --git a/src/Plugins/BotSharp.Plugin.Twilio/TwilioStreamMiddleware.cs b/src/Plugins/BotSharp.Plugin.Twilio/TwilioStreamMiddleware.cs index d653a3417..3dfb8f646 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/TwilioStreamMiddleware.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/TwilioStreamMiddleware.cs @@ -7,8 +7,10 @@ using BotSharp.Abstraction.Realtime.Sessions; using BotSharp.Abstraction.Routing; using BotSharp.Abstraction.Utilities; +using BotSharp.Core.Infrastructures; using BotSharp.Core.Session; using BotSharp.Plugin.Twilio.Interfaces; +using BotSharp.Plugin.Twilio.Models; using BotSharp.Plugin.Twilio.Models.Stream; using Microsoft.AspNetCore.Http; using System.Net.WebSockets; @@ -79,6 +81,15 @@ private async Task HandleWebSocket(IServiceProvider services, string agentId, st var convService = services.GetRequiredService(); await convService.SetConversationId(conversationId, []); + // No onebrain identity arrives on the Twilio media stream. Authenticate synchronously + // (so AsyncLocal identity writes survive) before the streaming session hooks run. + var request = new ConversationalVoiceRequest + { + AgentId = agentId, + ConversationId = conversationId + }; + HookEmitter.Emit(services, hook => hook.OnAuthenticate(request), agentId); + var hooks = services.GetHooks(agentId); foreach (var hook in hooks) { From e7a58ae5984e67a49bd385e466ad82910cd2422b Mon Sep 17 00:00:00 2001 From: "nick.yi" Date: Thu, 16 Jul 2026 17:50:51 +0800 Subject: [PATCH 3/4] optimize TwilioSessionHook --- .../Controllers/TwilioVoiceController.cs | 8 ++++++++ .../Hooks/TwilioConversationHook.cs | 1 + .../BotSharp.Plugin.Twilio/Services/TwilioService.cs | 3 ++- .../BotSharp.Plugin.Twilio/TwilioStreamMiddleware.cs | 12 +----------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs index 12e45a8c8..5dc53b375 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs @@ -115,6 +115,9 @@ await HookEmitter.Emit(_services, async hook => [HttpPost("twilio/voice/receive/{seqNum}")] public async Task ReceiveCallerMessage(ConversationalVoiceRequest request) { + // Authenticate (synchronous, so identity writes survive into the async session hooks) + HookEmitter.Emit(_services, hook => hook.OnAuthenticate(request), request.AgentId); + var twilio = _services.GetRequiredService(); var messageQueue = _services.GetRequiredService(); var sessionManager = _services.GetRequiredService(); @@ -206,6 +209,9 @@ await HookEmitter.Emit(_services, async hook => [HttpPost("twilio/voice/reply/{seqNum}")] public async Task ReplyCallerMessage(ConversationalVoiceRequest request) { + // Authenticate (synchronous, so identity writes survive into the async session hooks) + HookEmitter.Emit(_services, hook => hook.OnAuthenticate(request), request.AgentId); + var nextSeqNum = request.SeqNum + 1; var sessionManager = _services.GetRequiredService(); var twilio = _services.GetRequiredService(); @@ -346,7 +352,9 @@ public async Task PhoneCallStatus(ConversationalVoiceRequest reque { var twilio = _services.GetRequiredService(); + // Authenticate (synchronous, so identity writes survive into the async status hooks) HookEmitter.Emit(_services, hook => hook.OnAuthenticate(request), request.AgentId); + switch (request.CallStatus) { case "completed": diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Hooks/TwilioConversationHook.cs b/src/Plugins/BotSharp.Plugin.Twilio/Hooks/TwilioConversationHook.cs index 3ccc4f4d6..8762c5649 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Hooks/TwilioConversationHook.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Hooks/TwilioConversationHook.cs @@ -41,6 +41,7 @@ public override async Task OnFunctionExecuted(RoleDialogModel message, InvokeFun CallSid = sid, }; + // No OnAuthenticate before these hooks — identity is established only at the HTTP entry points. foreach (var hook in hooks) { if (await hook.ShouldReconnect(request, message)) diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs index e0618aaf6..1720d95d5 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs @@ -324,7 +324,8 @@ public async Task WaitingForAiResponse(ConversationalVoiceRequest ActionOnEmptyResult = true }; - await HookEmitter.Emit(_services, async hook => await hook.OnWaitingAgentResponse(request, instruction), + // No OnAuthenticate before these hooks — identity is established only at the HTTP entry points. + await HookEmitter.Emit(_services, async hook => await hook.OnWaitingAgentResponse(request, instruction), request.AgentId); response = ReturnInstructions(instruction); diff --git a/src/Plugins/BotSharp.Plugin.Twilio/TwilioStreamMiddleware.cs b/src/Plugins/BotSharp.Plugin.Twilio/TwilioStreamMiddleware.cs index 3dfb8f646..0f49ea07b 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/TwilioStreamMiddleware.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/TwilioStreamMiddleware.cs @@ -7,10 +7,8 @@ using BotSharp.Abstraction.Realtime.Sessions; using BotSharp.Abstraction.Routing; using BotSharp.Abstraction.Utilities; -using BotSharp.Core.Infrastructures; using BotSharp.Core.Session; using BotSharp.Plugin.Twilio.Interfaces; -using BotSharp.Plugin.Twilio.Models; using BotSharp.Plugin.Twilio.Models.Stream; using Microsoft.AspNetCore.Http; using System.Net.WebSockets; @@ -81,15 +79,7 @@ private async Task HandleWebSocket(IServiceProvider services, string agentId, st var convService = services.GetRequiredService(); await convService.SetConversationId(conversationId, []); - // No onebrain identity arrives on the Twilio media stream. Authenticate synchronously - // (so AsyncLocal identity writes survive) before the streaming session hooks run. - var request = new ConversationalVoiceRequest - { - AgentId = agentId, - ConversationId = conversationId - }; - HookEmitter.Emit(services, hook => hook.OnAuthenticate(request), agentId); - + // No OnAuthenticate before these hooks — identity is established only at the HTTP entry points. var hooks = services.GetHooks(agentId); foreach (var hook in hooks) { From a8a5dbde9143d7e7dac51206e97433b4654cb699 Mon Sep 17 00:00:00 2001 From: "nick.yi" Date: Fri, 17 Jul 2026 08:59:21 +0800 Subject: [PATCH 4/4] Add a comment to the OnAuthenticate method --- .../BotSharp.Abstraction/Crontab/ICrontabHook.cs | 4 ++++ .../Interfaces/ITwilioCallStatusHook.cs | 4 ++++ .../BotSharp.Plugin.Twilio/Interfaces/ITwilioSessionHook.cs | 3 +-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Crontab/ICrontabHook.cs b/src/Infrastructure/BotSharp.Abstraction/Crontab/ICrontabHook.cs index 94284150f..33c64c8c5 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Crontab/ICrontabHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Crontab/ICrontabHook.cs @@ -7,6 +7,10 @@ public interface ICrontabHook : IHookBase string[]? Triggers => null; + /// + /// Establishes the identity for the cron event; invoked synchronously for all hooks before any OnCronTriggered runs, so the identity flows down into the execution phase. + /// + /// void OnAuthenticate(CrontabItem item) { } Task OnCronTriggered(CrontabItem item) diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Interfaces/ITwilioCallStatusHook.cs b/src/Plugins/BotSharp.Plugin.Twilio/Interfaces/ITwilioCallStatusHook.cs index b54ce490c..3c73b5c95 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Interfaces/ITwilioCallStatusHook.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Interfaces/ITwilioCallStatusHook.cs @@ -6,6 +6,10 @@ namespace BotSharp.Plugin.Twilio.Interfaces; public interface ITwilioCallStatusHook : IHookBase { + /// + /// Establishes the operator identity synchronously at the call-status webhook entry, before any status hook method runs. + /// + /// void OnAuthenticate(ConversationalVoiceRequest request); Task OnVoicemailLeft(ConversationalVoiceRequest request) => Task.CompletedTask; Task OnUserDisconnected(ConversationalVoiceRequest request) => Task.CompletedTask; diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Interfaces/ITwilioSessionHook.cs b/src/Plugins/BotSharp.Plugin.Twilio/Interfaces/ITwilioSessionHook.cs index 3fd1c9c62..8632e72e4 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Interfaces/ITwilioSessionHook.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Interfaces/ITwilioSessionHook.cs @@ -8,8 +8,7 @@ namespace BotSharp.Plugin.Twilio.Interfaces; public interface ITwilioSessionHook : IHookBase { /// - /// Runs synchronously before any session hook, so identity writes (AsyncLocal) - /// set here survive into the subsequent async hooks. + /// Establishes the operator identity synchronously at the HTTP entry points only; the WebSocket and background flows manage identity separately. /// /// void OnAuthenticate(ConversationalVoiceRequest request);