Skip to content

fix: MCP tool refresh, final message delivery, and overflow retry#270

Merged
ModerRAS merged 1 commit intomasterfrom
fix/mcp-init-and-final-message-delivery
Apr 18, 2026
Merged

fix: MCP tool refresh, final message delivery, and overflow retry#270
ModerRAS merged 1 commit intomasterfrom
fix/mcp-init-and-final-message-delivery

Conversation

@ModerRAS
Copy link
Copy Markdown
Owner

@ModerRAS ModerRAS commented Apr 18, 2026

Summary

Fixes three bugs in the LLM agent process separation system:

Bug 1: Final message content not delivered to Telegram

Root cause: When the agent completes (Done terminal), \ChunkPollingService\ checked \ erminal.Content\ which was empty for Done chunks, then immediately closed the channel without reading the final snapshot from Redis.

Fix: \PollTaskAsync\ now calls \DeliverFinalSnapshotAsync()\ to read the \AGENT_SNAPSHOT:{taskId}\ key and deliver it to the channel before closing. The \TryCompleteFromTaskStateAsync\ fallback path also reads both the snapshot key and \lastContent\ from task state hash.

Bug 2: Overflow messages stuck when content spills to new TG message

Root cause: \markdownActuallySynced\ was set before calling \SynchronizeTelegramMessagesInternalAsync, so if the sync partially failed (new message send error), the final retry check \latestMarkdownSnapshot != markdownActuallySynced\ was false — no retry happened.

Fix: \SynchronizeTelegramMessagesInternalAsync\ now returns a \SyncComplete\ flag. \markdownActuallySynced\ is only set after a fully successful sync, and the final retry condition also checks !lastSyncComplete.

Bug 3: MCP tools not available after runtime changes

Root cause: Tool definitions were exported to Redis once at startup. When MCP servers were added/removed/restarted at runtime, \RegisterExternalMcpTools()\ updated the in-process registry, but the Redis key \AGENT_TOOL_DEFS\ was never refreshed.

Fix: Added \McpToolHelper.RefreshAgentToolDefsInRedisAsync()\ and call it after every \RegisterExternalMcpTools()\ in \EditMcpConfService\ (5 sites) and \McpInstallerToolService\ (4 sites). Startup also uses this centralized method.

Tests

  • Added 2 new tests for \ChunkPollingService:
    • Terminal Done + snapshot in Redis → final snapshot delivered via stream
    • Task-state Completed + lastContent → content delivered as fallback
  • All 417 tests pass (226 + 187 + 4)

Files Changed

  • \ChunkPollingService.cs\ - final snapshot delivery + helper method
  • \SendMessageService.Streaming.cs\ - SyncComplete flag + conditional markdownActuallySynced
  • \McpToolHelper.cs\ - \RefreshAgentToolDefsInRedisAsync()\ method
  • \EditMcpConfService.cs\ - Redis refresh after MCP changes (5 sites)
  • \McpInstallerToolService.cs\ - Redis refresh after MCP changes (4 sites) + DI for IConnectionMultiplexer
  • \GeneralBootstrap.cs\ - use centralized refresh method
  • Tests updated for new constructor + 2 new test cases

Summary by CodeRabbit

Release Notes

  • New Features

    • Tool definitions are now cached in Redis for improved system performance.
  • Bug Fixes

    • Improved handling of final snapshots in task polling.
    • Enhanced Telegram message synchronization with better completion state tracking for increased reliability.

…ls in Redis

Bug 1 - Final message not delivered:
ChunkPollingService now reads the final snapshot from Redis before
closing the channel on terminal Done chunks. Also handles task-state
Completed fallback by reading lastContent from the hash.

Bug 2 - Overflow messages stuck:
SynchronizeTelegramMessagesInternalAsync now returns a SyncComplete
flag. markdownActuallySynced is only set after a fully successful
sync, ensuring the final retry fires when sends partially fail.

Bug 3 - MCP tools not refreshed after runtime changes:
Added McpToolHelper.RefreshAgentToolDefsInRedisAsync() and call it
after every RegisterExternalMcpTools() in EditMcpConfService and
McpInstallerToolService. Startup also uses the centralized method.

Tests: Added 2 new tests verifying final snapshot delivery on Done
terminal and task-state Completed fallback. All 417 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 18, 2026

