-
-
Notifications
You must be signed in to change notification settings - Fork 622
.pr_agent_auto_best_practices
Pattern 1: When passing collections/options/metadata into hooks, retries, or outbound messages, defensively copy any mutable objects (lists, dictionaries, model instances) before mutating to avoid cross-request or cross-component state leakage.
Example code before:
// cachedRules reused across requests
var rules = cachedRules.ToList(); // shallow copy only
hook.OnLoaded(rules); // hook mutates rule objects
options.IsRetry = true; // mutates caller-owned options
message.MetaData = response.MetaData; // shares same dictionary instance
message.MetaData["k"] = "v";
Example code after:
var rules = cachedRules.Select(r => r.Clone()).ToList(); // deep copy / clone
hook.OnLoaded(rules);
var retryOptions = options is null ? new Options() : options with { IsRetry = true };
message.MetaData = response.MetaData is null ? null : new Dictionary<string, string>(response.MetaData);
Relevant past accepted suggestions:
Suggestion 1:
[reliability] `OnRoutingRulesLoaded` mutates cached rules
`OnRoutingRulesLoaded` mutates cached rules
`OnRoutingRulesLoaded` is invoked with a mutable list of `RoutingRule` objects that originate from cached/shared agent routing rule instances, so hook mutations can leak across requests. This violates the defensive-copy requirement and can cause hard-to-debug cross-request side effects.OnRoutingRulesLoaded receives a mutable list of RoutingRule instances that are sourced from GetRoutingRecords(). Because GetRoutingRecords() is cached and returns agent-owned RoutingRule objects, in-place mutations by hooks can leak across requests.
The current code uses ToList() which only copies the list container, not the contained RoutingRule objects. The hook contract explicitly encourages in-place mutation.
- src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs[150-170]
Suggestion 2:
Avoid mutating the input options
To avoid side effects, create a new InvokeAgentOptions object for the retry call instead of mutating the existing options object.
src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs [168-171]
-options ??= InvokeAgentOptions.Default();
-options.IsRetry = true;
-var retryResult = await InvokeAgent(agentId, dialogs, options);
+var retryOptions = new InvokeAgentOptions
+{
+ From = options?.From ?? InvokeSource.Manual,
+ UseStream = options?.UseStream ?? false,
+ IsRetry = true
+};
+var retryResult = await InvokeAgent(agentId, dialogs, retryOptions);
return retryResult;Suggestion 3:
Copy mutable dictionaries before mutation
Avoid mutating options.Data directly (it may be reused across requests); create a new dictionary copy before adding/updating keys.
src/Infrastructure/BotSharp.Core/Shared/JsonRepairService.cs [92-95]
var render = _services.GetRequiredService<ITemplateRender>();
-var data = options?.Data ?? [];
+var data = options?.Data is null
+ ? new Dictionary<string, object>()
+ : new Dictionary<string, object>(options.Data);
data["input"] = malformedJson;
var prompt = render.Render(template, data);Suggestion 4:
Prevent shared mutable metadata
To prevent shared state bugs from mutable dictionaries, create a new MetaData dictionary instance for the message instead of assigning it by reference from response.MetaData.
src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs [76-79]
message = RoleDialogModel.From(message, role: AgentRole.Assistant, content: response.Content);
message.CurrentAgentId = agent.Id;
-message.MetaData = response.MetaData;
+message.MetaData = response.MetaData != null ? new(response.MetaData) : null;
message.IsStreaming = response.IsStreaming;Suggestion 5:
Avoid shared metadata reference
Create a new dictionary instance for message.FunctionMetaData instead of assigning by reference to prevent potential shared state issues.
src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs [59]
-message.FunctionMetaData = response.FunctionMetaData;
+message.FunctionMetaData = response.FunctionMetaData != null ? new(response.FunctionMetaData) : null;Suggestion 6:
Prevent duplicate hook side effects
Replace currently calls Push, which emits OnAgentEnqueued, and then also emits OnAgentReplaced, causing duplicated hook side effects; update Replace to only emit the replacement hook and avoid invoking Push's enqueue logic.
src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs [199-207]
else if (_stack.Peek() != agentId)
{
fromAgent = _stack.Peek();
_stack.Pop();
- await Push(agentId);
+ _stack.Push(agentId);
await HookEmitter.Emit<IRoutingHook>(_services, async hook => await hook.OnAgentReplaced(fromAgent, toAgent, reason: reason),
agentId);
}Pattern 2: At API/storage/provider boundaries, add explicit null/empty guards (including correct boolean logic) and return a safe fallback (or null) with a clear log message instead of allowing NREs/OutOfRange exceptions.
Example code before:
public IActionResult Save([FromBody] Req request)
{
var bytes = Convert.FromBase64String(request.Thumbnail); // request may be null
...
}
var item = items.ElementAt(random.Next(items.Count())); // items may be empty
if (args != null || args.DelayMs > 0) { ... } // can still deref args when null
Example code after:
public IActionResult Save([FromBody] Req? request)
{
if (request?.Thumbnail is null) return BadRequest("Missing thumbnail.");
...
}
var list = items.ToList();
if (list.Count == 0) return null;
if (args != null && args.DelayMs > 0) { ... }
Relevant past accepted suggestions:
Suggestion 1:
[reliability] `request` not null-checked
`request` not null-checked
`SaveConversationThumbnail` dereferences `request.Thumbnail` without guarding against a null body, which can throw at this API boundary. This violates the requirement to add null/empty guards and safe fallbacks for boundary inputs.SaveConversationThumbnail dereferences request.Thumbnail without guarding request against null, which can throw when the request body is missing/invalid.
This is an API boundary ([FromBody] binding). The compliance rules require explicit null/empty guards and safe fallbacks at system boundaries.
- src/Infrastructure/BotSharp.OpenAPI/Controllers/Conversation/ConversationController.File.cs[121-133]
Suggestion 2:
Add null checks and fallback
Guard against router or response.Content being null (e.g., agent not found, provider errors) and fall back safely to the original JSON while logging the reason.
src/Infrastructure/BotSharp.Core/JsonRepair/JsonRepairService.cs [58-96]
var agentService = _services.GetRequiredService<IAgentService>();
var router = await agentService.GetAgent(ROUTER_AGENT_ID);
+if (router == null)
+{
+ _logger.LogWarning("Agent '{AgentId}' not found; returning original JSON.", ROUTER_AGENT_ID);
+ return malformedJson;
+}
var template = router.Templates?.FirstOrDefault(x => x.Name == TEMPLATE_NAME)?.Content;
if (string.IsNullOrEmpty(template))
{
- _logger.LogWarning($"Template '{TEMPLATE_NAME}' not found in agent '{ROUTER_AGENT_ID}'");
+ _logger.LogWarning("Template '{TemplateName}' not found in agent '{AgentId}'; returning original JSON.", TEMPLATE_NAME, ROUTER_AGENT_ID);
return malformedJson;
}
...
var response = await completion.GetChatCompletions(agent, dialogs);
-_logger.LogInformation($"JSON repair result: {response.Content}");
-return response.Content;
+var content = response?.Content;
+if (string.IsNullOrWhiteSpace(content))
+{
+ _logger.LogWarning("JSON repair returned empty content; returning original JSON.");
+ return malformedJson;
+}
+_logger.LogInformation("JSON repair result: {Json}", content);
+return content;
+Suggestion 3:
Prevent crash when no models match
Prevent a potential ArgumentOutOfRangeException in GetProviderModel by checking if the filtered models collection is empty. If it is, return null instead of attempting to select an element, which would cause a crash.
src/Infrastructure/BotSharp.Core/Infrastructures/LlmProviderService.cs [61-69]
if (capabilities != null)
{
models = models.Where(x => x.Capabilities != null && capabilities.Any(y => x.Capabilities.Contains(y)));
}
+var availableModels = models.ToList();
+if (!availableModels.Any())
+{
+ return null;
+}
+
var random = new Random();
-var index = random.Next(0, models.Count());
-var modelSetting = models.ElementAt(index);
+var index = random.Next(0, availableModels.Count);
+var modelSetting = availableModels.ElementAt(index);
return modelSetting;Suggestion 4:
Fix null reference bug
The condition has a logical error. It should use AND (&&) instead of OR (||) to ensure both that args is not null and the delay time is positive. The current condition will attempt to access DelayTime even when args is null, causing a NullReferenceException.
src/Infrastructure/BotSharp.Core.Crontab/Functions/TaskWaitFn.cs [25]
-if (args != null || args.DelayTime > 0)
+if (args != null && args.DelayTime > 0)Suggestion 5:
Check WebSocket state
The SendEventToUser method doesn't check if the WebSocket is in a valid state before sending data. If the client has disconnected or the connection is closing, this could throw an exception and crash the middleware.
src/Plugins/BotSharp.Plugin.ChatHub/ChatStreamMiddleware.cs [106]
-await SendEventToUser(webSocket, data);
+if (webSocket.State == WebSocketState.Open)
+{
+ await SendEventToUser(webSocket, data);
+}[Suggestion has been applied]
Suggestion 6:
Prevent null reference exception
The code uses a null conditional operator on agent but doesn't check if agent is null before accessing its Description property. If agent is null, this could lead to a NullReferenceException when trying to access agent.Description.
src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs [283]
-var instruction = messages.FirstOrDefault()?.Content.FirstOrDefault()?.Text ?? agent?.Description;
+var instruction = messages.FirstOrDefault()?.Content.FirstOrDefault()?.Text ?? agent?.Description ?? string.Empty;Suggestion 7:
Remove unnecessary async and add guard
The method is declared async but contains no await, which can introduce unnecessary state machine overhead and warnings. Also, accessing page.Url may throw if the page is disposed; wrap in a try-catch and return an empty string on failure to align with the null-path behavior.
-public async Task<string> GetCurrentUrl(MessageInfo message)
+public Task<string> GetCurrentUrl(MessageInfo message)
{
- var page = _instance.GetPage(message.ContextId);
- if (page == null)
- return string.Empty;
- return page.Url;
+ try
+ {
+ var page = _instance.GetPage(message.ContextId);
+ if (page == null) return Task.FromResult(string.Empty);
+ return Task.FromResult(page.Url ?? string.Empty);
+ }
+ catch
+ {
+ return Task.FromResult(string.Empty);
+ }
}Pattern 3: Avoid logging sensitive model/user content and avoid exposing internal configuration values in user-facing messages; log minimal metadata using structured logging and keep end-user errors generic.
Example code before:
_logger.LogWarning($"Provider response: {assistantText}");
return $"Too long; max tokens is {internalMaxTokens}";
Example code after:
_logger.LogWarning("Provider response truncated/invalid. provider={Provider} finish_reason={Reason}",
providerName, finishReason);
return "The request could not be completed. Please try again with a shorter input.";
Relevant past accepted suggestions:
Suggestion 1:
[security] GoogleAI logs response `text`
GoogleAI logs response `text`
The new warning logs include the full model output `text` (and related metadata), which may contain sensitive information and should not be logged. The updated MaxTokens fallback message includes the internal max-token value, exposing configuration details to the end user.GoogleAI provider logs raw assistant content (text) and exposes the internal MaxOutputTokens value in a user-facing message.
Logs must not include sensitive content, and user-visible errors should be generic to avoid leaking internal system/config details.
- src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/ChatCompletionProvider.cs[75-84]
- src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/ChatCompletionProvider.cs[184-200]
Suggestion 2:
[security] OpenAI logs response `text`
OpenAI logs response `text`
The updated warning logs include full model output content via `Content:{text}`, which may expose sensitive data in logs. The new Length handling block also returns a user-facing message that includes the internal max token count.OpenAI provider logs full assistant output (text) in warnings and includes internal token limit values in a user-facing fallback message.
To comply with secure logging and secure error handling, avoid logging sensitive content and avoid exposing internal configuration details to end users.
- src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs[76-85]
- src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs[191-203]
Suggestion 3:
Add null checks and fallback
Guard against router or response.Content being null (e.g., agent not found, provider errors) and fall back safely to the original JSON while logging the reason.
src/Infrastructure/BotSharp.Core/JsonRepair/JsonRepairService.cs [58-96]
var agentService = _services.GetRequiredService<IAgentService>();
var router = await agentService.GetAgent(ROUTER_AGENT_ID);
+if (router == null)
+{
+ _logger.LogWarning("Agent '{AgentId}' not found; returning original JSON.", ROUTER_AGENT_ID);
+ return malformedJson;
+}
var template = router.Templates?.FirstOrDefault(x => x.Name == TEMPLATE_NAME)?.Content;
if (string.IsNullOrEmpty(template))
{
- _logger.LogWarning($"Template '{TEMPLATE_NAME}' not found in agent '{ROUTER_AGENT_ID}'");
+ _logger.LogWarning("Template '{TemplateName}' not found in agent '{AgentId}'; returning original JSON.", TEMPLATE_NAME, ROUTER_AGENT_ID);
return malformedJson;
}
...
var response = await completion.GetChatCompletions(agent, dialogs);
-_logger.LogInformation($"JSON repair result: {response.Content}");
-return response.Content;
+var content = response?.Content;
+if (string.IsNullOrWhiteSpace(content))
+{
+ _logger.LogWarning("JSON repair returned empty content; returning original JSON.");
+ return malformedJson;
+}
+_logger.LogInformation("JSON repair result: {Json}", content);
+return content;
+Suggestion 4:
Log exceptions instead of swallowing
Avoid swallowing exceptions; at minimum log the exception so callers/operators can understand why parsing failed before repair is attempted.
src/Infrastructure/BotSharp.Core/JsonRepair/JsonRepairService.cs [33-41]
try
{
// First try direct deserialization
return malformedJson.JsonContent<T>();
}
-catch
+catch (Exception ex)
{
+ _logger.LogDebug(ex, "Direct JSON deserialization failed; attempting LLM repair.");
// Continue to repair
}Pattern 4: For identifier-like strings (model IDs, function names, keys), use ordinal/case-insensitive comparisons via StringComparer (e.g., OrdinalIgnoreCase) and build sets/maps with the comparer rather than relying on culture-sensitive comparisons.
Example code before:
if (string.Equals(id1, id2, StringComparison.InvariantCultureIgnoreCase)) { ... }
var set = new HashSet<string>(items);
if (set.Contains(name)) { ... } // but name matching should be case-insensitive
Example code after:
if (string.Equals(id1, id2, StringComparison.OrdinalIgnoreCase)) { ... }
var set = new HashSet<string>(items, StringComparer.OrdinalIgnoreCase);
if (!string.IsNullOrEmpty(name) && set.Contains(name)) { ... }
Relevant past accepted suggestions:
Suggestion 1:
[correctness] Culture-based model name compare
Culture-based model name compare
Model identifier matching uses `StringComparison.InvariantCultureIgnoreCase`, which is culture-sensitive and can cause inconsistent matches for identifier-like strings. This violates the requirement to use robust comparers for identifier matching.Model-name matching uses a culture-based comparison (InvariantCultureIgnoreCase) which is not robust for identifier matching and may yield inconsistent results.
Model names (like gpt-4o) are identifiers and should typically be compared using ordinal semantics to avoid culture-specific casing behavior.
- src/Infrastructure/BotSharp.Core/Infrastructures/SettingService.cs[57-57]
Suggestion 2:
Use case-insensitive model ID filtering
Update the model ID filtering logic in GetLlmConfigs to be case-insensitive. Use StringComparer.OrdinalIgnoreCase when checking if filter.ModelIds contains a model's ID to ensure consistent and robust filtering.
src/Infrastructure/BotSharp.Core/Infrastructures/LlmProviderService.cs [139-142]
if (filter.ModelIds != null)
{
- models = models.Where(x => filter.ModelIds.Contains(x.Id));
+ models = models.Where(x => filter.ModelIds.Contains(x.Id, StringComparer.OrdinalIgnoreCase));
}Suggestion 3:
Fix comparer misuse and null checks
Avoid passing a comparer to HashSet.Contains — it only accepts the element. Also, null-check function names to prevent NREs. Build the set with the comparer and call Contains with a single argument, and ensure x.Name is not null before accessing.
src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Rendering.cs [110-124]
public IEnumerable<FunctionDef> FilterFunctions(string instruction, Agent agent, StringComparer? comparer = null)
{
var functions = agent.Functions.AsEnumerable();
if (agent.FuncVisMode.IsEqualTo(AgentFuncVisMode.Auto) && !string.IsNullOrWhiteSpace(instruction))
{
- comparer = comparer ?? StringComparer.OrdinalIgnoreCase;
+ comparer ??= StringComparer.OrdinalIgnoreCase;
var matches = Regex.Matches(instruction, @"\b[A-Za-z0-9_]+\b");
var words = new HashSet<string>(matches.Select(m => m.Value), comparer);
- functions = functions.Where(x => words.Contains(x.Name, comparer));
+ functions = functions.Where(x => !string.IsNullOrEmpty(x.Name) && words.Contains(x.Name));
}
functions = functions.Concat(agent.SecondaryFunctions ?? []);
return functions;
}Suggestion 4:
Correct reasoning capability gating
Decouple reasoning-effort applicability from the temperature map. Using _defaultTemperature.ContainsKey(_model) to gate reasoning will disable it for supported models not listed. Introduce a separate check (e.g., a set of reasoning-capable models) or remove the gate to honor provided levels when the SDK accepts it.
src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs [18-541]
-private readonly Dictionary<string, float> _defaultTemperature = new()
+private readonly HashSet<string> _reasoningCapableModels = new(StringComparer.OrdinalIgnoreCase)
{
- { "o3", 1.0f },
- { "o3-mini", 1.0f },
- { "o4-mini", 1.0f },
- { "gpt-5", 1.0f },
- { "gpt-5-mini", 1.0f },
- { "gpt-5-nano", 1.0f }
+ "o3", "o3-mini", "o4-mini", "gpt-5", "gpt-5-mini", "gpt-5-nano"
};
...
private ChatReasoningEffortLevel? ParseReasoningEffortLevel(string? level)
{
- if (string.IsNullOrWhiteSpace(level) || !_defaultTemperature.ContainsKey(_model))
+ if (string.IsNullOrWhiteSpace(level))
{
return null;
}
- var effortLevel = ChatReasoningEffortLevel.Low;
- switch (level.ToLower())
+ if (!_reasoningCapableModels.Contains(_model))
{
- case "medium":
- effortLevel = ChatReasoningEffortLevel.Medium;
- break;
- case "high":
- effortLevel = ChatReasoningEffortLevel.High;
- break;
- default:
- break;
+ return null;
}
- return effortLevel;
+ return level.ToLower() switch
+ {
+ "medium" => ChatReasoningEffortLevel.Medium,
+ "high" => ChatReasoningEffortLevel.High,
+ _ => ChatReasoningEffortLevel.Low
+ };
}Pattern 5: In async/concurrent I/O and execution paths, prevent deadlocks and corruption by using proper async patterns: release locks safely, avoid blocking waits, ensure cleanup on cancellation, and handle streaming/fragmented reads correctly.
Example code before:
_sem.Wait(ct);
try { ... }
finally { _sem.Release(); } // may release without acquisition on exception paths
var text = Encoding.UTF8.GetString(buffer, 0, result.Count); // ignores fragmentation
var data = task.Result; // sync wait on async
Example code after:
var acquired = false;
try { await _sem.WaitAsync(ct); acquired = true; ... }
finally { if (acquired) _sem.Release(); }
while (!result.EndOfMessage) { accumulateBytes(); result = await ws.ReceiveAsync(...); }
var data = await task; // avoid blocking waits
Relevant past accepted suggestions:
Suggestion 1:
Prevent potential deadlock in semaphore usage
To prevent potential deadlocks, revert the InnerRunWithLock method to use a boolean flag to ensure the semaphore is always released correctly, even in the case of asynchronous exceptions.
src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyCodeInterpreter.cs [103-120]
private CodeInterpretResponse InnerRunWithLock(string codeScript, CodeInterpretOptions? options = null, CancellationToken cancellationToken = default)
{
- _semLock.Wait(cancellationToken);
-
+ var lockAcquired = false;
try
{
+ _semLock.Wait(cancellationToken);
+ lockAcquired = true;
return InnerRunCode(codeScript, options, cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error in {nameof(InnerRunWithLock)}");
return new() { ErrorMsg = ex.Message };
}
finally
{
- _semLock.Release();
+ if (lockAcquired)
+ {
+ _semLock.Release();
+ }
}
}Suggestion 2:
Ensure cleanup on cancellation
Refactor the task cancellation logic to ensure the finally block always executes, preventing the Python interpreter's state from being left corrupted upon cancellation. This involves removing the cancellation token from Task.Run and handling cancellation exceptions from WaitAsync to allow for cleanup.
src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyCodeInterpreter.cs [137-216]
private async Task<CodeInterpretResponse> CoreRunScript(string codeScript, CodeInterpretOptions? options = null, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var execTask = Task.Run(() =>
{
using (Py.GIL())
{
- // Import necessary Python modules
dynamic sys = Py.Import("sys");
dynamic io = Py.Import("io");
try
{
- // Redirect standard output/error to capture it
dynamic outIO = io.StringIO();
dynamic errIO = io.StringIO();
sys.stdout = outIO;
sys.stderr = errIO;
- // Set global items
using var globals = new PyDict();
if (codeScript.Contains("__main__") == true)
{
globals.SetItem("__name__", new PyString("__main__"));
}
- // Set arguments
var list = new PyList();
if (options?.Arguments?.Any() == true)
{
list.Append(new PyString(options?.ScriptName ?? "script.py"));
foreach (var arg in options!.Arguments)
{
if (!string.IsNullOrWhiteSpace(arg.Key) && !string.IsNullOrWhiteSpace(arg.Value))
{
list.Append(new PyString($"--{arg.Key}"));
list.Append(new PyString($"{arg.Value}"));
}
}
}
sys.argv = list;
- cancellationToken.ThrowIfCancellationRequested();
+ // cooperative cancellation point before exec
+ if (cancellationToken.IsCancellationRequested)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ }
- // Execute Python script
PythonEngine.Exec(codeScript, globals);
- // Get result
var stdout = outIO.getvalue()?.ToString() as string;
- var stderr = errIO.getvalue()?.ToString() as string;
- cancellationToken.ThrowIfCancellationRequested();
+ // cooperative cancellation after exec
+ if (cancellationToken.IsCancellationRequested)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ }
return new CodeInterpretResponse
{
Result = stdout?.TrimEnd('\r', '\n') ?? string.Empty,
Success = true
};
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error in {nameof(CoreRunScript)} in {Provider}.");
return new() { ErrorMsg = ex.Message };
}
finally
{
- // Restore the original stdout/stderr/argv
sys.stdout = sys.__stdout__;
sys.stderr = sys.__stderr__;
sys.argv = new PyList();
}
- };
- }, cancellationToken);
+ }
+ }); // do not pass cancellationToken here
- return await execTask.WaitAsync(cancellationToken);
+ try
+ {
+ return await execTask.WaitAsync(cancellationToken);
+ }
+ catch (OperationCanceledException)
+ {
+ // Ensure the inner task completes cleanup
+ try { await execTask; } catch { /* ignored: already logged */ }
+ throw;
+ }
}Suggestion 3:
Prevent potential process execution deadlock
Prevent a potential process deadlock by reading the standard output and error streams concurrently with waiting for the process to exit using Task.WhenAll.
src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyCodeInterpreter.cs [267-273]
-var stdoutTask = proc.StandardOutput.ReadToEndAsync();
-var stderrTask = proc.StandardError.ReadToEndAsync();
+var stdoutTask = proc.StandardOutput.ReadToEndAsync(token);
+var stderrTask = proc.StandardError.ReadToEndAsync(token);
-await proc.WaitForExitAsync();
+await Task.WhenAll(proc.WaitForExitAsync(token), stdoutTask, stderrTask);
var stdout = await stdoutTask;
var stderr = await stderrTask;Suggestion 4:
Fix WebSocket fragmentation handling
The code is using a fixed-size buffer for receiving WebSocket messages, but it doesn't handle fragmented messages correctly. WebSocket messages can be split across multiple frames, and the current implementation will process each frame independently, potentially causing data corruption for large messages.
src/Plugins/BotSharp.Plugin.ChatHub/ChatStreamMiddleware.cs [61-77]
var buffer = new byte[1024 * 32];
WebSocketReceiveResult result;
+var messageBuffer = new List<byte>();
do
{
result = await webSocket.ReceiveAsync(new(buffer), CancellationToken.None);
if (result.MessageType != WebSocketMessageType.Text)
{
continue;
}
- var receivedText = Encoding.UTF8.GetString(buffer, 0, result.Count);
- if (string.IsNullOrEmpty(receivedText))
+ messageBuffer.AddRange(new ArraySegment<byte>(buffer, 0, result.Count));
+
+ if (result.EndOfMessage)
{
- continue;
- }
+ var receivedText = Encoding.UTF8.GetString(messageBuffer.ToArray());
+ messageBuffer.Clear();
+
+ if (string.IsNullOrEmpty(receivedText))
+ {
+ continue;
+ }Suggestion 5:
Avoid potential deadlocks
The code is using .Result to synchronously wait for an asynchronous operation, which can lead to deadlocks in ASP.NET applications. This is a common anti-pattern that should be avoided.
src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs [85-93]
// Convert id to name
if (!Guid.TryParse(agentId, out _))
{
var agentService = _services.GetRequiredService<IAgentService>();
- var agents = agentService.GetAgentOptions([agentId]).Result;
+ var agents = agentService.GetAgentOptions([agentId]).GetAwaiter().GetResult();
if (agents.Count > 0)
{
agentId = agents.First().Id;[Suggestion has been applied]
Suggestion 6:
Check WebSocket state
The SendEventToUser method doesn't check if the WebSocket is in a valid state before sending data. If the client has disconnected or the connection is closing, this could throw an exception and crash the middleware.
src/Plugins/BotSharp.Plugin.ChatHub/ChatStreamMiddleware.cs [106]
-await SendEventToUser(webSocket, data);
+if (webSocket.State == WebSocketState.Open)
+{
+ await SendEventToUser(webSocket, data);
+}[Suggestion has been applied]
[Auto-generated best practices - 2026-03-16]