Skip to content

feat: temporal filtering for channel_recall + self-channel recall#284

Merged
jamiepine merged 3 commits into
mainfrom
feat/channel-recall-temporal
Mar 4, 2026
Merged

feat: temporal filtering for channel_recall + self-channel recall#284
jamiepine merged 3 commits into
mainfrom
feat/channel-recall-temporal

Conversation

@jamiepine

Copy link
Copy Markdown
Member

Summary

  • Add before, after (RFC 3339 timestamps), and oldest_first params to channel_recall so the bot can query specific time ranges and find earliest messages
  • Fix all prompts, tool descriptions, and docs that said "another channel" / "cross-channel" — channel_recall works on any channel including the current one, and the bot didn't know that
  • Add anti-deflection rule to channel prompt — the bot was suggesting users "check the database manually" or "ask Jamie" instead of using its own tools

Triggered by Kegan asking the bot to recall its first message ever. The bot couldn't do it (no temporal params, only fetches N most recent) and then deflected to manual workarounds instead of saying it couldn't.

Changes

src/tools/channel_recall.rs — new before, after, oldest_first args + updated JSON schema

src/conversation/history.rs — dynamic SQL with optional temporal WHERE clauses and ASC/DESC ordering

prompts/en/tools/channel_recall_description.md.j2 — documents temporal filtering, notes it queries the full persisted DB

prompts/en/channel.md.j2 — "another channel" → "any channel (including this one)", anti-deflection rule

AGENTS.md — module map description fix

…ecall

channel_recall only supported fetching the N most recent messages with no
time-based filtering, and all docs/prompts described it as cross-channel
only. This meant the bot couldn't answer questions like 'find my first
message' and didn't know it could query the current channel's history.

- Add before, after (RFC 3339), and oldest_first params to channel_recall
- Update SQL in load_channel_transcript to support dynamic WHERE/ORDER
- Fix all prompts and docs that said 'another channel' to say 'any channel'
- Add anti-deflection rule to channel prompt so the bot uses its tools
  instead of suggesting users check things manually
@coderabbitai

coderabbitai Bot commented Mar 2, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds temporal filtering (before/after RFC3339) and an oldest_first option to channel_recall and the transcript loader, expands channel_recall to query any channel (including the current one), updates related docs, and introduces multiple new public tool modules under src/tools (exec, browser, cron, task_* variants, spacebot_docs, config_inspect).

Changes

Cohort / File(s) Summary
Channel recall & history
src/tools/channel_recall.rs, src/conversation/history.rs
Adds before: Option<String>, after: Option<String>, and oldest_first: bool to ChannelRecallArgs; updates load_channel_transcript signature and SQL building to support temporal bounds and configurable ordering.
Documentation / prompts
prompts/en/channel.md.j2, prompts/en/tools/channel_recall_description.md.j2, AGENTS.md
Updates channel_recall description to state cross-channel capability, full persisted-history queries, RFC3339 temporal filters, and adds guideline about preferring tools/plain statements; expands module map in AGENTS.md.
New public tool modules
src/tools/exec.rs, src/tools/browser.rs, src/tools/cron.rs, src/tools/task_create.rs, src/tools/task_list.rs, src/tools/task_update.rs, src/tools/spacebot_docs.rs, src/tools/config_inspect.rs
Adds multiple new tool modules to the public API surface (exec, browser, cron, task_create, task_list, task_update, spacebot_docs, config_inspect).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: temporal filtering for channel_recall + self-channel recall' accurately summarizes the main changes: adding temporal filtering parameters and enabling channel_recall to work on the current channel.
Description check ✅ Passed The description clearly explains the motivation (user asked bot to recall first message), lists specific changes (temporal params, anti-deflection rule), and references modified files that align with 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 feat/channel-recall-temporal

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.

@jamiepine jamiepine marked this pull request as ready for review March 2, 2026 07:31
}
if after.is_some() {
sql.push_str(" AND created_at > ?");
}

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.

Minor correctness gotcha: created_at is coming from SQLite CURRENT_TIMESTAMP (YYYY-MM-DD HH:MM:SS), so comparing it directly to RFC3339 input via < ?/> ? can behave oddly (esp. same-day comparisons because of the space vs T). Using datetime(?) keeps RFC3339 working without changing storage format.

Suggested change
}
if before.is_some() {
sql.push_str(" AND created_at < datetime(?)");
}
if after.is_some() {
sql.push_str(" AND created_at > datetime(?)");
}

