diff --git a/AGENTS.md b/AGENTS.md index 97d13aa73..ae4d945b0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -174,7 +174,7 @@ src/ │ ├── react.rs — add emoji reaction (channel only) │ ├── memory_save.rs — write memory to store (branch + cortex + compactor) │ ├── memory_recall.rs— search + curate memories (branch only) -│ ├── channel_recall.rs— retrieve transcript from other channels (branch only) +│ ├── channel_recall.rs— retrieve transcript from any channel (branch only) │ ├── set_status.rs — update worker status (workers only) │ ├── shell.rs — execute shell commands (task workers) │ ├── file.rs — read/write/list files (task workers) diff --git a/prompts/en/channel.md.j2 b/prompts/en/channel.md.j2 index bc68ec21f..ca982c529 100644 --- a/prompts/en/channel.md.j2 +++ b/prompts/en/channel.md.j2 @@ -45,7 +45,7 @@ You are able to write code or do work extremely fast inside a worker, never say You have three paths for getting things done. Choosing the right one matters. -**Branch** — for thinking and memory. Branch when you need to recall, save, or forget something from long-term memory, manage the task board (create, list, update, or approve tasks), reason through a complex decision, figure out what instructions to give a worker, answer Spacebot self-knowledge questions (features, architecture, configuration, release notes), or retrieve transcript context from another channel. Branches have your full conversation context and access to the memory system (recall, save, and delete), Spacebot docs lookup (`spacebot_docs`), task tools (`task_create`, `task_list`, `task_update`), cross-channel transcript recall (`channel_recall`), and worker transcript inspection (`worker_inspect`). They return a conclusion. You never see the working. Branch often — it's cheap and keeps you responsive. +**Branch** — for thinking and memory. Branch when you need to recall, save, or forget something from long-term memory, manage the task board (create, list, update, or approve tasks), reason through a complex decision, figure out what instructions to give a worker, answer Spacebot self-knowledge questions (features, architecture, configuration, release notes), or retrieve transcript context from another channel. Branches have your full conversation context and access to the memory system (recall, save, and delete), Spacebot docs lookup (`spacebot_docs`), task tools (`task_create`, `task_list`, `task_update`), cross-channel transcript recall (`channel_recall` — queries the full persisted message database, supports temporal filtering), and worker transcript inspection (`worker_inspect`). They return a conclusion. You never see the working. Branch often — it's cheap and keeps you responsive. **Worker** — for doing. Workers have execution tools (see Worker Capabilities section below). They do NOT have your conversation context or access to memories — they only know what you tell them in the task description, so be specific. Two flavors: @@ -60,6 +60,8 @@ Use `worker_inspect` in a branch when you need to verify what a worker actually The key distinction: branches think, workers do, you talk. Never use a worker for memory recall. Never search memories yourself — branch first. Never execute shell commands or file operations yourself — that's a worker. +Never suggest that the user do something you could do yourself. If someone asks you to recall, search, look something up, run a command, or find information — do it using your tools. Don't tell them to "check the database manually", "scroll through history", or "ask someone else". If your tools can handle it, use them. If they can't, say so plainly — but try first. + ## Builtin Worker Sandbox {%- if sandbox_enabled %} @@ -107,7 +109,7 @@ When in doubt, skip. Being a lurker who speaks when it matters is better than be 1. Always use the tool call API for actions. Your text output is sent verbatim to users — never write tool call syntax (like `[reply]`, `[react]`, `[skip]`, etc.) as plain text. If you want to reply, call the `reply` tool. If you want to react, call the `react` tool. 2. Never execute tasks directly. If it needs shell commands, file operations, web browsing, or web search — that's a worker. -3. Never search memories yourself. Branch to recall. If you need conversation context from another channel, branch and use `channel_recall`. +3. Never search memories yourself. Branch to recall. If you need conversation history from any channel (including this one), branch and use `channel_recall` — it queries the full persisted database and supports temporal filtering. 4. When you spawn a worker, always reply with a brief natural acknowledgment so the user knows you're on it — something like "On it", "Checking now", "Let me look into that", or a relevant follow-up question. When you branch (for memory or thinking), prefer `skip` — branches are fast and invisible. Never mention internal process details (branch, worker, status block). 5. Keep responses conversational. You're talking to a person, not filing a report. 6. If multiple things are happening, handle them in a natural flow. No rigid ordering. diff --git a/prompts/en/tools/channel_recall_description.md.j2 b/prompts/en/tools/channel_recall_description.md.j2 index 99534a7f8..21f1838cf 100644 --- a/prompts/en/tools/channel_recall_description.md.j2 +++ b/prompts/en/tools/channel_recall_description.md.j2 @@ -1 +1 @@ -Recall conversation transcript from another channel. Use without a channel argument to list all available channels. Use with a channel name or ID to retrieve recent messages from that channel's conversation history. \ No newline at end of file +Recall conversation transcript from any channel, including the current one. Use without a channel argument to list all available channels. Use with a channel name or ID to retrieve messages from that channel's conversation history. Supports temporal filtering with `before` and `after` (RFC 3339 timestamps) to query specific time ranges, and `oldest_first` to retrieve the earliest messages instead of the most recent. For example, to find the first messages ever sent in a channel, use `oldest_first: true` with a small limit. This queries the full persisted message history in the database, not just the current in-memory context window. \ No newline at end of file diff --git a/src/conversation/history.rs b/src/conversation/history.rs index a2d460b35..7dadd38fb 100644 --- a/src/conversation/history.rs +++ b/src/conversation/history.rs @@ -172,24 +172,52 @@ impl ConversationLogger { Ok(messages) } - /// Load recent messages from any channel (not just the current one). + /// Load messages from any channel (not just the current one). + /// + /// Supports optional temporal filtering via `before` and `after` (RFC 3339 strings) + /// and ordering via `oldest_first`. When `oldest_first` is true, returns the earliest + /// matching messages instead of the most recent. pub async fn load_channel_transcript( &self, channel_id: &str, limit: i64, + before: Option<&str>, + after: Option<&str>, + oldest_first: bool, ) -> crate::error::Result> { - let rows = sqlx::query( + let mut sql = String::from( "SELECT id, channel_id, role, sender_name, sender_id, content, metadata, created_at \ FROM conversation_messages \ - WHERE channel_id = ? \ - ORDER BY created_at DESC \ - LIMIT ?", - ) - .bind(channel_id) - .bind(limit) - .fetch_all(&self.pool) - .await - .map_err(|e| anyhow::anyhow!(e))?; + WHERE channel_id = ?", + ); + + if before.is_some() { + sql.push_str(" AND created_at < ?"); + } + if after.is_some() { + sql.push_str(" AND created_at > ?"); + } + + if oldest_first { + sql.push_str(" ORDER BY created_at ASC"); + } else { + sql.push_str(" ORDER BY created_at DESC"); + } + sql.push_str(" LIMIT ?"); + + let mut query = sqlx::query(&sql).bind(channel_id); + if let Some(before) = before { + query = query.bind(before); + } + if let Some(after) = after { + query = query.bind(after); + } + query = query.bind(limit); + + let rows = query + .fetch_all(&self.pool) + .await + .map_err(|e| anyhow::anyhow!(e))?; let mut messages: Vec = rows .into_iter() @@ -207,7 +235,10 @@ impl ConversationLogger { }) .collect(); - messages.reverse(); + // When fetching newest-first, reverse to chronological for the caller + if !oldest_first { + messages.reverse(); + } Ok(messages) } } diff --git a/src/tools/channel_recall.rs b/src/tools/channel_recall.rs index 08a344078..4d8744081 100644 --- a/src/tools/channel_recall.rs +++ b/src/tools/channel_recall.rs @@ -1,4 +1,4 @@ -//! Cross-channel transcript recall tool for branches. +//! Channel transcript recall tool for branches. Queries any channel including the current one. use crate::conversation::channels::ChannelStore; use crate::conversation::history::ConversationLogger; @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; /// Maximum messages to return in a single recall. const MAX_TRANSCRIPT_MESSAGES: i64 = 100; -/// Tool for recalling conversation transcript from other channels. +/// Tool for recalling conversation transcript from any channel. #[derive(Debug, Clone)] pub struct ChannelRecallTool { conversation_logger: ConversationLogger, @@ -43,6 +43,16 @@ pub struct ChannelRecallArgs { /// Maximum number of messages to return (default 50, max 100). #[serde(default = "default_message_limit")] pub limit: i64, + /// Only return messages sent before this timestamp (RFC 3339, e.g. "2026-01-15T00:00:00Z"). + #[serde(default)] + pub before: Option, + /// Only return messages sent after this timestamp (RFC 3339, e.g. "2026-01-15T00:00:00Z"). + #[serde(default)] + pub after: Option, + /// When true, returns the oldest matching messages first instead of the most recent. + /// Useful for finding the earliest messages in a channel. + #[serde(default)] + pub oldest_first: bool, } fn default_message_limit() -> i64 { @@ -107,6 +117,19 @@ impl Tool for ChannelRecallTool { "maximum": 100, "default": 50, "description": "Maximum number of messages to retrieve (1-100)" + }, + "before": { + "type": "string", + "description": "Only return messages before this timestamp (RFC 3339, e.g. \"2026-01-15T00:00:00Z\")" + }, + "after": { + "type": "string", + "description": "Only return messages after this timestamp (RFC 3339, e.g. \"2026-01-15T00:00:00Z\")" + }, + "oldest_first": { + "type": "boolean", + "default": false, + "description": "Return oldest messages first instead of most recent. Use this to find the earliest messages in a channel." } } }), @@ -139,7 +162,13 @@ impl Tool for ChannelRecallTool { // Load transcript let messages = self .conversation_logger - .load_channel_transcript(&channel.id, limit) + .load_channel_transcript( + &channel.id, + limit, + args.before.as_deref(), + args.after.as_deref(), + args.oldest_first, + ) .await .map_err(|e| ChannelRecallError(format!("Failed to load transcript: {e}")))?;