Skip to content

fix(mcp): preserve trigger_at offset across schedule repeat advancement#959

Merged
ohdearquant merged 7 commits into
mainfrom
fix-792-repeat-tz
Jul 15, 2026
Merged

fix(mcp): preserve trigger_at offset across schedule repeat advancement#959
ohdearquant merged 7 commits into
mainfrom
fix-792-repeat-tz

Conversation

@ohdearquant

Copy link
Copy Markdown
Owner

Fixes #792. Root cause in the commit message. Regression test with a non-UTC offset included. Local gates deferred to CI due to build-lane contention; codex review follows.

Leo and others added 7 commits July 13, 2026 12:29
next_trigger_at parsed trigger_at via parse::<DateTime<Utc>>, normalizing
to UTC and discarding the original offset; advancement then re-serialized
with to_rfc3339 on the Utc value, rendering +00:00 regardless of the
caller's offset. Capture the original offset and re-apply it when
rendering the advanced timestamp, on both the normal-fire advance path
and the missed-event re-arm path.

Fixes #792

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rain

The repeat drain parsed trigger_at with strict DateTime::parse_from_rfc3339,
but the schedule write boundary (khive-pack-schedule) validates with the
relaxed at.parse::<DateTime<Utc>>() grammar and stores the original string
verbatim. Already-persisted relaxed timestamps (space instead of T, offset
without a colon) were silently skipped in the drain and never fired.

Parse with trigger_at_str.parse::<DateTime<FixedOffset>>() instead — same
grammar family as the write boundary — keeping the existing
with_timezone(&Utc) and offset() handling. Adds a drain-level regression
test using a relaxed legacy timestamp with a non-UTC offset.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ky tx-age sweep

run_writer_task registers a writer_task_tx handle in the process-wide
tx_registry singleton; the checkpoint tx_age_sweep_* tests read that
singleton under #[serial(tx_registry)]. The writer_task.rs tests that
spawn a writer task were missing from the group, leaking a longer-lived
writer_task_tx into the sweep's oldest() read and intermittently failing
the assertion on main CI. Join them to the serial group. No prod change.
@ohdearquant
ohdearquant marked this pull request as ready for review July 14, 2026 12:29
@ohdearquant

Copy link
Copy Markdown
Owner Author

Codex static review: APPROVE at ecfaf11 (0 blocking findings).

@ohdearquant
ohdearquant enabled auto-merge (squash) July 14, 2026 12:29

@oceanwaves630 oceanwaves630 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent full-diff read at head ecfaf11. Approving.

The offset-preservation fix (pending_events.rs) is correct and complete:

  • It now parses trigger_at as DateTime<FixedOffset> via the relaxed FromStr grammar — the same grammar the write boundary validates with — rather than the strict DateTime::parse_from_rfc3339. That choice matters: a legacy stored timestamp using a space separator and a colonless offset (... 09:00:00+0400) still parses and advances instead of being silently skipped forever as unparseable. The UTC instant used for every due-ness comparison and repeat computation is then derived via with_timezone(&Utc), so the comparison/arithmetic semantics are identical to the previous parse::<DateTime<Utc>>(); only the serialization of the advanced occurrence changes.

  • It captures the original offset once and renders both advancement sites at that offset. I grepped the full production body to confirm those two sites (the missed-path re-arm and the normal fire-and-advance) are the complete set of advanced-trigger_at serialization points — there is no third site left rendering bare UTC. fired_at correctly stays UTC (it is an event timestamp, not a schedule offset to preserve), and the SQL due-ness predicates normalize through datetime(), so they remain offset-agnostic.

  • Three targeted tests assert the fix across the paths that matter: a daily advance preserves the +04:00 offset and the same local wall-clock hour while adding exactly one day of instant; a relaxed legacy-grammar (space + +0400) timestamp is recognized as due and advanced (not skipped) with its offset preserved; and the missed-repeat re-arm path preserves the offset too. The unparseable fall-through still continues exactly as before, so the fail-safe is unchanged.

The remaining files are test-hardening (serial-tagging the tx_registry race, a --no-embed backend-routing fixture, and a behavior-preserving runtime-setup test seam that delegates via a no-op closure) with no new production behavior. CI is green.

Surgical, complete, well-tested. Approving.

@ohdearquant
ohdearquant merged commit cd6741f into main Jul 15, 2026
23 checks passed
@ohdearquant
ohdearquant deleted the fix-792-repeat-tz branch July 15, 2026 20:46
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.

schedule executor: repeat advancement drops original trigger_at timezone offset

2 participants