📝 Walkthrough

Walkthrough

The changes integrate Redis-backed refreshing of agent tool definitions across multiple services. A new method RefreshAgentToolDefsInRedisAsync is added to McpToolHelper to serialize and store tool definitions in Redis with 24-hour expiration. McpInstallerToolService and EditMcpConfService now receive IConnectionMultiplexer as a dependency and invoke this refresh method after tool management operations. ChunkPollingService is enhanced to deliver final snapshots before task completion, and SendMessageService.Streaming now tracks sync completion status.

Changes

Cohort / File(s) Summary
Redis Tool Definition Refresh
TelegramSearchBot.LLM/Service/AI/LLM/McpToolHelper.cs, TelegramSearchBot.LLM/Service/Tools/McpInstallerToolService.cs, TelegramSearchBot/Service/Manage/EditMcpConfService.cs, TelegramSearchBot/AppBootstrap/GeneralBootstrap.cs
Added RefreshAgentToolDefsInRedisAsync method to serialize and persist tool definitions in Redis. McpInstallerToolService and EditMcpConfService now accept IConnectionMultiplexer dependency and call refresh after tool management operations. Bootstrap refactored to delegate refresh logic to the helper method.
Snapshot Delivery Enhancement
TelegramSearchBot/Service/AI/LLM/ChunkPollingService.cs
Introduced DeliverFinalSnapshotAsync helper to read and write final snapshot from Redis before task completion. Updated terminal-chunk and task-state completion flows to ensure consumers receive last snapshot content before completion signal.
Streaming Sync Tracking
TelegramSearchBot/Service/BotAPI/SendMessageService.Streaming.cs
Added SyncComplete boolean to sync result tuple to track successful incremental Telegram synchronization. Final sync now triggers when snapshot differs from synced content OR previous sync was incomplete.
Test Coverage Updates
TelegramSearchBot.LLM.Test/Service/Tools/McpInstallerToolServiceTests.cs, TelegramSearchBot.Test/Service/AI/LLM/ChunkPollingServiceTests.cs
Updated McpInstallerToolService test setup with Redis mocking. Added new test cases for final snapshot delivery with both Redis snapshot values and task state fallback scenarios.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 A rabbit hops through Redis streams,
Tool definitions dance in dreams,
Snapshots flow before the end,
Sync completion, new friend,
Cache and care, all systems blend! 🔴✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the three main bug fixes: MCP tool refresh after runtime changes, final message delivery from Redis snapshots, and overflow retry logic for Telegram synchronization.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/mcp-init-and-final-message-delivery

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
TelegramSearchBot/Service/BotAPI/SendMessageService.Streaming.cs (3)

259-263: ⚠️ Potential issue | 🟠 Major

Keep failed deletes retryable.

A delete failure means the Telegram message is still live, but the returned state drops it and reports SyncComplete=true. That can make overflow/stale messages untracked and unretryable.

🐛 Proposed fix
                         try {
                             await this.botClient.DeleteMessage(chatId, existingMsg.MessageId, cancellationToken: cancellationToken);
                             this.logger.LogInformation($"SynchronizeMessages: Deleted message {existingMsg.MessageId} for empty chunk {i}.");
                             nextHtmlMap.Remove(existingMsg.MessageId);
-                        } catch (Exception ex) { this.logger.LogError(ex, $"SynchronizeMessages: Error deleting TG message {existingMsg.MessageId} for empty chunk {i}."); }
+                        } catch (Exception ex) {
+                            this.logger.LogError(ex, $"SynchronizeMessages: Error deleting TG message {existingMsg.MessageId} for empty chunk {i}.");
+                            syncComplete = false;
+                            nextTgMessagesState.Add(existingMsg);
+                        }
@@
                 try {
                     await this.botClient.DeleteMessage(chatId, currentTgMessages[i].MessageId, cancellationToken: cancellationToken);
                     this.logger.LogInformation($"SynchronizeMessages: Deleted superfluous message {currentTgMessages[i].MessageId}.");
                     nextHtmlMap.Remove(currentTgMessages[i].MessageId);
-                } catch (Exception ex) { this.logger.LogError(ex, $"SynchronizeMessages: Error deleting superfluous TG message {currentTgMessages[i].MessageId}."); }
+                } catch (Exception ex) {
+                    this.logger.LogError(ex, $"SynchronizeMessages: Error deleting superfluous TG message {currentTgMessages[i].MessageId}.");
+                    syncComplete = false;
+                    nextTgMessagesState.Add(currentTgMessages[i]);
+                }

