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

  1. Swap the bounds so since is strictly before until.
  2. Widen until when a single-instant window was intended.
  3. 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

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


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/f3fcfeaca4e3e5ab. Report an issue: GitHub.