zeroclaw-labs/zeroclaw · error
'since' must be before 'until'
Error message
'since' must be before 'until'
What it means
The same since < until contract enforced on the markdown backend's recall: both bounds are parsed as RFC 3339 and the call fails when since >= until (equal instants included). It fires before any entries are read, so it costs nothing and protects the daily-file scan from inverted windows.
Source
Thrown at crates/zeroclaw-memory/src/markdown.rs:237
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 all = self.read_all_entries().await?;
let keywords: Vec<String> = if is_recent_recall_query(query) {
Vec::new()
} else {
query
.to_lowercase()
.split_whitespace()
.map(str::to_string)
.collect()
};
let mut scored: Vec<MemoryEntry> = all
.into_iter()
.filter_map(|mut entry| {
if !entry_in_window(&entry.timestamp, since_dt.as_ref(), until_dt.as_ref()) {
return None;View on GitHub (pinned to 88bb9c8533)
Solutions
- Swap the bounds so since is strictly before until.
- Widen until when a single-instant window was intended.
- Validate and normalize both bounds (UTC) in the caller before invoking recall.
Example fix
// before
let entries = markdown.recall(query, limit, None,
Some("2026-08-22T00:00:00Z"),
Some("2026-08-01T00:00:00Z"),
).await?;
// after
let entries = markdown.recall(query, limit, None,
Some("2026-08-01T00:00:00Z"),
Some("2026-08-22T00:00:00Z"),
).await?; Defensive patterns
Strategy: validation
Validate before calling
fn valid_recall_window(since: Option<&str>, until: Option<&str>) -> anyhow::Result<()> {
if let (Some(s), Some(u)) = (since, until)
&& let (Ok(s), Ok(u)) = (
chrono::DateTime::parse_from_rfc3339(s),
chrono::DateTime::parse_from_rfc3339(u),
)
&& s >= u
{
anyhow::bail!("since ({s}) must be strictly before until ({u})");
}
Ok(())
} Type guard
fn is_valid_recall_window(since: Option<&str>, until: Option<&str>) -> bool {
valid_recall_window(since, until).is_ok()
} Prevention
- Validate date ranges in UI layers before submission; date pickers should not allow a zero-length window.
- Log the parsed window bounds (both UTC instants) at the call site so inversions are obvious in traces.
When it happens
Trigger: Calling MarkdownMemory::recall (directly or via recall_for_agents) with since on or after until — same shapes as the lucid variant: swapped args, equal instants, offset-induced inversion.
Common situations: Date pickers allowing zero-length ranges; string-sorted date pairs passed unsorted; DST shifts flipping the order of same-local-time bounds; migration tooling passing through user-supplied dates unvalidated.
Related errors
- 'since' must be before 'until'
- candidate SOP has no parsed steps
- 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/f3fcfeaca4e3e5ab.
Report an issue: GitHub.