Also applies to: 286-292

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TelegramSearchBot/Service/BotAPI/SendMessageService.Streaming.cs` around
lines 259 - 263, In SynchronizeMessages, do not remove message IDs from
nextHtmlMap when DeleteMessage fails—only call
nextHtmlMap.Remove(existingMsg.MessageId) after a successful await
this.botClient.DeleteMessage(...) inside the try block; in the catch block log
the error (using this.logger.LogError) but do NOT remove the ID so the message
remains retryable. Apply the same change to the other delete block referenced
(the one around lines 286-292) so both deletion paths only remove from
nextHtmlMap on success.

272-281: ⚠️ Potential issue | 🟠 Major

Stop appending after a failed new chunk send.

If chunk i fails but chunk i + 1 is sent, nextTgMessagesState becomes index-shifted; BuildResultForDbAsync then pairs the later Telegram message with the wrong markdown chunk. Mark invalid sends incomplete and stop so the final retry resumes from the missing chunk.

🐛 Proposed fix
                             if (newMsg != null && newMsg.MessageId != 0) {
                                 nextTgMessagesState.Add(newMsg);
                                 nextHtmlMap[newMsg.MessageId] = htmlChunk;
                             } else {
                                 this.logger.LogWarning($"SynchronizeMessages: Sending new TG message for chunk {i} returned null or invalid Message object.");
+                                syncComplete = false;
+                                break;
                             }
                         } catch (Exception ex) {
                             this.logger.LogError(ex, $"SynchronizeMessages: Error sending new TG message for chunk {i}.");
                             syncComplete = false;
+                            break;
                         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TelegramSearchBot/Service/BotAPI/SendMessageService.Streaming.cs` around
lines 272 - 281, In SynchronizeMessages, when a send for chunk i fails (either
newMsg is null/invalid or an exception occurs), stop appending subsequent
successful sends by marking syncComplete = false and breaking out of the loop
instead of continuing; specifically, in the block handling newMsg (where
nextTgMessagesState and nextHtmlMap are updated) only add entries when newMsg is
valid, and in the else branch (invalid newMsg) set syncComplete = false and
break, and likewise in the catch block set syncComplete = false and break so
BuildResultForDbAsync won’t get an index-shifted nextTgMessagesState paired with
the wrong markdown chunk.

238-256: ⚠️ Potential issue | 🟠 Major

Mark failed edits as incomplete before advancing the synced snapshot.

If EditMessageText fails or returns an invalid message, Telegram still contains the old chunk, but this method currently returns SyncComplete=true; the caller can then skip the final retry and leave stale content visible.

🐛 Proposed fix
                                 if (editedMsg != null && editedMsg.MessageId != 0) {
                                     nextTgMessagesState.Add(editedMsg);
                                     nextHtmlMap[editedMsg.MessageId] = htmlChunk;
                                 } else {
                                     this.logger.LogWarning($"SynchronizeMessages: Editing TG message {existingMsg.MessageId} for chunk {i} returned null or invalid MessageId. Adding existing to list.");
+                                    syncComplete = false;
                                     nextTgMessagesState.Add(existingMsg);
                                 }
