fix(channel): stop re-summarising worker results that were already relayed#333
Conversation
…layed Worker/branch results persisted in the LLM context in three overlapping places with no expiry, causing the channel to reiterate stale results on subsequent unrelated messages: 1. Status block 'Recently Completed' rendered full 500-char summaries into the system prompt on every turn indefinitely 2. History summary injection pushed raw result text as a permanent assistant message after retrigger 3. Channel prompt instruction said 'you must relay' with no awareness of whether results had already been relayed Add a relayed flag to CompletedItem and mark items relayed after a successful retrigger turn. The status block now filters out relayed items so the LLM only sees unrelayed work in 'Recently Completed'. Relayed items expire from the status block after 5 minutes. The LLM's natural-language relay reply remains in conversation history unchanged — the channel retains full context of what was discussed, it just stops being told to relay results it already relayed. Also fixes a pre-existing bug where completed_items pruning (cap at 10) only ran in the BranchResult arm, never for WorkerComplete events.
WalkthroughThis PR implements per-item relay tracking to prevent AI from re-summarizing previously relayed results. It adds relay flags to completed items, marks them as relayed after retrigger completion, prunes old relayed items, and updates status block rendering to exclude relayed items. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
| .retain(|item| !(item.relayed && item.completed_at < cutoff)); | ||
|
|
||
| // Hard cap: keep the 10 most recent. | ||
| while self.completed_items.len() > 10 { |
There was a problem hiding this comment.
remove(0) in a loop shifts the vec each iteration; since this is just a hard cap, drain(..excess) is simpler and avoids repeated moves.
| while self.completed_items.len() > 10 { | |
| // Hard cap: keep the 10 most recent. | |
| let excess = self.completed_items.len().saturating_sub(10); | |
| self.completed_items.drain(0..excess); |
| && let Some(ids) = message | ||
| .metadata | ||
| .get("retrigger_process_ids") | ||
| .and_then(|v| serde_json::from_value::<Vec<String>>(v.clone()).ok()) |
There was a problem hiding this comment.
Worth not swallowing parse errors here: if the metadata ever changes shape, this silently stops marking items as relayed (and the bot regresses).
| .and_then(|v| serde_json::from_value::<Vec<String>>(v.clone()).ok()) | |
| .and_then(|v| match serde_json::from_value::<Vec<String>>(v.clone()) { | |
| Ok(ids) => Some(ids), | |
| Err(error) => { | |
| tracing::debug!( | |
| channel_id = %self.id, | |
| %error, | |
| "failed to parse retrigger_process_ids metadata" | |
| ); | |
| None | |
| } | |
| }) |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/agent/status.rs (1)
137-141: Consider if pruning on every event is necessary.
prune_completed_items()is called unconditionally after everyProcessEvent, including high-frequency events likeWorkerStatusupdates. While the operation is cheap (bounded by the 10-item cap), it could be optimized to run only after events that actually add completed items (WorkerCompletewithnotify=true,BranchResult).That said, the current approach is simpler and the performance impact is negligible with the hard cap.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/agent/status.rs` around lines 137 - 141, prune_completed_items() is being invoked unconditionally after every ProcessEvent (including high-frequency WorkerStatus events); change the call site so pruning runs only when events can add completed items (e.g., inside handling branches for WorkerComplete where notify==true and BranchResult) instead of after every event. Locate the event dispatch/handler that currently calls self.prune_completed_items() and move the call into the specific match arms for WorkerComplete (check the notify flag) and BranchResult so WorkerStatus and other frequent no-op events skip pruning.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/agent/status.rs`:
- Around line 137-141: prune_completed_items() is being invoked unconditionally
after every ProcessEvent (including high-frequency WorkerStatus events); change
the call site so pruning runs only when events can add completed items (e.g.,
inside handling branches for WorkerComplete where notify==true and BranchResult)
instead of after every event. Locate the event dispatch/handler that currently
calls self.prune_completed_items() and move the call into the specific match
arms for WorkerComplete (check the notify flag) and BranchResult so WorkerStatus
and other frequent no-op events skip pruning.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 67eaedfd-b64f-4cc8-b69f-5c9d9a59a401
📒 Files selected for processing (3)
prompts/en/channel.md.j2src/agent/channel.rssrc/agent/status.rs
…erating-relayed-worker-results fix(channel): stop re-summarising worker results that were already relayed
Summary
CompletedItemso the status block stops injecting already-relayed results into the system promptcompleted_itemspruning (cap at 10) only ran forBranchResult, never forWorkerCompleteProblem
After a worker completes and its results are relayed to the user via the retrigger flow, subsequent unrelated messages cause the bot to reiterate the same results. In the transcript that surfaced this, the bot re-summarised the same traceability worker results three separate times while users were clearly talking about something else ("Is this the API for the dashboard?", "Can you invite me to the platform api repo?").
The root cause is that worker results persisted in the LLM's context in three overlapping places with no expiry:
[worker <uuid> completed]: <result>assistant message was pushed into history permanently.The LLM saw the same results on every turn and, following its instructions, kept re-summarising them.
Fix
Three-pronged approach — track relay state, stop re-injecting, update instructions:
src/agent/status.rsrelayed: boolfield toCompletedItem(defaults tofalse)mark_relayed()method to flag items after successful retriggerprune_completed_items()— drops relayed items older than 5 minutes, hard caps at 10BranchResultmatch arm into the universalprune_completed_items()call (bug fix — it never ran forWorkerComplete)render_with_time_context()to filter out relayed items from "Recently Completed" — only unrelayed items appearsrc/agent/channel.rsflush_pending_retrigger()now storesretrigger_process_idsin the synthetic message metadatareplied=true), extracts those IDs and callsstatus.mark_relayed()prompts/en/channel.md.j2What the channel still sees
The LLM's natural-language relay reply (e.g. "worker finished — here's the traceability breakdown: ...") remains in conversation history as a normal assistant message, unchanged. The channel retains full context of what was discussed — it just stops being told to relay results it already relayed turns ago.
relayed=trueValidation
just gate-prpasses clean — 428 tests, no clippy warnings, no fmt issues.