Comment on lines 162 to +171
// 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,
)

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.

Might be worth validating before/after up-front. As-is, an invalid RFC3339 string will just turn into "no matches" at the SQL layer, which is hard to debug from the agent side.

Suggested change
// 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,
)
if let Some(before) = args.before.as_deref() {
chrono::DateTime::parse_from_rfc3339(before)
.map_err(|e| ChannelRecallError(format!("Invalid `before` timestamp: {e}")))?;
}
if let Some(after) = args.after.as_deref() {
chrono::DateTime::parse_from_rfc3339(after)
.map_err(|e| ChannelRecallError(format!("Invalid `after` timestamp: {e}")))?;
}
// Load transcript
let messages = self
.conversation_logger
.load_channel_transcript(
&channel.id,
limit,
args.before.as_deref(),
args.after.as_deref(),
args.oldest_first,
)

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/conversation/history.rs`:
- Around line 194-199: The temporal filters use raw string comparison (" AND
created_at < ?" / " AND created_at > ?") which misorders RFC3339 vs
space-separated datetimes; update the sql.append calls to normalize datetimes
using SQLite's datetime() like the existing pattern at lines ~413–415: replace
the two sql.push_str invocations that append " AND created_at < ?" and " AND
created_at > ?" with " AND datetime(created_at) < datetime(?)" and " AND
datetime(created_at) > datetime(?)" respectively so created_at and the bound
parameter are compared as normalized datetimes (keep the same bound parameter
variables before/after).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ebd1e1 and f9e32ce.

📒 Files selected for processing (5)
  • AGENTS.md
  • prompts/en/channel.md.j2
  • prompts/en/tools/channel_recall_description.md.j2
  • src/conversation/history.rs
  • src/tools/channel_recall.rs

Comment on lines +194 to +199
if before.is_some() {
sql.push_str(" AND created_at < ?");
}
if after.is_some() {
sql.push_str(" AND created_at > ?");
}

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and get line count
wc -l src/conversation/history.rs

Repository: spacedriveapp/spacebot

Length of output: 97


🏁 Script executed:

# Read the relevant section (lines 180-220 for context)
sed -n '180,220p' src/conversation/history.rs

Repository: spacedriveapp/spacebot

Length of output: 1309


🏁 Script executed:

# Check the function signature and parameter documentation
sed -n '170,200p' src/conversation/history.rs

Repository: spacedriveapp/spacebot

Length of output: 1081


🏁 Script executed:

# Look for the referenced timeline query at lines 413-415
sed -n '410,420p' src/conversation/history.rs

Repository: spacedriveapp/spacebot

Length of output: 377


🏁 Script executed:

# Search for how before/after are documented or typed
rg -n "before|after" src/conversation/history.rs | head -20

Repository: spacedriveapp/spacebot

Length of output: 810


🏁 Script executed:

# Run the verification script to demonstrate SQLite comparison behavior
python3 << 'PY'
import sqlite3

conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("CREATE TABLE t(created_at TEXT)")
cur.executemany("INSERT INTO t(created_at) VALUES (?)", [
    ("2026-01-15 00:30:00",),
    ("2026-01-15 23:59:59",),
])

bound = "2026-01-15T00:00:00Z"

print(f"Testing comparison with bound value: {bound}")
print()

plain = cur.execute(
    "SELECT created_at FROM t WHERE created_at < ? ORDER BY created_at",
    (bound,)
).fetchall()

normalized = cur.execute(
    "SELECT created_at FROM t WHERE datetime(created_at) < datetime(?) ORDER BY created_at",
    (bound,)
).fetchall()

print("Plain text compare (created_at < ?):", plain)
print("datetime() compare (datetime(created_at) < datetime(?)):", normalized)
print()
print("Analysis:")
print(f"  'T' (ASCII {ord('T')}) vs ' ' (ASCII {ord(' ')}) in string comparison")
print(f"  2026-01-15T00:00:00Z > 2026-01-15 (as strings): {bound > '2026-01-15'}")
PY

Repository: spacedriveapp/spacebot

Length of output: 396


🏁 Script executed:

# Verify the table schema to confirm created_at is stored as TEXT
rg -n "conversation_messages|CREATE TABLE" src/conversation/history.rs | head -10

Repository: spacedriveapp/spacebot

Length of output: 553


🏁 Script executed:

# Check if there are other places where before/after are used to see the broader pattern
rg -n "load_channel_transcript|RFC 3339" src/conversation/history.rs

Repository: spacedriveapp/spacebot

Length of output: 204


Use SQLite datetime normalization in temporal filters.

Lines 194-195 and 198 compare created_at to raw RFC 3339 string parameters without datetime normalization. String comparison of "2026-01-15T00:00:00Z" (where 'T' = ASCII 84) with "2026-01-15 HH:MM:SS" (where ' ' = ASCII 32) produces inverted filter results: rows are included when they should be excluded.

🛠️ Proposed fix
         if before.is_some() {
-            sql.push_str(" AND created_at < ?");
+            sql.push_str(" AND datetime(created_at) < datetime(?)");
         }
         if after.is_some() {
-            sql.push_str(" AND created_at > ?");
+            sql.push_str(" AND datetime(created_at) > datetime(?)");
         }

A reference implementation with the correct pattern already exists in the same file (lines 413–415).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if before.is_some() {
sql.push_str(" AND created_at < ?");
}
if after.is_some() {
sql.push_str(" AND created_at > ?");
}
if before.is_some() {
sql.push_str(" AND datetime(created_at) < datetime(?)");
}
if after.is_some() {
sql.push_str(" AND datetime(created_at) > datetime(?)");
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/conversation/history.rs` around lines 194 - 199, The temporal filters use
raw string comparison (" AND created_at < ?" / " AND created_at > ?") which
misorders RFC3339 vs space-separated datetimes; update the sql.append calls to
normalize datetimes using SQLite's datetime() like the existing pattern at lines
~413–415: replace the two sql.push_str invocations that append " AND created_at
< ?" and " AND created_at > ?" with " AND datetime(created_at) < datetime(?)"
and " AND datetime(created_at) > datetime(?)" respectively so created_at and the
bound parameter are compared as normalized datetimes (keep the same bound
parameter variables before/after).

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

