chenhg5/cc-connect · warning
missed by %v (stale)
Error message
missed by %v (stale)
What it means
When the TimerScheduler restarts (Start), it re-evaluates persisted jobs. A job whose ScheduledAt is so far in the past that it exceeds the recovery grace window is considered stale: it is not fired, and MarkFired records an error of the form "missed by <duration> (stale)" (the delay is negative; its magnitude is how long ago the job was due). This is a stored outcome, not a panic — jobs missed slightly are fired immediately, but very overdue ones are skipped.
Source
Thrown at core/timer.go:313
func (ts *TimerScheduler) Start() error {
jobs := ts.store.List()
now := time.Now()
var scheduled, missed, skipped int
for _, job := range jobs {
if job.Fired {
continue
}
delay := job.ScheduledAt.Sub(now)
if delay <= 0 {
// Past due
if -delay <= missedJobGracePeriod {
// Just missed — fire immediately
slog.Info("timer: firing missed job immediately", "id", job.ID, "overdue", -delay)
ts.scheduleAt(job, 0)
missed++
} else {
slog.Warn("timer: skipping stale job", "id", job.ID, "scheduled_at", job.ScheduledAt, "overdue", -delay)
ts.store.MarkFired(job.ID, fmt.Errorf("missed by %v (stale)", -delay))
skipped++
}
} else {
ts.scheduleAt(job, delay)
scheduled++
}
}
slog.Info("timer: scheduler started", "scheduled", scheduled, "missed_fired", missed, "skipped_stale", skipped, "total", len(jobs))
return nil
}
func (ts *TimerScheduler) Stop() {
ts.mu.Lock()
defer ts.mu.Unlock()
for id, t := range ts.timers {
t.Stop()
delete(ts.timers, id)
}View on GitHub (pinned to 4000b2338a)
Solutions
- Re-add the job with a fresh future ScheduledAt via AddJob if it should still run.
- Check the timer store for the MarkFired record to confirm the skip and the exact overdue duration.
- For periodic work, use a cron-style job or re-arm the job after each run instead of one-shot scheduling that can go stale.
- Shorten downtime or schedule with enough slack so brief restarts fall inside the immediate-fire window.
Example fix
// before
sched.AddJob(&core.TimerJob{ID: "nightly", SessionKey: k, ScheduledAt: lastFriday, Prompt: p}) // stale after restart
// after
sched.AddJob(&core.TimerJob{ID: "nightly", SessionKey: k, ScheduledAt: time.Now().Add(12*time.Hour), Prompt: p}) Defensive patterns
Strategy: retry
Validate before calling
if !job.ScheduledAt.After(time.Now()) { /* re-schedule or skip explicitly */ } Try / catch
// check the stored outcome after restart
if rec, ok := store.Fired(job.ID); ok && strings.Contains(rec.Error, "stale") {
// job was skipped as stale; re-arm it
sched.AddJob(cloneWithNewTime(job, time.Now().Add(time.Minute)))
} Prevention
- Re-arm recurring jobs after each run instead of one-shot far-future schedules
- On daemon startup, scan the timer store and re-schedule stale jobs proactively
- Avoid long downtime windows overlapping scheduled fire times
When it happens
Trigger: Scheduler downtime longer than the recovery threshold after a job's ScheduledAt passed — e.g. the machine was off/asleep for hours, the daemon crashed, or the persisted store contains old jobs from a previous session that were never cleaned up.
Common situations: Laptop resumed after a weekend with a timer from Friday; systemd service stopped during a deploy window that overlapped a scheduled job; testing with a fixed ScheduledAt in the past and expecting it to fire on Start.
Understand the failure class
Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.
Related errors
- session_key is required
- scheduled_at is required
- either prompt or exec is required
- project %q not found
- job timed out after %v
AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06).
Data as JSON: /api/errors/e2e22440f6e1d43d.
Report an issue: GitHub.