@@
                             } catch (Exception ex) {
                                 this.logger.LogError(ex, $"SynchronizeMessages: Error editing TG message {existingMsg.MessageId} for chunk {i}. Adding existing to list.");
+                                syncComplete = false;
                                 nextTgMessagesState.Add(existingMsg);
                             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TelegramSearchBot/Service/BotAPI/SendMessageService.Streaming.cs` around
lines 238 - 256, In SynchronizeMessages, when EditMessageText fails (catch
Exception) or returns null/invalid (editedMsg == null || editedMsg.MessageId ==
0), mark that chunk as not synchronized before progressing the snapshot by
setting the sync-completion flag/indicator to false (e.g., set a local
syncComplete = false or add the messageId to an incomplete set) and avoid
updating nextHtmlMap for that message; keep the ApiRequestException branch for
"message is not modified" as a successful case. Ensure the code paths that call
this method (which inspect SyncComplete) will see the failure indicator so a
final retry will run for that chunk.
🧹 Nitpick comments (4)
TelegramSearchBot.LLM/Service/Tools/McpInstallerToolService.cs (1)

27-38: Constructor DI addition is correct; consider null-guarding.

Existing parameters use ??throw (see EditMcpConfService for _mcpServerManager), while here none are null-guarded. Low risk since DI always provides singletons, but consider _redis = redis ?? throw new ArgumentNullException(nameof(redis)); for consistency and earlier failure if mis-wired.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TelegramSearchBot.LLM/Service/Tools/McpInstallerToolService.cs` around lines
27 - 38, The constructor for McpInstallerToolService doesn’t null-guard its
injected dependencies; add null checks (e.g., _redis = redis ?? throw new
ArgumentNullException(nameof(redis))) in the McpInstallerToolService
constructor—apply the same pattern to other injected fields (_logger,
_mcpServerManager) if they aren’t already guarded to match EditMcpConfService
and fail fast on misconfiguration.
TelegramSearchBot/Service/Manage/EditMcpConfService.cs (1)

246-246: Refresh calls are consistently placed after RegisterExternalMcpTools — good.

All 5 sites correctly pair RegisterExternalMcpTools(_mcpServerManager) with await McpToolHelper.RefreshAgentToolDefsInRedisAsync(connectionMultiplexer) inside the existing try/catch blocks, so agent processes will see tool changes immediately after add/edit/env-update/remove/restart. This matches the invariant maintained in McpInstallerToolService.cs.

One nit: in HandleEditingInputValueAsync (around line 393), the refresh still runs when the FieldIdTimeout/FieldIdEnabled branches set changeDescription to an error string (no config actually changed). It is harmless but produces an unnecessary Redis write; pre-existing logic, so not blocking.

Also applies to: 393-393, 435-435, 486-486, 551-551

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TelegramSearchBot/Service/Manage/EditMcpConfService.cs` at line 246,
HandleEditingInputValueAsync currently always calls
McpToolHelper.RefreshAgentToolDefsInRedisAsync(connectionMultiplexer) after
RegisterExternalMcpTools(_mcpServerManager), even when branches for
FieldIdTimeout/FieldIdEnabled set changeDescription to an error (no config
change). Modify HandleEditingInputValueAsync so that after the branch that sets
changeDescription for FieldIdTimeout/FieldIdEnabled you only call
RegisterExternalMcpTools(_mcpServerManager) and await
McpToolHelper.RefreshAgentToolDefsInRedisAsync(connectionMultiplexer) when an
actual change occurred (e.g., changeDescription does not indicate the
error/no-op). Keep the existing try/catch structure and the pairing of
RegisterExternalMcpTools with RefreshAgentToolDefsInRedisAsync, but gate the
refresh behind a condition that checks that a config change was performed.
TelegramSearchBot/Service/AI/LLM/ChunkPollingService.cs (2)

149-172: Optional: the inner DeliverFinalSnapshotAsync call is largely redundant here.

TryCompleteFromTaskStateAsync is only reached when PollTaskAsync already observed AgentSnapshot(taskId) as Null (see lines 107-112). Re-reading the same key on line 151 will almost always be a no-op; it's a useful belt-and-suspenders read to close a tiny race between the two StringGetAsync calls, but a short comment noting that (or dropping it in favor of relying solely on the lastContent fallback) would make the intent clearer.

Either option is fine — noting this for clarity only.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TelegramSearchBot/Service/AI/LLM/ChunkPollingService.cs` around lines 149 -
172, The DeliverFinalSnapshotAsync call inside TryCompleteFromTaskStateAsync is
effectively redundant because PollTaskAsync already observed
AgentSnapshot(taskId) == Null; either remove the
DeliverFinalSnapshotAsync(taskId, tracked, db, cancellationToken) call or add a
short clarifying comment above it noting it's a belt‑and‑suspenders re-read to
close a tiny race between the two StringGetAsync calls and that the subsequent
lastContent fallback (the lastContentEntry/lastContent check and
CompleteTrackedTaskAsync) is the actual safety net; update
TryCompleteFromTaskStateAsync accordingly and ensure behavior remains covered by
the lastContent/CompleteTrackedTaskAsync path.

186-199: Minor: the catch (Exception) swallows cancellation as well.

tracked.Channel.Writer.WriteAsync(..., cancellationToken) can throw OperationCanceledException when the polling loop is stopping, and the current catch silently discards it. That's consistent with the "best-effort" comment, but it makes shutdown observe a bit odd (the task still tries to complete afterwards even though cancellation was signaled). Consider either letting cancellation propagate or logging it.

♻️ Optional refinement
-        private async Task DeliverFinalSnapshotAsync(string taskId, TrackedTask tracked, IDatabase db, CancellationToken cancellationToken) {
-            try {
-                var snapshotJson = await db.StringGetAsync(LlmAgentRedisKeys.AgentSnapshot(taskId));
-                if (!snapshotJson.HasValue) return;
-
-                var snapshot = JsonConvert.DeserializeObject<AgentStreamChunk>(snapshotJson.ToString());
-                if (snapshot != null && !string.IsNullOrEmpty(snapshot.Content) && snapshot.Content != tracked.LastContent) {
-                    tracked.LastContent = snapshot.Content;
-                    await tracked.Channel.Writer.WriteAsync(snapshot, cancellationToken);
-                }
-            } catch (Exception) {
-                // Best-effort: don't let snapshot read failure prevent task completion
-            }
-        }
+        private async Task DeliverFinalSnapshotAsync(string taskId, TrackedTask tracked, IDatabase db, CancellationToken cancellationToken) {
+            try {
+                var snapshotJson = await db.StringGetAsync(LlmAgentRedisKeys.AgentSnapshot(taskId));
+                if (!snapshotJson.HasValue) return;
+
+                var snapshot = JsonConvert.DeserializeObject<AgentStreamChunk>(snapshotJson.ToString());
+                if (snapshot != null && !string.IsNullOrEmpty(snapshot.Content) && snapshot.Content != tracked.LastContent) {
+                    tracked.LastContent = snapshot.Content;
+                    await tracked.Channel.Writer.WriteAsync(snapshot, cancellationToken);
+                }
+            } catch (OperationCanceledException) {
+                throw;
+            } catch (Exception) {
+                // Best-effort: don't let snapshot read/deserialize failure prevent task completion
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TelegramSearchBot/Service/AI/LLM/ChunkPollingService.cs` around lines 186 -
199, In DeliverFinalSnapshotAsync, the broad catch is swallowing cancellation;
change the error handling to let OperationCanceledException (or
TaskCanceledException) propagate by catching it separately and rethrowing (or
simply not catching it), while retaining the existing best-effort behavior for
other exceptions—e.g., replace the single catch (Exception) with a catch
(OperationCanceledException) { throw; } followed by a general catch (Exception)
{ /* best-effort log/ignore */ } so tracked.Channel.Writer.WriteAsync(...)
cancellation is observed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@TelegramSearchBot.LLM/Service/AI/LLM/McpToolHelper.cs`:
- Around line 1063-1073: RefreshAgentToolDefsInRedisAsync currently throws any
Redis exceptions and lacks a null-guard for the redis parameter, which can cause
upstream operations (e.g., EditMcpConfService.* handlers,
RestartAllServersAsync, McpInstallerToolService paths) to fail even though the
MCP change succeeded; modify RefreshAgentToolDefsInRedisAsync to validate redis
!= null and throw an ArgumentNullException if null, then wrap the Redis call in
a try/catch that logs the exception via the existing logging facility (or a new
logger) and suppresses the exception so the operation is a soft-fail (or
alternatively return a bool indicating success) — keep callers unchanged but
rely on the method to handle transient Redis outages and emit a clear warning
including exception details and context (method name and
LlmAgentRedisKeys.AgentToolDefs) so UX isn’t broken by stale Redis.

---

Outside diff comments:
In `@TelegramSearchBot/Service/BotAPI/SendMessageService.Streaming.cs`:
- Around line 259-263: In SynchronizeMessages, do not remove message IDs from
nextHtmlMap when DeleteMessage fails—only call
nextHtmlMap.Remove(existingMsg.MessageId) after a successful await
this.botClient.DeleteMessage(...) inside the try block; in the catch block log
the error (using this.logger.LogError) but do NOT remove the ID so the message
remains retryable. Apply the same change to the other delete block referenced
(the one around lines 286-292) so both deletion paths only remove from
nextHtmlMap on success.
- Around line 272-281: In SynchronizeMessages, when a send for chunk i fails
(either newMsg is null/invalid or an exception occurs), stop appending
subsequent successful sends by marking syncComplete = false and breaking out of
the loop instead of continuing; specifically, in the block handling newMsg
(where nextTgMessagesState and nextHtmlMap are updated) only add entries when
newMsg is valid, and in the else branch (invalid newMsg) set syncComplete =
false and break, and likewise in the catch block set syncComplete = false and
break so BuildResultForDbAsync won’t get an index-shifted nextTgMessagesState
paired with the wrong markdown chunk.
- Around line 238-256: In SynchronizeMessages, when EditMessageText fails (catch
Exception) or returns null/invalid (editedMsg == null || editedMsg.MessageId ==
0), mark that chunk as not synchronized before progressing the snapshot by
setting the sync-completion flag/indicator to false (e.g., set a local
syncComplete = false or add the messageId to an incomplete set) and avoid
updating nextHtmlMap for that message; keep the ApiRequestException branch for
"message is not modified" as a successful case. Ensure the code paths that call
this method (which inspect SyncComplete) will see the failure indicator so a
final retry will run for that chunk.

---

Nitpick comments:
In `@TelegramSearchBot.LLM/Service/Tools/McpInstallerToolService.cs`:
- Around line 27-38: The constructor for McpInstallerToolService doesn’t
null-guard its injected dependencies; add null checks (e.g., _redis = redis ??
throw new ArgumentNullException(nameof(redis))) in the McpInstallerToolService
constructor—apply the same pattern to other injected fields (_logger,
_mcpServerManager) if they aren’t already guarded to match EditMcpConfService
and fail fast on misconfiguration.

In `@TelegramSearchBot/Service/AI/LLM/ChunkPollingService.cs`:
- Around line 149-172: The DeliverFinalSnapshotAsync call inside
TryCompleteFromTaskStateAsync is effectively redundant because PollTaskAsync
already observed AgentSnapshot(taskId) == Null; either remove the
DeliverFinalSnapshotAsync(taskId, tracked, db, cancellationToken) call or add a
short clarifying comment above it noting it's a belt‑and‑suspenders re-read to
close a tiny race between the two StringGetAsync calls and that the subsequent
lastContent fallback (the lastContentEntry/lastContent check and
CompleteTrackedTaskAsync) is the actual safety net; update
TryCompleteFromTaskStateAsync accordingly and ensure behavior remains covered by
the lastContent/CompleteTrackedTaskAsync path.
- Around line 186-199: In DeliverFinalSnapshotAsync, the broad catch is
swallowing cancellation; change the error handling to let
OperationCanceledException (or TaskCanceledException) propagate by catching it
separately and rethrowing (or simply not catching it), while retaining the
existing best-effort behavior for other exceptions—e.g., replace the single
catch (Exception) with a catch (OperationCanceledException) { throw; } followed
by a general catch (Exception) { /* best-effort log/ignore */ } so
tracked.Channel.Writer.WriteAsync(...) cancellation is observed.

In `@TelegramSearchBot/Service/Manage/EditMcpConfService.cs`:
- Line 246: HandleEditingInputValueAsync currently always calls
McpToolHelper.RefreshAgentToolDefsInRedisAsync(connectionMultiplexer) after
RegisterExternalMcpTools(_mcpServerManager), even when branches for
FieldIdTimeout/FieldIdEnabled set changeDescription to an error (no config
change). Modify HandleEditingInputValueAsync so that after the branch that sets
changeDescription for FieldIdTimeout/FieldIdEnabled you only call
RegisterExternalMcpTools(_mcpServerManager) and await
McpToolHelper.RefreshAgentToolDefsInRedisAsync(connectionMultiplexer) when an
actual change occurred (e.g., changeDescription does not indicate the
error/no-op). Keep the existing try/catch structure and the pairing of
RegisterExternalMcpTools with RefreshAgentToolDefsInRedisAsync, but gate the
refresh behind a condition that checks that a config change was performed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 23d73e61-d495-4a1a-9e5a-d5ac5782cc9f

📥 Commits

Reviewing files that changed from the base of the PR and between 22588e3 and f0d47d0.

📒 Files selected for processing (8)
  • TelegramSearchBot.LLM.Test/Service/Tools/McpInstallerToolServiceTests.cs
  • TelegramSearchBot.LLM/Service/AI/LLM/McpToolHelper.cs
  • TelegramSearchBot.LLM/Service/Tools/McpInstallerToolService.cs
  • TelegramSearchBot.Test/Service/AI/LLM/ChunkPollingServiceTests.cs
  • TelegramSearchBot/AppBootstrap/GeneralBootstrap.cs
  • TelegramSearchBot/Service/AI/LLM/ChunkPollingService.cs
  • TelegramSearchBot/Service/BotAPI/SendMessageService.Streaming.cs
  • TelegramSearchBot/Service/Manage/EditMcpConfService.cs

Comment on lines +1063 to +1073
/// <summary>
/// Re-exports tool definitions to Redis so that newly spawned agent processes
/// discover the latest available tools (including any MCP servers added/removed at runtime).
/// </summary>
public static async Task RefreshAgentToolDefsInRedisAsync(StackExchange.Redis.IConnectionMultiplexer redis) {
var toolDefs = ExportToolDefinitions();
var json = JsonConvert.SerializeObject(toolDefs);
await redis.GetDatabase().StringSetAsync(
LlmAgentRedisKeys.AgentToolDefs, json, TimeSpan.FromHours(24));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Refresh failures will surface to callers; consider the blast radius.

RefreshAgentToolDefsInRedisAsync does not log and does not swallow errors. In GeneralBootstrap.cs the call is wrapped in a try/catch that treats the failure as a soft warning, but in EditMcpConfService.HandleAddingTimeoutAsync/HandleEditingInputValueAsync/HandleEditingEnvValueAsync/HandleDeletingConfirmAsync/RestartAllServersAsync and in all McpInstallerToolService paths, the refresh is inside the outer operation's try/catch. A transient Redis outage will cause the add/remove/update/restart operation to report failure to the user even though the MCP server change already succeeded (server was added but refresh threw, which rolls nothing back). Consider either:

  1. Catching and logging within RefreshAgentToolDefsInRedisAsync so stale-Redis does not break the MCP lifecycle UX, or
  2. Wrapping each call site in its own try/catch and only warning on failure.

Also worth noting: no null-guard on redis, which would produce an NRE rather than a meaningful error if a caller ever passes null.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TelegramSearchBot.LLM/Service/AI/LLM/McpToolHelper.cs` around lines 1063 -
1073, RefreshAgentToolDefsInRedisAsync currently throws any Redis exceptions and
lacks a null-guard for the redis parameter, which can cause upstream operations
(e.g., EditMcpConfService.* handlers, RestartAllServersAsync,
McpInstallerToolService paths) to fail even though the MCP change succeeded;
modify RefreshAgentToolDefsInRedisAsync to validate redis != null and throw an
ArgumentNullException if null, then wrap the Redis call in a try/catch that logs
the exception via the existing logging facility (or a new logger) and suppresses
the exception so the operation is a soft-fail (or alternatively return a bool
indicating success) — keep callers unchanged but rely on the method to handle
transient Redis outages and emit a clear warning including exception details and
context (method name and LlmAgentRedisKeys.AgentToolDefs) so UX isn’t broken by
stale Redis.

@github-actions
Copy link
Copy Markdown
Contributor

🔍 PR检查报告

📋 检查概览

🧪 测试结果

平台 状态 详情
Ubuntu 🔴 失败 测试结果不可用
Windows 🔴 失败 测试结果不可用

📊 代码质量

  • ✅ 代码格式化检查
  • ✅ 安全漏洞扫描
  • ✅ 依赖包分析
  • ✅ 代码覆盖率收集

📁 测试产物

  • 测试结果文件已上传为artifacts
  • 代码覆盖率已上传到Codecov

🔗 相关链接


此报告由GitHub Actions自动生成

@ModerRAS ModerRAS merged commit 854437e into master Apr 18, 2026
8 checks passed
@ModerRAS ModerRAS deleted the fix/mcp-init-and-final-message-delivery branch April 18, 2026 02:39
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.

1 participant