Skip to content

fix(cron): add delay_ms to avoid LLM computing absolute timestamps#377

Merged
penso merged 1 commit intomoltis-org:mainfrom
Devansh-bit:fix/cron-delay-ms
Mar 10, 2026
Merged

fix(cron): add delay_ms to avoid LLM computing absolute timestamps#377
penso merged 1 commit intomoltis-org:mainfrom
Devansh-bit:fix/cron-delay-ms

Conversation

@Devansh-bit
Copy link
Copy Markdown
Contributor

Summary

  • The at schedule kind requires at_ms as an absolute epoch milliseconds value
  • LLMs reliably miscalculate this due to training-data clock skew — jobs end up scheduled in the past and never fire
  • Add delay_ms (server-resolved relative offset from now) so the LLM only needs to express a duration like 600000 for 10 minutes

Changes

crates/tools/src/cron_tool.rs

  • In normalize_schedule_value, resolve delay_msat_ms using SystemTime::now() at call time before the schedule is stored
  • Accepts aliases: delayMs, delay, in, in_ms, offset_ms
  • Updated tool schema description to guide the LLM to use delay_ms instead of computing at_ms itself
  • at_ms is preserved and unchanged for callers that supply a real absolute timestamp

Validation

Completed

  • No secrets or private tokens
  • Rust fmt passes
  • Clippy passes
  • Conventional commit message

Remaining

  • cargo test (cron unit tests)
  • just release-preflight

Manual QA

  1. Ask the agent: "remind me in 10 seconds"
  2. Verify the cron job fires ~10 seconds later (previously it would be scheduled a year in the past and never fire)

The 'at' schedule kind previously required the LLM to supply at_ms as
an absolute epoch millisecond value. LLMs reliably miscalculate this
(training-data clock skew means 'now' is off by months or years),
causing one-shot jobs to be scheduled in the past and never fire.

Add delay_ms (aliases: delayMs, delay, in, in_ms, offset_ms) as a
server-resolved relative offset: the tool computes now_ms + delay_ms
at call time, so the LLM only needs to express durations (e.g. 600000
for 10 minutes) rather than absolute timestamps.

Existing at_ms is preserved for callers that have a real absolute time.
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 10, 2026

Greptile Summary

This PR introduces a delay_ms field to the cron-tool schedule normalizer so that LLM agents can express one-off schedules as a relative duration (e.g. 600000 for 10 minutes) rather than computing an absolute epoch timestamp — a task LLMs consistently get wrong due to training-data clock skew. The change is confined to normalize_schedule_value in crates/tools/src/cron_tool.rs and the JSON schema description exposed to the model.

Key changes:

  • normalize_schedule_value now detects and removes delay_ms (plus aliases delayMs, delay, in, in_ms, offset_ms), resolves it to an absolute at_ms using SystemTime::now(), and inserts kind:'at' if not already present.
  • The tool schema description is updated to guide LLMs toward delay_ms and away from at_ms.

Issues found:

  • now + delay (u64 + u64) can overflow if the caller supplies an extremely large delay_ms; a panic (debug) or silent timestamp wrap-around (release) would result — use saturating_add or checked_add.
  • When both at_ms and delay_ms are present, delay_ms is silently removed and ignored because or_insert does not overwrite the existing at_ms; this should either raise an error or let delay_ms take precedence.
  • The "in" alias is very generic and risks accidental coercion of unrelated JSON fields.

Confidence Score: 3/5

  • The feature direction is sound but two logic bugs (overflow and silent field discard) should be resolved before merging.
  • The change is small and well-motivated, but the now + delay overflow and the silent delay_ms-discard-when-at_ms-present bugs are genuine correctness issues that can reproduce the exact failure mode (job scheduled in the past, never fires) the PR is trying to fix.
  • crates/tools/src/cron_tool.rs — specifically the delay_ms resolution block (lines 145–156)

Important Files Changed

Filename Overview
crates/tools/src/cron_tool.rs Adds delay_ms resolution in normalize_schedule_value and updates the tool schema description; contains an integer-overflow risk in now + delay and a silent-discard bug when both delay_ms and at_ms are supplied.

Sequence Diagram

sequenceDiagram
    participant LLM as LLM Agent
    participant CronTool as CronTool::call()
    participant Normalize as normalize_schedule_value()
    participant SysTime as SystemTime::now()
    participant CronSvc as CronService

    LLM->>CronTool: {name, schedule: {kind:'at', delay_ms: 600000}}
    CronTool->>Normalize: &mut schedule value
    Normalize->>Normalize: take_alias → canonicalize field names
    Normalize->>Normalize: obj.remove("delay_ms") → delay = 600000
    Normalize->>SysTime: SystemTime::now()
    SysTime-->>Normalize: now_ms (u64)
    Normalize->>Normalize: at_ms = now_ms + delay (⚠️ potential overflow)
    Normalize->>Normalize: obj.entry("at_ms").or_insert(at_ms)
    Normalize->>Normalize: obj.entry("kind").or_insert("at")
    Normalize-->>CronTool: Ok(schedule = {kind:'at', at_ms: <absolute>})
    CronTool->>CronSvc: create_job(CronJobCreate{schedule})
    CronSvc-->>CronTool: CronJob
    CronTool-->>LLM: tool result (job created)
Loading

Last reviewed commit: c4c2642

Comment thread crates/tools/src/cron_tool.rs
Comment thread crates/tools/src/cron_tool.rs
Comment thread crates/tools/src/cron_tool.rs
@Devansh-bit
Copy link
Copy Markdown
Contributor Author

Addressed all three issues from the Greptile review:

  1. Overflow: Changed now + delaynow.saturating_add(delay) so an extreme delay_ms can't panic or wrap.

  2. Silent discard when both at_ms and delay_ms present: or_insert has been replaced with an explicit obj.contains_key("at_ms") check that returns an error — the conflict is now surfaced rather than silently ignored.

  3. Overly generic "in" alias: Removed "in" from the delay_ms alias list (kept in_ms, offset_ms, delayMs, delay).

Added three unit tests covering the new behaviour: delay_ms resolves to a future at_ms, the delayMs alias works, and supplying both at_ms and delay_ms is rejected with a clear error message.

@penso penso merged commit 00ad2d4 into moltis-org:main Mar 10, 2026
1 check passed
penso pushed a commit that referenced this pull request Mar 23, 2026
)

The 'at' schedule kind previously required the LLM to supply at_ms as
an absolute epoch millisecond value. LLMs reliably miscalculate this
(training-data clock skew means 'now' is off by months or years),
causing one-shot jobs to be scheduled in the past and never fire.

Add delay_ms (aliases: delayMs, delay, in, in_ms, offset_ms) as a
server-resolved relative offset: the tool computes now_ms + delay_ms
at call time, so the LLM only needs to express durations (e.g. 600000
for 10 minutes) rather than absolute timestamps.

Existing at_ms is preserved for callers that have a real absolute time.
jmikedupont2 pushed a commit to meta-introspector/moltis that referenced this pull request Mar 23, 2026
…oltis-org#377)

The 'at' schedule kind previously required the LLM to supply at_ms as
an absolute epoch millisecond value. LLMs reliably miscalculate this
(training-data clock skew means 'now' is off by months or years),
causing one-shot jobs to be scheduled in the past and never fire.

Add delay_ms (aliases: delayMs, delay, in, in_ms, offset_ms) as a
server-resolved relative offset: the tool computes now_ms + delay_ms
at call time, so the LLM only needs to express durations (e.g. 600000
for 10 minutes) rather than absolute timestamps.

Existing at_ms is preserved for callers that have a real absolute time.
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.

2 participants