Skip to content

fix(channel): stop re-summarising worker results that were already relayed#333

Merged
jamiepine merged 1 commit into
mainfrom
fix/stop-reiterating-relayed-worker-results
Mar 6, 2026
Merged

fix(channel): stop re-summarising worker results that were already relayed#333
jamiepine merged 1 commit into
mainfrom
fix/stop-reiterating-relayed-worker-results

Conversation

@jamiepine

Copy link
Copy Markdown
Member

Summary

  • Fixes the channel repeatedly re-summarising stale worker/branch results on subsequent unrelated messages
  • Adds relay tracking to CompletedItem so the status block stops injecting already-relayed results into the system prompt
  • Fixes a pre-existing bug where completed_items pruning (cap at 10) only ran for BranchResult, never for WorkerComplete

Problem

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:

Source Behaviour
Status block "Recently Completed" Rendered full 500-char result summaries into the system prompt on every turn, indefinitely. No "already relayed" tracking, no time-based expiry.
History summary injection After retrigger, a [worker <uuid> completed]: <result> assistant message was pushed into history permanently.
Channel prompt instruction Told the LLM "you MUST relay the full substance" with no distinction between new results and already-relayed ones.

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.rs

  • Added relayed: bool field to CompletedItem (defaults to false)
  • Added mark_relayed() method to flag items after successful retrigger
  • Added prune_completed_items() — drops relayed items older than 5 minutes, hard caps at 10
  • Moved the cap-at-10 pruning out of the BranchResult match arm into the universal prune_completed_items() call (bug fix — it never ran for WorkerComplete)
  • Changed render_with_time_context() to filter out relayed items from "Recently Completed" — only unrelayed items appear

src/agent/channel.rs

  • flush_pending_retrigger() now stores retrigger_process_ids in the synthetic message metadata
  • After a successful retrigger turn (replied=true), extracts those IDs and calls status.mark_relayed()

prompts/en/channel.md.j2

  • Added explicit instruction: once a result has been relayed, do not repeat/re-summarise it unless the user explicitly asks

What 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.

Source Before After
Conversation history (assistant reply) Preserved Preserved (unchanged)
Status block "Recently Completed" Full summary, indefinitely Hidden once relayed=true
Channel prompt instruction "you MUST relay" "once relayed, do NOT repeat"

Validation

just gate-pr passes clean — 428 tests, no clippy warnings, no fmt issues.

…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.
@coderabbitai

coderabbitai Bot commented Mar 5, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This 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

Cohort / File(s) Summary
Prompt guidance
prompts/en/channel.md.j2
Updated AI prompt to clarify that previously relayed results should not be repeated or re-summarized in future turns unless explicitly requested.
Retrigger relay tracking
src/agent/channel.rs
Added logic to read retrigger_process_ids from retrigger metadata and mark corresponding completed processes as relayed, preventing re-summarization of stale results.
Status block relay management
src/agent/status.rs
Introduced per-item relayed flag on CompletedItem, added mark_relayed() public API, implemented prune_completed_items() to expire relayed items after 5 minutes and cap total items at 10, and updated rendering to display only unrelayed items (max 5) with truncated summaries.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and specifically describes the main fix: preventing re-summarisation of worker results that were already relayed, which is the central purpose of this PR.
Description check ✅ Passed The description comprehensively explains the problem, the three-pronged fix, the changes across all modified files, and includes validation results. It clearly relates to the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/stop-reiterating-relayed-worker-results

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.

Comment thread src/agent/status.rs
.retain(|item| !(item.relayed && item.completed_at < cutoff));

// Hard cap: keep the 10 most recent.
while self.completed_items.len() > 10 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Suggested change
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);

Comment thread src/agent/channel.rs
&& let Some(ids) = message
.metadata
.get("retrigger_process_ids")
.and_then(|v| serde_json::from_value::<Vec<String>>(v.clone()).ok())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Worth not swallowing parse errors here: if the metadata ever changes shape, this silently stops marking items as relayed (and the bot regresses).

Suggested change
.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
}
})

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 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 every ProcessEvent, including high-frequency events like WorkerStatus updates. 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 (WorkerComplete with notify=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

📥 Commits

Reviewing files that changed from the base of the PR and between 1e466e3 and c69c082.

📒 Files selected for processing (3)
  • prompts/en/channel.md.j2
  • src/agent/channel.rs
  • src/agent/status.rs

@jamiepine jamiepine merged commit 73fe7c0 into main Mar 6, 2026
4 checks passed
rktmeister pushed a commit to rktmeister/spacebot that referenced this pull request Mar 11, 2026
…erating-relayed-worker-results

fix(channel): stop re-summarising worker results that were already relayed
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