// Package jobs owns generation-job state, ownership, idempotency, and worker // coordination independently of HTTP, provider, storage, and billing adapters. package jobs import ( "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "sort" "strings" "time" ) type Status string const ( StatusQueued Status = "queued" StatusRunning Status = "running" StatusSucceeded Status = "succeeded" StatusFailed Status = "failed" StatusExpired Status = "expired" StatusCancelled Status = "cancelled" ) func (status Status) Terminal() bool { return status == StatusSucceeded || status == StatusFailed || status == StatusExpired || status == StatusCancelled } func (status Status) Valid() bool { return status == StatusQueued || status == StatusRunning || status.Terminal() } type Job struct { ID string `json:"id"` OwnerID string `json:"ownerId"` ExternalClientID string `json:"externalClientId,omitempty"` Capability string `json:"capability"` Provider string `json:"provider"` ReqKey string `json:"reqKey"` Status Status `json:"status"` Prompt string `json:"prompt,omitempty"` InputAssetIDs []string `json:"inputAssetIds"` InputURLs []string `json:"inputUrls"` OutputAssetIDs []string `json:"outputAssetIds"` ProviderTaskID string `json:"providerTaskId,omitempty"` ProviderDispatchStartedAt *time.Time `json:"providerDispatchStartedAt,omitempty"` RequestPayload json.RawMessage `json:"requestPayload"` ResponsePayload json.RawMessage `json:"responsePayload,omitempty"` Error *JobError `json:"error,omitempty"` RetryOf string `json:"retryOf,omitempty"` IdempotencyKey string `json:"idempotencyKey,omitempty"` IdempotencyFingerprint string `json:"idempotencyFingerprint,omitempty"` Priority int `json:"priority,omitempty"` Attempts int `json:"attempts,omitempty"` MaxAttempts int `json:"maxAttempts,omitempty"` ScheduledAt time.Time `json:"scheduledAt,omitempty"` LockedAt *time.Time `json:"lockedAt,omitempty"` LockedBy string `json:"lockedBy,omitempty"` StartedAt *time.Time `json:"startedAt,omitempty"` CompletedAt *time.Time `json:"completedAt,omitempty"` DispatchReadyAt *time.Time `json:"dispatchReadyAt,omitempty"` FinalizedAt *time.Time `json:"finalizedAt,omitempty"` WebhookURL string `json:"webhookUrl,omitempty"` WebhookAttempts int `json:"webhookAttempts,omitempty"` WebhookLastStatus json.RawMessage `json:"webhookLastStatus,omitempty"` UsageContext json.RawMessage `json:"usageContext,omitempty"` Billing json.RawMessage `json:"billing,omitempty"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type JobError struct { Code any `json:"code,omitempty"` Message string `json:"message"` Retryable bool `json:"retryable,omitempty"` } type Scope struct { OwnerID string ExternalClientID string } func (scope Scope) Owns(job Job) bool { if scope.OwnerID == "" || job.OwnerID != scope.OwnerID { return false } return scope.ExternalClientID == "" || job.ExternalClientID == scope.ExternalClientID } type ErrorKind string const ( ErrorInvalid ErrorKind = "invalid" ErrorNotFound ErrorKind = "not_found" ErrorConflict ErrorKind = "conflict" ) type Error struct { Kind ErrorKind Status int Message string } func (err *Error) Error() string { return err.Message } var ErrUniqueIdempotency = errors.New("generation job idempotency key already exists") var ErrTransitionConflict = errors.New("generation job transition conflict") type ListFilter struct { Scope Scope Status Status Capability string Limit int Before *time.Time } type CreateCommand struct { Job Job IdempotencyBody map[string]any // HoldDispatch keeps a newly persisted job invisible to workers until an // external prerequisite (currently the idempotent wallet charge) is durable. HoldDispatch bool } func NormalizePriority(value int) int { if value < -100 { return -100 } if value > 100 { return 100 } return value } func NormalizePublicLimit(value int) int { if value == 0 { return 50 } if value < 1 { return 1 } if value > 200 { return 200 } return value } // Fingerprint excludes the compatibility body key idempotencyKey, recursively // sorts object keys, preserves array order, and hashes the exact stable JSON. func Fingerprint(body map[string]any) (string, error) { source := make(map[string]any, len(body)) for key, value := range body { if key != "idempotencyKey" { source[key] = value } } stable, err := stableJSON(source) if err != nil { return "", err } sum := sha256.Sum256(stable) return hex.EncodeToString(sum[:]), nil } func stableJSON(value any) ([]byte, error) { switch typed := value.(type) { case map[string]any: keys := make([]string, 0, len(typed)) for key := range typed { keys = append(keys, key) } sort.Strings(keys) var output strings.Builder output.WriteByte('{') for index, key := range keys { if index > 0 { output.WriteByte(',') } encodedKey, _ := json.Marshal(key) encodedValue, err := stableJSON(typed[key]) if err != nil { return nil, err } output.Write(encodedKey) output.WriteByte(':') output.Write(encodedValue) } output.WriteByte('}') return []byte(output.String()), nil case []any: var output strings.Builder output.WriteByte('[') for index, item := range typed { if index > 0 { output.WriteByte(',') } encoded, err := stableJSON(item) if err != nil { return nil, err } output.Write(encoded) } output.WriteByte(']') return []byte(output.String()), nil default: encoded, err := json.Marshal(value) if err != nil { return nil, fmt.Errorf("encode idempotency fingerprint: %w", err) } return encoded, nil } }