// Package jobs implements the background job runner: a hybrid scheduler that // drives idempotent periodic runners (workspace fetch, worktree prune, // attachment cleanup, job reaper, job purge) directly via tickers, and routes // retry-sensitive event handlers (webhook delivery) through a DB-backed jobs // table with SELECT FOR UPDATE SKIP LOCKED dispatch. package jobs import ( "context" "encoding/json" "errors" "time" "github.com/google/uuid" ) // JobType identifies a kind of work that a Handler knows how to execute. // Runners (periodic) are NOT job types — they tick directly. Handlers // (event-driven) are. type JobType string const ( // TypeWebhookDeliver delivers a notify.Message to the configured webhook URL. TypeWebhookDeliver JobType = "webhook.deliver" ) // JobStatus matches the CHECK constraint on jobs.status. type JobStatus string const ( StatusPending JobStatus = "pending" StatusRunning JobStatus = "running" StatusCompleted JobStatus = "completed" StatusDead JobStatus = "dead" ) // Job mirrors a row in the jobs table. type Job struct { ID uuid.UUID Type JobType Payload json.RawMessage Status JobStatus ScheduledAt time.Time Attempts int32 MaxAttempts int32 LeasedAt *time.Time LeasedBy *string LastError *string CompletedAt *time.Time CreatedAt time.Time UpdatedAt time.Time } // MaxPayloadBytes caps payload size at 64 KB. Enforced at enqueue time. const MaxPayloadBytes = 64 * 1024 // Handler executes a single Job. Returning a permanentError sends the job // straight to dead-letter; any other error triggers retry per the worker's // backoff schedule. Returning nil marks the job completed. type Handler interface { Type() JobType Handle(ctx context.Context, job Job) error } // permanentError marks an error as non-retryable (4xx-equivalent). type permanentError struct{ err error } // Permanent wraps err so the worker routes the job to dead-letter without // further retries. func Permanent(err error) error { if err == nil { return nil } return &permanentError{err: err} } func (e *permanentError) Error() string { return e.err.Error() } func (e *permanentError) Unwrap() error { return e.err } // IsPermanent reports whether err was wrapped by Permanent. func IsPermanent(err error) bool { var pe *permanentError return errors.As(err, &pe) } // RunnerConfig is the per-runner configuration loaded from config.yaml. // Zero Interval means "use the runner's documented default". type RunnerConfig struct { Enabled bool Interval time.Duration } // WorktreePruneConfig extends RunnerConfig with prune-specific knobs. type WorktreePruneConfig struct { Enabled bool Interval time.Duration IdleThreshold time.Duration WarningLead time.Duration } // AttachmentCleanupConfig extends RunnerConfig with retention. type AttachmentCleanupConfig struct { Enabled bool Interval time.Duration Retention time.Duration } // JobReaperConfig extends RunnerConfig with the lease timeout used to detect // crashed workers. type JobReaperConfig struct { Enabled bool Interval time.Duration LeaseTimeout time.Duration } // JobPurgeConfig extends RunnerConfig with completed/dead retention. type JobPurgeConfig struct { Enabled bool Interval time.Duration CompletedRetention time.Duration } // AcpEventsPurgeConfig extends RunnerConfig with acp_events retention. type AcpEventsPurgeConfig struct { Enabled bool Interval time.Duration Retention time.Duration } // Config is the full jobs.Module configuration. type Config struct { Enabled bool Workers int ShutdownGrace time.Duration WorkspaceFetch RunnerConfig WorktreePrune WorktreePruneConfig AttachmentCleanup AttachmentCleanupConfig JobReaper JobReaperConfig JobPurge JobPurgeConfig AcpEventsPurge AcpEventsPurgeConfig }