{"record":{"id":"6ad6d220e136fd99","repo":"chenhg5/cc-connect","slug":"timeout-mins-must-be-0-6ad6d2","errorCode":null,"errorMessage":"timeout_mins must be >= 0","messagePattern":"timeout_mins must be >= 0","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/timer.go","lineNumber":85,"sourceCode":"\tif j.Prompt == \"\" && j.Exec == \"\" {\n\t\treturn fmt.Errorf(\"either prompt or exec is required\")\n\t}\n\tif j.Prompt != \"\" && j.Exec != \"\" {\n\t\treturn fmt.Errorf(\"prompt and exec are mutually exclusive\")\n\t}\n\tmode := NormalizeCronSessionMode(j.SessionMode)\n\tif mode != \"\" && mode != \"new_per_run\" {\n\t\treturn fmt.Errorf(\"invalid session_mode %q (want reuse, new_per_run, or new-per-run)\", j.SessionMode)\n\t}\n\tif j.Mode != \"\" {\n\t\tswitch j.Mode {\n\t\tcase \"default\", \"bypassPermissions\", \"acceptEdits\", \"plan\", \"auto\", \"dontAsk\":\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"invalid mode %q\", j.Mode)\n\t\t}\n\t}\n\tif j.TimeoutMins != nil && *j.TimeoutMins < 0 {\n\t\treturn fmt.Errorf(\"timeout_mins must be >= 0\")\n\t}\n\treturn nil\n}\n\n// TimerStore persists timer jobs to a JSON file.\ntype TimerStore struct {\n\tpath string\n\tmu   sync.Mutex\n\tjobs []*TimerJob\n}\n\nfunc NewTimerStore(dataDir string) (*TimerStore, error) {\n\tdir := filepath.Join(dataDir, \"timers\")\n\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\treturn nil, err\n\t}\n\tpath := filepath.Join(dir, \"jobs.json\")\n\ts := &TimerStore{path: path}","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/core/timer.go#L67-L103","documentation":"validateTimerJob rejects a TimerJob whose optional TimeoutMins pointer is non-nil but negative. A negative timeout is nonsensical for job execution; zero (nil or 0) means no timeout, positive values cap the run.","triggerScenarios":"AddJob with a TimerJob whose TimeoutMins points to a negative int (e.g. int32/int pointer to -1 used as a 'default' sentinel, or arithmetic that subtracted past zero).","commonSituations":"Using -1 as a sentinel for 'unset' instead of leaving the pointer nil; config values like timeout_mins = -5; computing a remaining-time budget that underflowed.","solutions":["Use 0 for 'no timeout' and nil for 'unset'; never a negative number.","Fix the sentinel: replace *TimeoutMins = -1 with TimeoutMins = nil before AddJob.","Clamp loaded config values: if v < 0 { v = 0 } when parsing timeout_mins.","Check the caller-side computation that produced the negative duration."],"exampleFix":"// before\nto := -1\njob := &core.TimerJob{SessionKey: k, ScheduledAt: when, Prompt: p, TimeoutMins: &to}\nsched.AddJob(job)\n// after\njob := &core.TimerJob{SessionKey: k, ScheduledAt: when, Prompt: p, TimeoutMins: nil} // nil = unset\nsched.AddJob(job)","handlingStrategy":"validation","validationCode":"if job.TimeoutMins != nil && *job.TimeoutMins < 0 { return errors.New(\"timeout_mins must be >= 0\") }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Use nil for unset and 0 for no-timeout; never negative sentinels like -1","Clamp parsed config values: if v < 0 { v = 0 }","Avoid unchecked integer arithmetic when deriving timeout budgets"],"tags":["timer","validation","range"],"backgroundTag":"value-out-of-range","analyzedSha":"4000b2338aa6e850c99df54f8b0ed6ed7460b401","analyzedAt":"2026-09-06T11:45:09.575Z","contentChangedAt":"2026-09-06T11:45:09.575Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}