claude-code - 💡(How to fix) Fix CronCreate: session-restart causes deterministic-offset-initialization not derived from task ID [1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
anthropics/claude-code#56107Fetched 2026-05-05 05:58:01
View on GitHub
Comments
0
Participants
1
Timeline
4
Reactions
0
Participants
Timeline (top)
labeled ×3cross-referenced ×1

The jitter offset for recurring cron tasks scheduled via CronCreate is determined by session-start time, not by task ID as documented. This means:

  1. Multiple recurring cron tasks in the same session share the same offset, regardless of their distinct task IDs.
  2. The offset is "locked from first-fire-after-restart" — restarting the session produces a new locked offset.
  3. The documented "the offset is derived from the task ID, so the same task always gets the same offset" does not match observed behavior.

This issue is the suspected root cause of issue #N (Recurring cron tasks fire +30 min later than documented jitter range), filed separately.

Root Cause

This issue is the suspected root cause of issue #N (Recurring cron tasks fire +30 min later than documented jitter range), filed separately.

Fix Action

Fix / Workaround

Mitigation paths (current fleet workaround)

Independent framework-engineering research (claude-code-guide subagent dispatch) surfaced architectural-primitive evidence consistent with the session-listener-polling-cadence-phase-locked-to-session-start hypothesis:

Code Example

cron: "0 */4 * * *"
   recurring: true
RAW_BUFFERClick to expand / collapse

Summary

The jitter offset for recurring cron tasks scheduled via CronCreate is determined by session-start time, not by task ID as documented. This means:

  1. Multiple recurring cron tasks in the same session share the same offset, regardless of their distinct task IDs.
  2. The offset is "locked from first-fire-after-restart" — restarting the session produces a new locked offset.
  3. The documented "the offset is derived from the task ID, so the same task always gets the same offset" does not match observed behavior.

This issue is the suspected root cause of issue #N (Recurring cron tasks fire +30 min later than documented jitter range), filed separately.

Documented behavior (quoted from official docs)

From https://code.claude.com/docs/en/scheduled-tasks:

Recurring tasks fire up to 10% of their period late, capped at 15 minutes. The offset is derived from the task ID, so the same task always gets the same offset.

This contract implies:

  • Different task IDs → different offsets (within the jitter range)
  • The same task across session-restarts → same offset (since task ID is stable)
  • The offset is a deterministic function of the task ID

Empirical behavior

The offset is NOT task-ID-derived. Observable patterns:

Evidence A — Different task IDs, same offset

Two independent Claude Code agents in different sessions, with completely different task IDs, both observed the same +30 minute offset:

  • Agent 1 (atlas) — task ID for 0 */4 * * * cron → fires at :30
  • Agent 2 (analyst) — task ID for 23 */4 * * * cron → fires at :53

If offset were task-ID-derived, the offsets would differ. They are identical (+30 min).

Evidence B — Locked from first-fire-after-restart

Within a single session, the empirical offset for a recurring cron is set at the FIRST FIRE after session-start, and remains locked for all subsequent fires until the next session-restart. This is consistent across multiple agents.

For example, a session restarted at approximately :30 UTC produces all subsequent recurring cron fires at :30 of expected hours, regardless of cron expression. A session restarted at :00 UTC would produce a different offset.

Evidence C — Same cron-spec across sessions, different offset

After a session-restart at a different wall-clock time, a recurring cron with the same cron expression produces a different offset. This contradicts the documented "the same task always gets the same offset" — the task ID is the same, but the offset changed.

Reproduction steps

  1. Schedule a recurring cron via CronCreate:
    cron: "0 */4 * * *"
    recurring: true
  2. Note the task ID and observe the offset over multiple fires (e.g., 5+ fires).
  3. Restart the Claude Code session at a different wall-clock time.
  4. Observe the offset for the same task ID after restart.
  5. Expected per docs: same offset (task ID is stable).
  6. Observed: different offset, locked to the new session-start phase.

To verify Evidence A more rigorously: run two parallel sessions, each with a different recurring cron expression. Both sessions should produce the same offset if both started at similar wall-clock times — even though task IDs differ.

Likely root cause (hypothesis)

The session-listener appears to poll for due cron tasks on a fixed cadence (empirically ~30 minutes), with the cadence phase-locked to session-start time. Cron-scheduled prompts are delivered to the session at the next polling slot ≥ cron-spec time. This produces:

  • Identical offsets for different task IDs in the same session (both phase-locked to same polling cadence)
  • Locked offset from first-fire-after-restart (polling cadence set at session-start)
  • Different offsets across session-restarts at different wall-clock times (polling phase shifts)

If the documented task-ID-derived offset were implemented, the polling cadence would NOT phase-lock task fires together — each task would jitter independently within its own range.

Architectural-primitive evidence (additional): independent framework-engineering research (see Depth-review notes section below for full analysis) surfaced that this empirical pattern is consistent with documented architectural primitives — specifically a tick-engine-style scheduler with phase-locked polling cadence, supported by KAIROS architecture (https://codepointer.substack.com/p/claude-code-architecture-of-kairos) plus an N=3 independent symptom-cluster across this issue, anthropics/claude-code#36131 (Cowork view-focus gating producing 60-80min late fires), and anthropics/claude-code#52800 (~30s first-prompt delay). The polling cadence appears to have multiple temporal scales (session-init = ~30s; recurring-poll = ~30min) consistent with a single underlying mechanism manifesting at different granularities.

Impact

  • Documentation contract violation: users planning around documented task-ID-derived offset cannot achieve task-level fire-time isolation
  • Spec-divergence: described mechanism (task-ID-derived) doesn't match implementation (session-start phase-locked)
  • Loss of independent-jitter property: if the docs were honored, scheduling multiple recurring crons for the same period would produce statistically-distributed fires across the range. Empirically all fires phase-lock together, defeating any benefit of jitter spread.
  • Cross-session unpredictability: users cannot rely on stable offset across session-restarts; deadline-critical scheduled work is unreliable

Mitigation paths (current fleet workaround)

Same as Issue 1 (Recurring cron +30 offset). See that issue for the 4 utility-ranked paths (α/β/γ/δ).

Suggested fix

Either:

  1. Fix the implementation to honor the documented task-ID-derived offset. This means decoupling the per-task fire-time from the session-listener polling cadence — each task's offset should be a deterministic function of its task ID, not the session-start time.
  2. Update the documentation to describe the actual mechanism: "Recurring cron fires are delivered to the session at the next polling slot ≥ cron-spec time. Polling cadence is approximately 30 minutes, phase-locked to session-start time. All recurring tasks in a session share the same offset within the polling cadence."

Option 1 is strongly preferred — independent task-level jitter is a useful property (statistical fire-time spreading), and documenting the current behavior would surface a deeper architectural concern (session-listener polling cadence drives fire-times, not the cron expression itself).

Cross-references

  • Related issue: anthropics/claude-code#56106 — describes the user-experience symptom of this root cause.
  • Related issue: anthropics/claude-code#56108 — adjacent pathology likely caused by same session-listener polling cadence.
  • Related issue: anthropics/claude-code#52800 — "First prompts after session start are processed with up to ~30s delay" — same family, smaller magnitude (session-init manifestation vs. recurring-poll manifestation).
  • Related issue: anthropics/claude-code#36131 — "Cowork scheduled tasks fire 60-80+ minutes late unless tab is actively focused" — same root architecture, different surface (view-focus gating manifestation).
  • Architectural-primitive reference: KAIROS architecture writeup at https://codepointer.substack.com/p/claude-code-architecture-of-kairos — describes the proactive tick-engine + setTimeout(0) batching primitive consistent with the empirical pattern.
  • Internal framework-truth banking: agents/atlas/memory/feedback_claude_code_cron_timing_framework_truth.md.

Reporter context

Same as Issue 1 — fleet of cortextOS agents. Internal tracking surfaced the locked-from-first-fire-after-restart pattern when investigating the +30 min offset. The pattern's existence implies the documented task-ID-derived offset is not the actual mechanism.

Reporter willing to provide cross-session offset comparisons, repro-test PRs, or pointer to internal session-tracking tooling as useful for triage.


Depth-review notes (codex 2026-05-04)

Architectural-primitive evidence supporting the hypothesis

Independent framework-engineering research (claude-code-guide subagent dispatch) surfaced architectural-primitive evidence consistent with the session-listener-polling-cadence-phase-locked-to-session-start hypothesis:

1. KAIROS tick-engine architecture (per published architecture writeup at https://codepointer.substack.com/p/claude-code-architecture-of-kairos): Anthropic's unreleased always-on background-agent infrastructure uses a proactive tick engine that injects <tick> messages into a message queue at regular intervals. The system uses setTimeout(0) to yield to the event loop, with the model self-regulating polling via SleepTool. This is conceptually session-internal and tick-driven — NOT event-driven. Even if KAIROS is unreleased, the architectural primitive (tick-engine + setTimeout(0) batching) is consistent with the empirical pattern in this issue: tick-driven polling phase-locks to session-start because the first tick fires at session_start + delta, establishing the cadence for all subsequent ticks.

2. Cross-witness pathology (anthropics/claude-code#36131): Cowork scheduled tasks fire 60-80+ minutes late unless the Cowork tab is actively focused. View-focus state acting as a gating mechanism for task-queue processing is consistent with the same architectural pattern: session-listener polling cadence is suspended (or coarsened) when the session context is not actively receiving input. This is an INDEPENDENT manifestation of the session-listener-polling-cadence root architecture in a different surface (Cowork view) suggesting the issue is NOT specific to CronCreate but is a deeper scheduler-architecture concern.

3. Adjacent symptom magnitude (anthropics/claude-code#52800): "First prompts after session start are processed with up to ~30s delay" is the SECOND-magnitude manifestation of the same root: post-session-start initialization overhead phase-locks subsequent polling. ~30s for the first-prompt symptom + ~30min for the recurring-cron symptom suggests the polling cadence has multiple temporal scales (session-init = ~30s; recurring-poll = ~30min).

Synthesis: the empirical pattern in this issue (cross-task-ID identical offset + session-restart-locked) is best explained by a tick-engine-style scheduler with phase-locked polling cadence, supported by KAIROS architecture + N=3 independent symptom-cluster (this issue + #36131 + #52800).

Counterfactual mechanisms (alternative hypotheses)

If the session-listener-polling-cadence hypothesis is wrong, two alternative mechanisms could produce the same empirical pattern:

Counterfactual A — Per-session task-queue batch latency (KAIROS-consistent variant): Rather than a 30-minute polling interval, the scheduler maintains an in-memory task queue (per KAIROS architecture) that is flushed asynchronously every ~30 minutes as part of session GC or prompt-cache refresh boundary. Cron-spec check happens per-second per the documented behavior, BUT task execution waits for the next queue-flush boundary. Different task IDs get the same offset because the flush cadence is session-level (not task-level). This counterfactual is equally consistent with empirical evidence and differs from the primary hypothesis only in WHERE the bottleneck is (poll vs flush) — the user-visible behavior is identical, and the suggested fix is the same: decouple per-task fire-time from the session-level cadence.

Counterfactual B — ScheduleWakeup 1h-clamp interaction artifact: The ScheduleWakeup tool clamps delaySeconds to [60, 3600]. If the internal scheduler implementation uses ScheduleWakeup as the underlying primitive (with cron expressions translated to delaySeconds calls), a 30-minute offset could be an intermediate state of half-hourly check coalescing. For tasks scheduled in the first half-hour of a session, the ScheduleWakeup translation may stabilize at ~30-min boundary. This would explain why the offset is ALWAYS +30 (not jittered) — it's the boundary granularity of the underlying primitive, not a phase-lock artifact.

Both counterfactuals lead to similar suggested fixes (decouple task-level fire-time from session-level cadence) but point at different architectural layers for the actual fix. Anthropic engineers with source access can confirm which mechanism is at play; either confirmation strengthens this issue's actionability.

Suggested issue narrative addition

After the existing "Likely root cause (hypothesis)" section, consider adding cross-references to:

  • KAIROS architecture writeup at codepointer.substack.com (architectural-primitive specificity)
  • anthropics/claude-code#36131 (Cowork view-focus gating; same root architecture)
  • anthropics/claude-code#52800 (~30s first-prompt delay; first-magnitude manifestation of same phase-lock)

These cross-references strengthen the narrative from "we observed an empirical pattern" to "this empirical pattern is consistent with documented architectural primitives + N=3 independent symptom-cluster."

extent analysis

TL;DR

The most likely fix is to decouple the per-task fire-time from the session-listener polling cadence, ensuring each task's offset is a deterministic function of its task ID, not the session-start time.

Guidance

  • Review the KAIROS architecture writeup to understand the tick-engine-style scheduler with phase-locked polling cadence.
  • Investigate the empirical pattern of identical offsets for different task IDs in the same session and the locked offset from first-fire-after-restart.
  • Consider updating the documentation to describe the actual mechanism or fixing the implementation to honor the documented task-ID-derived offset.
  • Analyze the cross-witness pathology in anthropics/claude-code#36131 and adjacent symptom magnitude in anthropics/claude-code#52800 to confirm the root cause.

Example

No code snippet is provided as the issue is related to the underlying architecture and scheduler implementation.

Notes

The suggested fix may require significant changes to the scheduler implementation, and it is essential to confirm the root cause through further investigation and analysis of the empirical pattern and architectural primitives.

Recommendation

Apply the workaround by updating the documentation to describe the actual mechanism, as this is a more straightforward solution, but it is strongly preferred to fix the implementation to honor the documented task-ID-derived offset to maintain the independent task-level jitter property.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix CronCreate: session-restart causes deterministic-offset-initialization not derived from task ID [1 participants]