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

  1. Re-add the job with a fresh future ScheduledAt via AddJob if it should still run.
  2. Check the timer store for the MarkFired record to confirm the skip and the exact overdue duration.
  3. 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.
  4. 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

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


AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06). Data as JSON: /api/errors/e2e22440f6e1d43d. Report an issue: GitHub.