♻️ Duplicate comments (1)
src/conversation/history.rs (1)

194-199: ⚠️ Potential issue | 🟠 Major

Use SQLite datetime() for temporal comparisons.

Raw string comparison between RFC 3339 input (2026-01-15T00:00:00Z) and SQLite's CURRENT_TIMESTAMP format (2026-01-15 00:30:00) produces incorrect results due to ASCII ordering ('T' > ' '). The same file already uses the correct pattern at lines 469-472 in load_channel_timeline.

🐛 Proposed fix
         if before.is_some() {
-            sql.push_str(" AND created_at < ?");
+            sql.push_str(" AND datetime(created_at) < datetime(?)");
         }
         if after.is_some() {
-            sql.push_str(" AND created_at > ?");
+            sql.push_str(" AND datetime(created_at) > datetime(?)");
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/conversation/history.rs` around lines 194 - 199, The SQL temporal
comparisons currently use raw string comparisons for created_at with
placeholders (the if before.is_some()/if after.is_some() blocks), which yields
incorrect ordering for RFC3339 strings; update those conditions to use SQLite's
datetime() on both sides — e.g., change " AND created_at < ?" to " AND
datetime(created_at) < datetime(?)" and similarly for the after branch —
matching the pattern used in load_channel_timeline; keep the same parameter
placeholders and types so callers need not change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/conversation/history.rs`:
- Around line 194-199: The SQL temporal comparisons currently use raw string
comparisons for created_at with placeholders (the if before.is_some()/if
after.is_some() blocks), which yields incorrect ordering for RFC3339 strings;
update those conditions to use SQLite's datetime() on both sides — e.g., change
" AND created_at < ?" to " AND datetime(created_at) < datetime(?)" and similarly
for the after branch — matching the pattern used in load_channel_timeline; keep
the same parameter placeholders and types so callers need not change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 53d78588-db18-49af-9b39-e0246a3f5dd3

📥 Commits

Reviewing files that changed from the base of the PR and between f9e32ce and c1bf283.

📒 Files selected for processing (3)
  • AGENTS.md
  • prompts/en/channel.md.j2
  • src/conversation/history.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • prompts/en/channel.md.j2

@jamiepine jamiepine enabled auto-merge March 4, 2026 23:27
@jamiepine jamiepine disabled auto-merge March 4, 2026 23:27
@jamiepine jamiepine merged commit 0ba60d0 into main Mar 4, 2026
4 checks passed
rktmeister pushed a commit to rktmeister/spacebot that referenced this pull request Mar 11, 2026
…recall-temporal

feat: temporal filtering for channel_recall + self-channel recall
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