zeroclaw-labs/zeroclaw · error
'since' must be before 'until'
Error message
'since' must be before 'until'
What it means
LucidMemory::recall parses the optional since/until RFC 3339 window filters and requires since < until strictly — the check uses s >= u, so equal instants are rejected too. It fires before the local sqlite lookup and any lucid subprocess, purely from argument validation, and exists so an inverted range never silently returns zero results.
Source
Thrown at crates/zeroclaw-memory/src/lucid.rs:448
let until_dt = until
.map(chrono::DateTime::parse_from_rfc3339)
.transpose()
.map_err(|e| {
::zeroclaw_log::record!(
WARN,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
.with_outcome(::zeroclaw_log::EventOutcome::Failure)
.with_attrs(
::serde_json::json!({"field": "until", "error": format!("{}", e)})
),
"recall window bound rejected"
);
anyhow::Error::msg(format!("invalid 'until' date (expected RFC 3339): {e}"))
})?;
if let (Some(s), Some(u)) = (&since_dt, &until_dt)
&& s >= u
{
anyhow::bail!("'since' must be before 'until'");
}
let recall_query = normalize_recent_recall_query(query);
let local_results = self
.local
.recall(recall_query, limit, session_id, since, until)
.await?;
if limit == 0
|| local_results.len() >= limit
|| local_results.len() >= self.local_hit_threshold
{
return Ok(local_results);
}
if self.in_failure_cooldown() {
return Ok(local_results);
}View on GitHub (pinned to 88bb9c8533)
Solutions
- Order the arguments so since is strictly earlier than until.
- If the intent is a single instant, widen until by at least one second (or one unit of your choice).
- Normalize both timestamps to UTC before sending so offsets cannot invert the comparison.
Example fix
// before
memory.recall(query, limit, session_id,
Some("2026-01-10T00:00:00Z"), // since (later)
Some("2026-01-02T00:00:00Z"), // until (earlier)
).await?;
// after
memory.recall(query, limit, session_id,
Some("2026-01-02T00:00:00Z"), // since (earlier)
Some("2026-01-10T00:00:00Z"), // until (later)
).await?; Defensive patterns
Strategy: validation
Validate before calling
fn valid_recall_window(since: Option<&str>, until: Option<&str>) -> bool {
match (since, until) {
(Some(s), Some(u)) => match (
chrono::DateTime::parse_from_rfc3339(s),
chrono::DateTime::parse_from_rfc3339(u),
) {
(Ok(s), Ok(u)) => s < u, // strict: equal instants are rejected
_ => false,
},
_ => true,
}
} Type guard
fn is_valid_recall_window(since: Option<&str>, until: Option<&str>) -> bool {
valid_recall_window(since, until)
} Try / catch
if !valid_recall_window(since, until) {
return Ok(Vec::new()); // or surface a 4xx to the caller
}
memory.recall(query, limit, session_id, since, until).await; Prevention
- Validate the window at the API boundary (before it reaches memory) and reject inverted ranges with a client-facing message.
- Render since/until from typed DateTime values serialized to UTC RFC 3339 instead of passing through user strings.
- Treat equal since/until as invalid in callers — the backend enforces strict inequality.
When it happens
Trigger: Calling recall (or a higher path reaching it) with both filters where since is on or after until, e.g. since="2026-01-10T00:00:00Z", until="2026-01-02T00:00:00Z", or both set to the same instant.
Common situations: Swapped since/until arguments; inclusive single-instant ranges where both ends are equal; timezone offsets (+02:00 vs Z) making the since instant land after until; UI date pickers sending default zero or identical dates.
Related errors
- 'since' must be before 'until'
- lucid command timed out after {}ms
- modal custom_id exceeds Discord's 100-char limit; cannot ope
- slash command registration failed for '{name}' ({status}): {
- unknown git channel provider `{other}` (supported: github, g
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/894aef3c59ced5b5.
Report an issue: GitHub.