{"record":{"id":"e2e22440f6e1d43d","repo":"chenhg5/cc-connect","slug":"missed-by-v-stale","errorCode":null,"errorMessage":"missed by %v (stale)","messagePattern":"missed by (.+?) \\(stale\\)","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"core/timer.go","lineNumber":313,"sourceCode":"func (ts *TimerScheduler) Start() error {\n\tjobs := ts.store.List()\n\tnow := time.Now()\n\tvar scheduled, missed, skipped int\n\tfor _, job := range jobs {\n\t\tif job.Fired {\n\t\t\tcontinue\n\t\t}\n\t\tdelay := job.ScheduledAt.Sub(now)\n\t\tif delay <= 0 {\n\t\t\t// Past due\n\t\t\tif -delay <= missedJobGracePeriod {\n\t\t\t\t// Just missed — fire immediately\n\t\t\t\tslog.Info(\"timer: firing missed job immediately\", \"id\", job.ID, \"overdue\", -delay)\n\t\t\t\tts.scheduleAt(job, 0)\n\t\t\t\tmissed++\n\t\t\t} else {\n\t\t\t\tslog.Warn(\"timer: skipping stale job\", \"id\", job.ID, \"scheduled_at\", job.ScheduledAt, \"overdue\", -delay)\n\t\t\t\tts.store.MarkFired(job.ID, fmt.Errorf(\"missed by %v (stale)\", -delay))\n\t\t\t\tskipped++\n\t\t\t}\n\t\t} else {\n\t\t\tts.scheduleAt(job, delay)\n\t\t\tscheduled++\n\t\t}\n\t}\n\tslog.Info(\"timer: scheduler started\", \"scheduled\", scheduled, \"missed_fired\", missed, \"skipped_stale\", skipped, \"total\", len(jobs))\n\treturn nil\n}\n\nfunc (ts *TimerScheduler) Stop() {\n\tts.mu.Lock()\n\tdefer ts.mu.Unlock()\n\tfor id, t := range ts.timers {\n\t\tt.Stop()\n\t\tdelete(ts.timers, id)\n\t}","sourceCodeStart":295,"sourceCodeEnd":331,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/core/timer.go#L295-L331","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nsched.AddJob(&core.TimerJob{ID: \"nightly\", SessionKey: k, ScheduledAt: lastFriday, Prompt: p}) // stale after restart\n// after\nsched.AddJob(&core.TimerJob{ID: \"nightly\", SessionKey: k, ScheduledAt: time.Now().Add(12*time.Hour), Prompt: p})","handlingStrategy":"retry","validationCode":"if !job.ScheduledAt.After(time.Now()) { /* re-schedule or skip explicitly */ }","typeGuard":null,"tryCatchPattern":"// check the stored outcome after restart\nif rec, ok := store.Fired(job.ID); ok && strings.Contains(rec.Error, \"stale\") {\n    // job was skipped as stale; re-arm it\n    sched.AddJob(cloneWithNewTime(job, time.Now().Add(time.Minute)))\n}","preventionTips":["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"],"tags":["timer","scheduler","recovery"],"backgroundTag":"request-timeout","analyzedSha":"4000b2338aa6e850c99df54f8b0ed6ed7460b401","analyzedAt":"2026-09-06T11:45:09.575Z","contentChangedAt":"2026-09-06T11:45:09.575Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}