Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions prompts/en/channel.md.j2
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ You have a soul, an identity, and a personality. These are loaded separately and

## How You Work

Every turn, you receive the user's message along with a live status block showing active workers, branches, and recently completed work. Use this to stay aware of what's happening without asking.
Every turn, you receive the user's message along with a live status block showing active workers and branches. Use this to stay aware of what's happening without asking.
The status block includes a current date/time line with timezone and UTC. Treat that as the source of truth for words like "today", "tomorrow", "yesterday", "now", and "later today".

When a background process (branch or worker) completes, you will receive a system message containing the full result text, tagged with the process type and ID. The user has NOT seen any of it — you must relay the substance to them using the reply tool. Include actual content and details, not just a summary teaser. Do not mention internal processes (branch, worker, process IDs). If a result is background work the user didn't ask about, incorporate it silently.
When a background process (branch or worker) completes, you will receive a **system message** containing the full result text. The user has NOT seen any of it — you must relay the substance to them using the reply tool. Include actual content and details, not just a summary teaser. Do not mention internal processes (branch, worker, process IDs). If a result is background work the user didn't ask about, incorporate it silently.

**Important:** Once you have relayed a result, it is done. The status block may still show a "Recently Completed" section for unrelayed work — but if you already relayed a result in a previous turn, do NOT repeat, re-summarise, or reference it again unless the user explicitly asks about it. Treat relayed results the same as any other past conversation — they are in the history, the user saw them, move on.

When work produces a user-facing file artifact (PDF, DOCX, CSV, ZIP, image, etc.), deliver the file with `send_file`. Do not paste local filesystem paths as the main handoff. Local paths are machine-local and usually useless to the user.

Expand Down
30 changes: 30 additions & 0 deletions src/agent/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1573,6 +1573,24 @@ impl Channel {
content: OneOrMany::one(rig::message::AssistantContent::text(record)),
});
}

// Mark the completed items as relayed in the status block so their
// full result summaries stop appearing on subsequent turns. This
// prevents the LLM from re-summarising stale worker/branch results.
if replied
&& 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
}
})

{
let mut status = self.state.status_block.write().await;
status.mark_relayed(&ids);
tracing::debug!(
channel_id = %self.id,
count = ids.len(),
"marked retrigger results as relayed in status block"
);
}
}

// Check context size and trigger compaction if needed
Expand Down Expand Up @@ -2426,11 +2444,23 @@ impl Channel {
.collect::<Vec<_>>()
.join("\n");

// Collect the process IDs so we can mark them as relayed in the
// status block after the retrigger turn completes successfully.
let retrigger_process_ids: Vec<String> = self
.pending_results
.iter()
.map(|r| r.process_id.clone())
.collect();

let mut metadata = self.pending_retrigger_metadata.clone();
metadata.insert(
"retrigger_result_summary".to_string(),
serde_json::Value::String(result_summary),
);
metadata.insert(
"retrigger_process_ids".to_string(),
serde_json::json!(retrigger_process_ids),
);

let synthetic = InboundMessage {
id: uuid::Uuid::new_v4().to_string(),
Expand Down
53 changes: 45 additions & 8 deletions src/agent/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ pub struct CompletedItem {
pub description: String,
pub completed_at: DateTime<Utc>,
pub result_summary: String,
/// Whether this item's result has been relayed to the user via retrigger.
/// Once relayed, the result summary is excluded from the status block to
/// prevent the LLM from re-summarising stale results.
pub relayed: bool,
}

/// Status of an active link conversation.
Expand Down Expand Up @@ -94,6 +98,7 @@ impl StatusBlock {
description: worker.task,
completed_at: Utc::now(),
result_summary: result.clone(),
relayed: false,
});
}
}
Expand All @@ -120,19 +125,42 @@ impl StatusBlock {
description: branch.description,
completed_at: Utc::now(),
result_summary: conclusion.clone(),
relayed: false,
});
}

// Keep only last 10 completed items
if self.completed_items.len() > 10 {
self.completed_items.remove(0);
}
}
ProcessEvent::AgentMessageSent { to_agent_id, .. } => {
self.track_link_conversation(to_agent_id.as_ref());
}
_ => {}
}

// Prune completed items: drop relayed items older than 5 minutes,
// then cap at 10 to bound status block size.
self.prune_completed_items();
}

/// Mark completed items as relayed so the status block stops showing
/// their full result summaries. Called after a retrigger turn succeeds.
pub fn mark_relayed(&mut self, process_ids: &[String]) {
for item in &mut self.completed_items {
if process_ids.contains(&item.id) {
item.relayed = true;
}
}
}

/// Remove stale completed items: relayed items older than 5 minutes are
/// dropped entirely, then total count is capped at 10.
fn prune_completed_items(&mut self) {
let cutoff = Utc::now() - chrono::Duration::minutes(5);
self.completed_items
.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);

self.completed_items.remove(0);
}
}

/// Add a new active branch.
Expand Down Expand Up @@ -246,10 +274,19 @@ impl StatusBlock {
output.push('\n');
}

// Recently completed
if !self.completed_items.is_empty() {
// Recently completed — only show items not yet relayed to the user.
// Relayed items already appeared in conversation via the retrigger flow;
// keeping their full summaries here causes the LLM to re-summarise them.
let unrelayed: Vec<_> = self
.completed_items
.iter()
.rev()
.filter(|item| !item.relayed)
.take(5)
.collect();
if !unrelayed.is_empty() {
output.push_str("## Recently Completed\n");
for item in self.completed_items.iter().rev().take(5) {
for item in &unrelayed {
let type_str = match item.item_type {
CompletedItemType::Branch => "branch",
CompletedItemType::Worker => "worker",
Expand Down