run

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 7, 2026 License: Apache-2.0 Imports: 32 Imported by: 0

Documentation

Index

Constants

View Source
const (
	PriorityLowValue    = 1
	PriorityNormalValue = 2
	PriorityHighValue   = 3
)

Variables

View Source
var (
	ErrTaskClaimMismatch        = errors.New("run: task claim mismatch")
	ErrRunSkipped               = errors.New("run: skipped by concurrency policy")
	ErrRunQueued                = errors.New("run: queued by concurrency policy")
	ErrQueuedRunUnavailable     = errors.New("run: queued run already claimed or unavailable")
	ErrQueuedRunNotFound        = errors.New("run: queued run not found")
	ErrMaxConcurrentRunsReached = errors.New("run: max concurrent runs reached")
	// ErrJobPaused is returned by RetryFromFailureAdmitted when an agent-initiated
	// retry is refused because the job is paused. A human pause outranks an agent
	// retry (design-agent-in-the-loop.md, retry safety valves).
	ErrJobPaused = errors.New("run: cannot retry while job is paused")
	// ErrPartitionNotRetryable is returned by RetryPartition when the addressed
	// instance is terminal but not FAILED. The retryable set is documented at
	// the guard in RetryPartition; controllers surface this as 409 with the
	// reason, distinguishable from ErrTaskRunNotTerminal (still running).
	ErrPartitionNotRetryable = errors.New("run: only a failed partition can be retried")
	// ErrPartitionRunNotRetryable is returned when the partition is failed but
	// its JobRun cannot execute a reset instance. Running runs can hand the work
	// to their live engine; succeeded/failed runs can be reopened. Cancelled,
	// queued, and unknown states must fail closed before any row/event mutation.
	ErrPartitionRunNotRetryable = errors.New("run: partition retry requires a running or completed run")
	// ErrPartitionRetryBlocked is returned by RetryPartition when the addressed
	// instance is FAILED but nothing in this run can ever make it ready again:
	// every cross-step predecessor group is terminal and the step's trigger
	// rule is not satisfied by them, or an in-group dependsOn sibling is
	// terminal without succeeding. Accepting such a retry would reset a row no
	// engine can dispatch. Controllers surface this as 409 and point at
	// whole-run retry, which resets failed and skipped work as a set.
	ErrPartitionRetryBlocked = errors.New("run: partition retry is blocked by an unsatisfied dependency")

	// ErrTaskRunNotTerminal is returned by RetryPartition when the addressed
	// fan-out instance is still pending or running. Resetting a RUNNING instance
	// mid-flight would orphan its container and let the eventual completion
	// overwrite the fresh attempt, so a per-partition retry is terminal-only.
	// The REST layer maps this to 409.
	ErrTaskRunNotTerminal = errors.New("run: task instance is not terminal")

	// ErrRunHasPendingWork is returned by Complete when a dispatchable
	// per-partition retry is still pending. RetryPartition sets the durable
	// partition_retry_pending marker in the same transaction as the reset; every
	// terminal transition clears it. Marking the JobRun terminal while that
	// marker is pending freezes the retried instance: RetryPartition of a
	// still-running local run reports reopened=false (no HTTP kickoff), and the
	// in-process engine may already have left the group. The same transaction as
	// the status write must refuse so this cannot TOCTOU with the retry.
	// Running/claimed retries do not match; a retry still waiting on a live
	// dependency does — it is exactly as stranded by a terminal JobRun as a
	// ready one, and the replacement engine is what releases it.
	ErrRunHasPendingWork = errors.New("run: cannot complete while task work is pending")
)
View Source
var ErrAmbiguousTaskRun = errors.New("run: multiple task instances match (run, task); TaskRun ID required")

ErrAmbiguousTaskRun is returned when a (job_run_id, task_id) predicate matches more than one TaskRun row. Write paths must then address the instance by primary key rather than silently matching the first sibling.

View Source
var ErrInvalidPriority = errors.New("run: invalid priority")
View Source
var ErrPartitionNotFound = errors.New("run: partition not found")

ErrPartitionNotFound is returned when a --partition selector names a value that the task's fan-out group does not contain (or when the task is not fanned at all). Its message lists the available partition values so the operator can retry without a second round trip.

View Source
var (
	// ErrRunDiffJobMismatch is returned when either run does not belong to the
	// requested job, or the two runs are from different jobs.
	ErrRunDiffJobMismatch = errors.New("run: run diff job mismatch")
)
View Source
var ErrTaskInstanceNotRetryable = errors.New("run: task instance is not retryable")

ErrTaskInstanceNotRetryable is returned when a retry targets an instance row that a cascade or cancellation has already resolved.

View Source
var ErrTaskRunNotFound = errors.New("run: task not found in run")

ErrTaskRunNotFound is returned when no task matching the given id/name exists in the run.

Functions

func FailedOrLastTaskRunForTask

func FailedOrLastTaskRunForTask(rows []models.TaskRun) *models.TaskRun

FailedOrLastTaskRunForTask picks the one instance that best represents a (runID, taskID) for failure attribution: the first failed instance in partition order, else the first non-successful one, else the first row. Incident classification and agent context both need "the instance that explains this failure" — reading an arbitrary sibling could classify a group from a *succeeded* row.

func FromContext

func FromContext(ctx context.Context) (uuid.UUID, bool)

func GroupIdentityHash

func GroupIdentityHash(instanceHashes []string) string

GroupIdentityHash folds a fan-out group's per-instance identity hashes into the ONE aggregate hash a downstream step folds into its own cache identity.

instanceHashes must be the effective hashes of the group's terminal-SUCCESS instances in PARTITION-INDEX order (emission order — stable across runs and independent of scheduling order or which instance finished last). Empty entries are skipped; an all-empty list yields "".

The definition is fixed by the design and is shared with the SQL read path (predecessorGroupHash, internal/run/store.go): it is

sha256( "fanout-group:" || h(0) || "\n" || h(1) || "\n" || … )

Both sites reuse fanOutGroupHashPrefix so the two can never disagree on the namespace. One aggregate entry per predecessor is load-bearing: N entries would change the SHAPE of the downstream key's pred_hash: lines, so adding or removing a single partition would re-key the whole downstream subtree.

func IsFanOutInstance

func IsFanOutInstance(tr *models.TaskRun) bool

IsFanOutInstance reports whether a TaskRun row is one instance of a fanned group. Exported so internal/worker can ask the question from the SAME definition the SQL lane uses rather than re-deriving "does this row have a partition" from the column set — the two drifting is how a fanned instance ends up treated as an ordinary task on one path only.

func IsSuccessfulTaskResult

func IsSuccessfulTaskResult(result string) bool

func IsTerminal

func IsTerminal(status TaskStatus) bool

IsTerminal reports whether a task status is terminal — the task will not transition again. This is the single definition of the terminal vocabulary (succeeded, failed, skipped, cached), reused by owner replay, the recovery scan, and archival so the set lives in exactly one place.

func IsTerminalSuccess

func IsTerminalSuccess(status TaskStatus) bool

IsTerminalSuccess returns true for task statuses that represent successful completion.

func IsTolerantTriggerRule

func IsTolerantTriggerRule(rule string) bool

IsTolerantTriggerRule reports whether a rule explicitly handles upstream FAILURE, and therefore must never be pre-emptively skipped when a predecessor fails under the `continue` failure policy — its own rule evaluation, once every predecessor is terminal, is what decides.

It lives here with CollectPredecessorStatuses and SatisfiesTriggerRule because all three answer the same question and all three have more than one caller: the local executor's skipDescendantsFiltered (internal/job) and the distributed worker's descendant sweep (internal/worker) both need it, and they cannot share a helper anywhere else — internal/job imports internal/worker, so the dependency can only point this way. They had drifted: the local sweep filtered by rule and the worker's did not, so under `continue` a distributed run skipped the very all_done consumer a failed predecessor had just released.

func PartitionStatusCounts

func PartitionStatusCounts(instances []*TaskRun) map[string]int

PartitionStatusCounts builds the per-status histogram of a fan-out group. Keys are TaskStatus values; a status with no instances is absent rather than zero, so the map stays small for a 10k-instance group. Returns nil for an empty group so the field is omitted from JSON.

func PriorityLabel

func PriorityLabel(priority int) string

func PriorityValue

func PriorityValue(priority string) (int, error)

func RecoverRunState

func RecoverRunState(topo RunTopology, checkpoint *models.RunCheckpoint, terminalRows []models.TaskRun) (*RunState, RecoveryResult, error)

RecoverRunState reconstructs a RunState for an owned run after an ownership change. topo is reloaded from the catalog; checkpoint is the latest full snapshot (nil to replay from scratch); terminalRows are the post-checkpoint terminal task_runs rows ordered by terminal_sequence ascending (from Store.TerminalTaskRunsSince).

It restores the snapshot (or a fresh state), replays each terminal row to advance the DAG, then classifies the leftover non-terminal tasks: running tasks become ReDispatch (their worker outcome was lost), and ready tasks become Ready. Wall-clock time is never consulted; ordering is by terminal_sequence so recovery is deterministic and clock-skew-safe.

func RecoverRunStateWithFanOut

func RecoverRunStateWithFanOut(
	topo RunTopology,
	checkpoint *models.RunCheckpoint,
	terminalRows, instanceRows []models.TaskRun,
	catalog []models.Task,
) (*RunState, RecoveryResult, error)

RecoverRunStateWithFanOut is the full recovery entry point. catalog carries the run's catalog Task rows so the rehydrated state re-seeds the fan-out scheduling metadata (maxParallel, step names) the checkpoint deliberately does not snapshot — the catalog is its single source of truth.

func RecoverRunStateWithInstances

func RecoverRunStateWithInstances(topo RunTopology, checkpoint *models.RunCheckpoint, terminalRows, instanceRows []models.TaskRun) (*RunState, RecoveryResult, error)

RecoverRunStateWithInstances additionally rebuilds fan-out in-group edges from the run's instance rows. Prefer RecoverRunStateWithFanOut: without the catalog rows the recovered state cannot re-seed fanOut.maxParallel.

func SatisfiesTriggerRule

func SatisfiesTriggerRule(rule string, predStatuses []TaskStatus) bool

SatisfiesTriggerRule evaluates the trigger rule against the provided predecessor statuses. It returns true when the task should run, false when it should be skipped. An empty rule defaults to all_success; a task with no predecessors always runs.

func SchemaGateOverridden

func SchemaGateOverridden(store *Store, runID uuid.UUID) bool

SchemaGateOverridden reports whether an APPROVED tier-3 `override_schema_gate` action bypassed output-schema enforcement for this one run (models.JobRun.SchemaGateOverride, written only by the incident action executor after a human approval).

It is the single read point both validation entry points consult, so the bypass cannot apply to the unfanned path and silently not to the fanned one. A missing store/run or a read error is "not overridden" — the gate stays ON, which is the safe direction for a security-adjacent bypass.

func SecretIdentityDescriptorRef

func SecretIdentityDescriptorRef(envKey, ref string, identity secret.Identity) models.TaskExecutionSecretRef

SecretIdentityDescriptorRef converts a resolved secret identity into the immutable TaskRun descriptor shape. It intentionally omits the secret value.

func SetHasAnyFanOutConsumerErrForTest

func SetHasAnyFanOutConsumerErrForTest(err error)

SetHasAnyFanOutConsumerErrForTest overrides HasAnyFanOutConsumerForRun's result process-wide for exactly as long as the override is set. Pass nil to restore the real behaviour; callers MUST do so (typically via t.Cleanup).

func SetHasFanOutSuccessorErrForTest

func SetHasFanOutSuccessorErrForTest(err error)

SetHasFanOutSuccessorErrForTest overrides HasFanOutSuccessor's result process-wide for exactly as long as the override is set — see hasFanOutSuccessorErrForTest. Pass nil to restore the real behaviour; callers MUST do so (typically via t.Cleanup) before their test returns.

func SetStartParamsEnricher

func SetStartParamsEnricher(fn StartParamsEnricher)

SetStartParamsEnricher registers the enricher run creation applies, or clears it when fn is nil. Called once from the server bootstrap; with none registered, run creation behaves exactly as it did before the seam existed.

func ValidateCheckpointBlob

func ValidateCheckpointBlob(blob []byte) error

ValidateCheckpointBlob reports whether Restore would accept blob.

A recovering owner must decide this BEFORE it queries the terminal tail. The tail query is filtered by the checkpoint's sequence_high, so if the checkpoint is then rejected and recovery falls back to a from-scratch replay with a replay start of zero, the rows in hand are still only the post-checkpoint tail: every terminal transition at or below sequence_high is silently lost, and the run resurrects work it already finished. Validating first lets the caller drop the sequence filter in the same breath it drops the checkpoint.

func ValidateTaskOutputSchema

func ValidateTaskOutputSchema(store *Store, runID, taskID uuid.UUID, output map[string]string, outputSchema []byte, schemaValidation string) error

ValidateTaskOutputSchema validates a task's captured output against its declared schema, persists any violations, and escalates them according to the configured validation mode.

func ValidateTaskOutputSchemaInstance

func ValidateTaskOutputSchemaInstance(
	store *Store,
	runID, taskID, taskRunID uuid.UUID,
	output map[string]string,
	outputSchema []byte,
	schemaValidation string,
) error

ValidateTaskOutputSchemaInstance is the instance-keyed form of ValidateTaskOutputSchema (internal/run/schema_validation.go).

It differs in exactly one respect: the violations are recorded on the TaskRun named by taskRunID rather than on whatever row `(runID, taskID)` resolves to. That matters because SaveSchemaViolations now refuses an ambiguous catalog task id, and the refusal is only logged — so under fan-out the step-keyed form silently recorded NOTHING. In fail mode that loses the evidence for the very failure being reported; in warn mode it opens a schema_violation incident with no row behind it. taskID is still carried for the log lines, the error text and the emitted event, all of which identify the STEP.

func WithContext

func WithContext(ctx context.Context, id uuid.UUID) context.Context

Types

type BlobDiff

type BlobDiff struct {
	// HashEqual is true when the two blobs decompose the same identity hash. By
	// construction equal hashes mean every hashed input was identical, so Changes
	// holds no DISCRIMINATING entry; HashEqual=false with none can only happen
	// when a blob is oversized/unparseable (see Degraded).
	//
	// Changes itself is not necessarily empty when HashEqual is true: a values-mode
	// hit carries a fieldExcluded marker naming an input that was deliberately kept
	// OUT of the key (its predecessor hashes may well differ — that is the point).
	// Every consumer that counts or headlines "what changed" must therefore filter
	// through discriminatingChanges rather than reading len(Changes).
	HashEqual bool `json:"hashEqual"`
	// SubjectHash / BaselineHash are the inline Compute() digests each blob
	// carries, surfaced so a caller can confirm the diff matched the runs it
	// expected.
	SubjectHash  string `json:"subjectHash,omitempty"`
	BaselineHash string `json:"baselineHash,omitempty"`
	// Changes lists every discriminating field, sorted by Field for determinism.
	Changes []FieldChange `json:"changes,omitempty"`
	// Degraded is set with a human-readable reason when a field-level diff was
	// not possible (an oversized blob, a version mismatch, or unparseable JSON).
	// The hash-level verdict (HashEqual) is still valid; only the field detail is
	// unavailable.
	Degraded string `json:"degraded,omitempty"`
	// Notes are qualifiers about HOW the two keys were computed, as opposed to
	// which inputs differ. Today the only note is the chain: values exclusion,
	// which the summary line and every renderer must surface: without it a
	// consumer that stayed cached while its predecessor visibly changed is an
	// unexplainable skip (spec §4.3).
	Notes []string `json:"notes,omitempty"`
}

BlobDiff is the structured result of diffing two HashInput blobs.

func DiffHashInputBlobs

func DiffHashInputBlobs(subject, baseline []byte) (*BlobDiff, error)

DiffHashInputBlobs decodes and diffs two canonical HashInput blobs: subject is the run being explained, baseline is what it is compared against (the prior run of the same task for a miss, or the cache-origin entry for a hit). Either blob may be nil/empty (e.g. caching was disabled, or there is no prior run); the result reports that gracefully via Degraded rather than erroring.

type CacheHitSource

type CacheHitSource struct {
	RunID     uuid.UUID
	CreatedAt time.Time
	ExpiresAt *time.Time
}

type CallbackRun

type CallbackRun struct {
	ID          uuid.UUID      `json:"id"`
	CallbackID  uuid.UUID      `json:"callback_id"`
	Status      CallbackStatus `json:"status"`
	Error       string         `json:"error,omitempty"`
	StartedAt   time.Time      `json:"started_at"`
	CompletedAt *time.Time     `json:"completed_at,omitempty"`
}

type CallbackStatus

type CallbackStatus string
const (
	CallbackStatusRunning   CallbackStatus = "running"
	CallbackStatusSucceeded CallbackStatus = "succeeded"
	CallbackStatusFailed    CallbackStatus = "failed"
)

type CheckpointConfig

type CheckpointConfig struct {
	Events    int           // checkpoint after this many new terminal transitions
	Interval  time.Duration // ...or this much elapsed since the last checkpoint, whichever first
	KeepFulls int           // full snapshots to retain when pruning
}

CheckpointConfig controls checkpoint cadence and retention.

type CheckpointWriter

type CheckpointWriter struct {
	// contains filtered or unexported fields
}

CheckpointWriter persists a RunState snapshot on a cadence — whichever comes first of Events new terminal transitions or Interval elapsed — and prunes old checkpoints afterward. One per owned run; the owner calls Maybe after applying completions and Force on graceful handoff/shutdown. v1 always writes full snapshots (is_incremental=false); delta checkpoints are a later optimization. Not safe for concurrent use.

func NewCheckpointWriter

func NewCheckpointWriter(store checkpointPersister, runID uuid.UUID, cfg CheckpointConfig) *CheckpointWriter

NewCheckpointWriter builds a writer for runID; zero/negative config values fall back to the design defaults (100 events, 2s, keep 3 fulls).

func (*CheckpointWriter) Force

func (w *CheckpointWriter) Force(rs *RunState, ownerGeneration int64) error

Force writes a full checkpoint and prunes, regardless of cadence.

func (*CheckpointWriter) Maybe

func (w *CheckpointWriter) Maybe(rs *RunState, ownerGeneration int64) error

Maybe writes a checkpoint when the cadence threshold is met, else no-ops.

type CompleteResult

type CompleteResult struct {
	Ready    []uuid.UUID // tasks that newly became ready to dispatch
	Complete bool        // the run reached a terminal state
	Owned    bool        // false if this node does not own the run (caller should fall back)
}

CompleteResult reports the outcome of applying a worker completion.

type CompleteTaskResult

type CompleteTaskResult struct {
	SkippedTaskIDs []uuid.UUID
	Expansion      *FanOutExpansion
}

CompleteTaskResult holds the result of a task completion, including any tasks that were skipped due to branch filtering and any fan-out expansion this completion performed.

type CompletionResult

type CompletionResult struct {
	TerminalSequence int64
	Ready            []uuid.UUID
	Skipped          []SkippedTask
	Complete         bool
	Applied          bool
}

CompletionResult is what ApplyCompletion returns: the sequence stamped on the completed task, the tasks that newly became ready to dispatch, the tasks the owner skipped as a consequence, and whether the run is now complete.

Applied distinguishes a completion this call actually advanced from one that was already applied and is being replayed (see ApplyCompletion). Callers decide whether to persist with Durable, never by testing TerminalSequence against zero.

func (CompletionResult) Durable

func (r CompletionResult) Durable() bool

Durable reports whether the result carries terminal rows the owner must write: a sequence stamped on the completing task, or owner-decided skips. A result that is not Durable must NOT be persisted — CompleteTaskOwner would stamp terminal_sequence = 0 on the row, and recovery reads the terminal tail with a strictly-greater predicate (`terminal_sequence > ?`), so a zero-stamped row is invisible to replay after a takeover.

type DispatchableTask

type DispatchableTask struct {
	TaskID    uuid.UUID
	TaskRunID uuid.UUID
	Attempt   int
}

DispatchableTask is a ready task plus the attempt number to stamp on its dispatch (1 for a first run, incremented for a re-dispatch after recovery).

The two identities are deliberately separate and both load-bearing. TaskID is always the *catalog* task, which is what rate-limit rules, trigger rules, and every other catalog lookup are keyed by. TaskRunID names the specific instance row to execute and to fence its completion against; it is uuid.Nil for an unfanned task, where the (run, task) pair still names exactly one row. Collapsing the two — sending an instance id as the task id — makes every catalog lookup silently miss.

func (DispatchableTask) ExecutionRef

func (d DispatchableTask) ExecutionRef() uuid.UUID

ExecutionRef is the identity a dispatched task's row-level operations address: the instance when the task is fanned, the catalog task otherwise (which loadTaskRunByIDOrUnique resolves to the run's single row).

type ExpandedGroup

type ExpandedGroup struct {
	TaskID      uuid.UUID
	TaskName    string
	OnEmpty     string
	Skipped     bool
	MaxParallel int
	Instances   []ExpandedInstance
	Dependents  map[string][]string
}

ExpandedGroup is the N instances of one fanned successor step.

type ExpandedInstance

type ExpandedInstance struct {
	TaskRunID               uuid.UUID
	TaskID                  uuid.UUID
	PartitionIndex          int
	Partition               pkgtask.Partition
	OutstandingPredecessors int
}

ExpandedInstance is one materialized fan-out TaskRun the local executor needs because it never re-reads the store mid-run.

type FanOutExpansion

type FanOutExpansion struct {
	ProducerTaskID uuid.UUID
	Partitions     []pkgtask.Partition
	Groups         []ExpandedGroup
}

FanOutExpansion is the payload CompleteTaskResult returns so all three advancement paths observe the same instance set.

type FieldChange

type FieldChange struct {
	// Field is the dotted path of the changed input, e.g. "image",
	// "env.DATABASE_URL", "predecessorOutputs.extract.row_count".
	Field string `json:"field"`
	// Kind classifies how to interpret Before/After.
	Kind blobFieldKind `json:"kind"`
	// Before is the value in the baseline blob ("" / null when the field was
	// absent there — i.e. the input was added). For redacted env values this is
	// the digest, never the plaintext.
	Before string `json:"before,omitempty"`
	// After is the value in the subject blob ("" / null when the field was
	// removed). For redacted env values this is the digest.
	After string `json:"after,omitempty"`
	// Added is true when the field exists only in the subject blob.
	Added bool `json:"added,omitempty"`
	// Removed is true when the field exists only in the baseline blob.
	Removed bool `json:"removed,omitempty"`
	// Redacted is true when the compared value is a redacted env digest, so a
	// renderer can label it "(redacted; digest differs)" rather than printing the
	// digest as if it were the literal value.
	Redacted bool `json:"redacted,omitempty"`
	// Note is a human-readable qualifier a renderer prints in place of a
	// before/after comparison. It is set on fieldExcluded entries
	// ("excluded (chain: values)") so both the CLI table's CHANGE column and the
	// Console render one authored phrase rather than each inventing its own.
	Note string `json:"note,omitempty"`
}

FieldChange is one discriminating field between two HashInput blobs.

type InstanceIdentity

type InstanceIdentity struct {
	TaskRunID      uuid.UUID
	PartitionValue string
	PartitionIndex int
	Status         TaskStatus
	// Output is the decoded output map the instance recorded (nil when it
	// emitted none, or when the column could not be decoded).
	Output map[string]string
	// IdentityHash is the EFFECTIVE identity — effective_hash when a
	// value-verified short-circuit was proven for this instance, else hash. It
	// is the same value the SQL read path folds into a downstream key
	// (predecessorGroupHash), so a hash rebuilt from these rows and one computed
	// by the distributed lane cannot disagree.
	IdentityHash string
}

InstanceIdentity is one fan-out instance's persisted outcome: what it emitted and the identity it presents to downstream consumers.

It exists because neither of the two read surfaces the local scheduler already has can answer "what did the siblings I did NOT execute produce?". TaskRunInstances returns the run.TaskRun projection, which deliberately omits the hash columns; the in-memory maps runFannedGroup builds only ever describe the instances THIS invocation dispatched. After a manual partition retry that is a single instance, and the group's fan-in aggregate and aggregate identity hash must still cover all N.

type JobRun

type JobRun struct {
	ID            uuid.UUID         `json:"id"`
	JobID         uuid.UUID         `json:"job_id"`
	JobAlias      string            `json:"job_alias,omitempty"`
	JobLabels     map[string]string `json:"job_labels,omitempty"`
	BackfillID    *uuid.UUID        `json:"backfill_id,omitempty"`
	TriggerType   string            `json:"trigger_type,omitempty"`
	TriggerAlias  string            `json:"trigger_alias,omitempty"`
	Status        Status            `json:"status"`
	Priority      int               `json:"priority"`
	Params        map[string]string `json:"params,omitempty"`
	Quarantine    bool              `json:"quarantine"`
	StartedAt     time.Time         `json:"started_at"`
	CompletedAt   *time.Time        `json:"completed_at,omitempty"`
	CreatedAt     time.Time         `json:"created_at"`
	UpdatedAt     time.Time         `json:"updated_at"`
	Error         string            `json:"error,omitempty"`
	Tasks         []*TaskRun        `json:"tasks"`
	Callbacks     []*CallbackRun    `json:"callbacks"`
	CacheHits     int               `json:"cache_hits"`
	ExecutedTasks int               `json:"executed_tasks"`
	TotalTasks    int               `json:"total_tasks"`
}

type LeaseStore

type LeaseStore struct {
	// contains filtered or unexported fields
}

LeaseStore manages run_leases rows for Phase 2 run-owner coordination. All operations are safe to call when owner mode is disabled — they simply become no-ops.

func NewLeaseStore

func NewLeaseStore(db *gorm.DB) *LeaseStore

NewLeaseStore constructs a LeaseStore backed by the given connection.

func (*LeaseStore) AcquireExpiredLeases

func (ls *LeaseStore) AcquireExpiredLeases(ctx context.Context, newOwner string, ttl time.Duration) (int64, error)

AcquireExpiredLeases takes over every run lease whose holder let it expire (lease_expires_at <= now), reassigning it to newOwner with an incremented generation and a fresh expiry — in a single atomic UPDATE. The expiry predicate is the compare-and-swap: if two nodes sweep concurrently, the first commit moves those rows out of the expired set, so the second updates nothing (no double takeover). Returns the number of leases taken over; the caller's dispatch loop then sees them via OwnedRunsWithGenerations and recovers their in-memory state from the latest checkpoint + terminal tail.

This is the run-owner failover mechanism: in in-memory mode the DB's per-task predecessor counters are stale (the owner advanced the DAG in memory), so ClaimNext recovery does not apply — a peer must take ownership and replay.

func (*LeaseStore) AcquireLease

func (ls *LeaseStore) AcquireLease(ctx context.Context, runID uuid.UUID, ownerNode string, ttl time.Duration) (int64, error)

AcquireLease writes a run_leases row for the given run, recording the owning node and expiry. If a row already exists (e.g., from a previous attempt), it is left unchanged — the initial write is treated as idempotent: whoever wrote it first is the owner.

Returns the generation written on success (always 1 for a fresh lease).

func (*LeaseStore) GetLease

func (ls *LeaseStore) GetLease(ctx context.Context, runID uuid.UUID) (*models.RunLease, error)

GetLease returns the current lease record for runID, if any.

func (*LeaseStore) IsOwner

func (ls *LeaseStore) IsOwner(ctx context.Context, ownerNode string, runID uuid.UUID) (bool, error)

IsOwner returns true if ownerNode currently holds a valid (non-expired) lease on runID. Used to validate requests before acting as owner.

func (*LeaseStore) OwnedRuns

func (ls *LeaseStore) OwnedRuns(ctx context.Context, ownerNode string) ([]uuid.UUID, error)

OwnedRuns returns the IDs of all runs currently owned by ownerNode whose leases have not yet expired. Used by the renewal ticker to build its batch.

func (*LeaseStore) OwnedRunsWithGenerations

func (ls *LeaseStore) OwnedRunsWithGenerations(ctx context.Context, ownerNode string) (map[uuid.UUID]int64, error)

OwnedRunsWithGenerations returns a map of run IDs to lease generations for every non-expired lease owned by ownerNode, in a single query. Used by the dispatch loop to avoid an N+1 GetLease pattern on every tick.

func (*LeaseStore) RenewOwnedLeases

func (ls *LeaseStore) RenewOwnedLeases(ctx context.Context, ownerNode string, newExpiresAt time.Time) (int64, error)

RenewOwnedLeases extends lease_expires_at in a single UPDATE for every non-expired lease owned by ownerNode — no upstream SELECT required. Returns the number of rows actually renewed (which is also the count of currently owned, non-expired leases).

Use this on the per-node renewal ticker; it replaces an OwnedRuns + RenewRunLeases pair with one round-trip.

func (*LeaseStore) RenewRunLeases

func (ls *LeaseStore) RenewRunLeases(ctx context.Context, ownerNode string, runIDs []uuid.UUID, newExpiresAt time.Time) (int64, error)

RenewRunLeases performs a single batched UPDATE extending lease_expires_at for every run in runIDs that is still owned by ownerNode. The WHERE clause on owner_node is the safety net that prevents renewing a lease that was taken over by another node between the decision and the write.

Returns the number of rows actually updated.

type OwnerManager

type OwnerManager struct {
	// contains filtered or unexported fields
}

OwnerManager holds the authoritative in-memory RunState for the runs this node owns (run-owner in-memory mode, CAESIUM_RUN_OWNER_IN_MEMORY=true). It is the integration seam the dispatch loop and the /internal/complete handler call:

  • Adopt(runID) — seed a fresh RunState for a run this node just created.
  • Recover(runID) — rebuild a run's state from checkpoint + terminal tail on lease takeover.
  • Ready(runID) — the ready queue the dispatch loop pulls from.
  • MarkDispatched — record a task pushed to a worker.
  • Complete(...) — apply a worker completion: advance the DAG in memory, durably write only terminal rows, and checkpoint.
  • Drop(runID) — release a run on completion or lease loss.

Concurrency: the global mu guards only the runs map (brief lookups / inserts / deletes). All per-run work — RunState mutation and the run's DB operations — is serialized by that run's own ownedRun.mu, so different runs proceed concurrently and a slow DB call for one run never blocks the others. DB work done while building a run (Adopt/Recover) runs before the run is published into the map, so it holds no manager lock at all.

func NewOwnerManager

func NewOwnerManager(store *Store, cfg CheckpointConfig) *OwnerManager

NewOwnerManager builds a manager backed by store, using cfg for checkpoint cadence and retention.

func (*OwnerManager) Adopt

func (m *OwnerManager) Adopt(runID uuid.UUID, generation int64) error

Adopt seeds a fresh in-memory state for a run this node created and owns at the given generation. Topology is loaded from the catalog (outside any lock). Idempotent: a second Adopt for an already-tracked run is a no-op.

func (*OwnerManager) Complete

func (m *OwnerManager) Complete(runID, taskID uuid.UUID, status TaskStatus, result, errMsg, claimedBy string, output map[string]string, branchSelections []string) (CompleteResult, error)

Complete applies a worker-reported terminal outcome to the owned run: it resolves any branch skips, advances the in-memory DAG, durably writes the terminal rows (completed task + skips) via CompleteTaskOwner, and checkpoints on cadence. Returns the newly-ready tasks and whether the run is complete. Owned is false when this node does not own the run, signalling the caller to fall back to the SQL path.

Completions can be delivered more than once — a worker re-POSTs the identical envelope when this handler answers 503 for transient dqlite contention, and by then the DAG has already advanced in memory even though the durable write did not land. ApplyCompletion replays what that first delivery decided, so the retry re-persists the same terminal rows at the same sequences. The write is gated on CompletionResult.Durable: a result with nothing this owner stamped must never be persisted, because CompleteTaskOwner would write terminal_sequence = 0 and recovery reads the terminal tail with `terminal_sequence > ?`, making the row invisible after a takeover.

All run work is serialized by the run's own lock; finalize/drop run after that lock is released, so the brief map lock is never held during a DB call.

func (*OwnerManager) CompleteInstance

func (m *OwnerManager) CompleteInstance(runID, taskID, taskRunID uuid.UUID, status TaskStatus, result, errMsg, claimedBy string, output map[string]string, branchSelections []string, partitions []pkgtask.Partition) (CompleteResult, error)

func (*OwnerManager) Drop

func (m *OwnerManager) Drop(runID uuid.UUID)

Drop releases the run's in-memory state (on completion or lease loss). A final checkpoint is forced so a subsequent takeover replays the least tail.

The checkpoint is written BEFORE the run is forgotten, and that order is load-bearing against a concurrent partition retry. Forgetting first leaves a window in which Release finds the run untracked, returns without marking it stale and without deleting its checkpoints — and then this forced "run is complete" snapshot lands after the store's DeleteCheckpoints and survives, so the next recovery restores a complete run and never dispatches the reset instance. Writing first means Release either still sees the run (marks it stale, so this write is a no-op) or runs afterwards and deletes what was written.

func (*OwnerManager) MarkDispatched

func (m *OwnerManager) MarkDispatched(runID, taskID uuid.UUID, worker string, attempt int, leaseExpiresAtMs int64)

MarkDispatched records that a ready task was pushed to a worker.

func (*OwnerManager) Owns

func (m *OwnerManager) Owns(runID uuid.UUID) bool

Owns reports whether this node is tracking in-memory state for the run.

func (*OwnerManager) Ready

func (m *OwnerManager) Ready(runID uuid.UUID) []uuid.UUID

Ready returns the run's current ready queue in dispatch order, or nil if the run is not owned by this node.

func (*OwnerManager) ReadyForDispatch

func (m *OwnerManager) ReadyForDispatch(runID uuid.UUID) []DispatchableTask

ReadyForDispatch returns the run's ready tasks paired with their current attempt, for the dispatch loop to push. Nil if the run is not owned here.

func (*OwnerManager) ReclaimExpiredClaims

func (m *OwnerManager) ReclaimExpiredClaims(runID uuid.UUID) []uuid.UUID

ReclaimExpiredClaims returns this run's in-flight tasks whose worker claim lease has lapsed to the ready queue, and reports which it re-queued.

It closes the one hole the two existing reapers leave between them. A worker that dies mid-task leaves its row `running` with a dead claim_expires_at. Claimer.ReclaimExpired will not touch it — its live-lease guard deliberately skips rows belonging to a run whose owner is alive, so the reaper can never race the owner's dispatch loop. The owner, meanwhile, only ever re-queued in-flight work on TAKEOVER (Recover → requeueRunning); for a run whose owner is perfectly healthy, the instance stayed `running` in memory forever, consuming a fanOut.maxParallel slot and blocking the run from ever completing.

The durable claim_expires_at is authoritative, not the owner's own LeaseExpiresAtMs: a worker renews its claim lease directly and never tells the owner, so the in-memory copy is only a filter (see RunState.AnyLeaseOverdue) — it can over-report, never under-report, and the query settles it.

The reset and the re-queue happen under the run's own lock, so this cannot race the dispatch it feeds. Returns nil for a run this node does not own.

func (*OwnerManager) Recover

func (m *OwnerManager) Recover(runID uuid.UUID, generation int64) (RecoveryResult, error)

Recover rebuilds a run's in-memory state after a lease takeover: it loads the topology, the latest checkpoint, and the post-checkpoint terminal rows, then reconstructs RunState. All of this runs outside any manager lock (the run is not yet published). The RecoveryResult tells the caller which tasks are ready and which running tasks were re-queued for dispatch.

Because the rebuild holds no lock, a store-side invalidation (a partition retry reopening this run) can land in the middle of it. The epoch snapshot taken here is what stops the rebuild from publishing its now-obsolete view over the top: put refuses it, and the loop rebuilds off the current rows. The generation checkpoint is written only AFTER a successful publish, so a refused rebuild leaves nothing durable behind either.

func (*OwnerManager) Release

func (m *OwnerManager) Release(runID uuid.UUID) error

Release forgets a run WITHOUT checkpointing its in-memory state and discards every checkpoint it has on disk. It is for the case where that state is known to be stale — the store refused the owner's completion because a per-partition retry reopened work the state never saw — and a recovering owner must rebuild from the rows instead.

The order is load-bearing. The run is marked stale under its own lock first, so a completion that captured the pointer cannot checkpoint it afterwards; then the checkpoints are deleted while this owner still holds the run; only then is the run forgotten. Forgetting it first would open a window in which a recovery tick restores a checkpoint the invalidation has not reached yet, into a fresh owner that would never see the reset row.

func (*OwnerManager) WithReclaimInterval

func (m *OwnerManager) WithReclaimInterval(d time.Duration) *OwnerManager

WithReclaimInterval overrides the floor between owner-side expired-claim queries for one run. Zero or negative restores the default.

type OwnerTaskState

type OwnerTaskState struct {
	Status           TaskStatus `json:"status"`
	Attempt          int        `json:"attempt"`
	ClaimedBy        string     `json:"claimed_by,omitempty"`
	LeaseExpiresAtMs int64      `json:"lease_expires_at_ms,omitempty"`
	// Started distinguishes a task whose container is actually up from one that
	// is merely DISPATCHED — accepted by a peer and sitting in its worker pool
	// with nothing running yet. Both are Status running here (MarkDispatched is
	// the only transition into it), and telling them apart is what lets
	// fail_fast cancel a sibling before it starts instead of watching it start
	// after the group has already failed. See MarkStarted / SyncStartedFromRows;
	// the durable marker is the TaskRun's runtime_id.
	Started bool `json:"started,omitempty"`
}

OwnerTaskState is the per-task state the run owner holds in memory for a run it owns (run-owner mode). It mirrors the subset of task_runs columns the owner needs to coordinate dispatch and reconstruct after a crash.

type RecoveryResult

type RecoveryResult struct {
	// Ready are pending tasks whose predecessors are satisfied — dispatch them.
	Ready []uuid.UUID
	// ReDispatch are tasks left running by the previous owner with no terminal
	// row, i.e. in-flight work whose worker outcome never reached the owner.
	// They are re-dispatched with attempt+1 and a fresh claim.
	ReDispatch []uuid.UUID
	// MaxSequence is the highest terminal_sequence observed (checkpoint or row);
	// the recovered owner continues allocating from here.
	MaxSequence int64
	// SequenceGaps are missing terminal_sequence values between the checkpoint
	// and the highest observed row.  A gap means the previous owner allocated a
	// sequence but crashed before persisting the row; the affected task is
	// treated as never-completed and recovered via ReDispatch.  Reported for
	// observability; not an error.
	SequenceGaps []int64
	// Complete is true when every task is already terminal (nothing to do).
	Complete bool
}

RecoveryResult summarizes what a recovering owner must do after reconstructing a run's state from a checkpoint plus the post-checkpoint terminal rows.

type RegisterTaskInput

type RegisterTaskInput struct {
	Task                    *models.Task
	Atom                    *models.Atom
	OutstandingPredecessors int
	PartitionIndex          int
}

type ResolvedGroup

type ResolvedGroup struct {
	TaskID   uuid.UUID
	TaskName string
	Duration time.Duration
}

ResolvedGroup is one fan-out group that reached a fully terminal state, with the wall-clock duration from its first instance's dispatch. Duration is zero when the start is unknown (a group whose first dispatch predates a takeover).

type Result

type Result string

type RunDiff

type RunDiff struct {
	JobID      uuid.UUID `json:"jobId"`
	LeftRunID  uuid.UUID `json:"leftRunId"`
	RightRunID uuid.UUID `json:"rightRunId"`

	LeftStatus  Status `json:"leftStatus"`
	RightStatus Status `json:"rightStatus"`

	LeftTrigger  WhyTrigger `json:"leftTrigger"`
	RightTrigger WhyTrigger `json:"rightTrigger"`

	TriggerChanges    []FieldChange `json:"triggerChanges,omitempty"`
	ParamChanges      []FieldChange `json:"paramChanges,omitempty"`
	Tasks             []RunDiffTask `json:"tasks"`
	TasksAdded        []string      `json:"tasksAdded,omitempty"`
	TasksRemoved      []string      `json:"tasksRemoved,omitempty"`
	PartitionsAdded   []string      `json:"partitionsAdded,omitempty"`
	PartitionsRemoved []string      `json:"partitionsRemoved,omitempty"`
	GeneratedAt       time.Time     `json:"generatedAt"`
}

RunDiff is the machine-readable, read-side diff of two runs of the same job. It is cache-bust attribution only: it compares persisted HashInput blobs and trigger/run params, not row- or column-level data values.

type RunDiffTask

type RunDiffTask struct {
	TaskName  string `json:"taskName"`
	Partition string `json:"partition,omitempty"`

	LeftTaskRunID  uuid.UUID `json:"leftTaskRunId"`
	RightTaskRunID uuid.UUID `json:"rightTaskRunId"`
	LeftTaskID     uuid.UUID `json:"leftTaskId"`
	RightTaskID    uuid.UUID `json:"rightTaskId"`

	LeftStatus   TaskStatus `json:"leftStatus"`
	RightStatus  TaskStatus `json:"rightStatus"`
	LeftAttempt  int        `json:"leftAttempt"`
	RightAttempt int        `json:"rightAttempt"`
	LeftHash     string     `json:"leftHash,omitempty"`
	RightHash    string     `json:"rightHash,omitempty"`

	Verdict   RunDiffVerdict `json:"verdict"`
	HashEqual bool           `json:"hashEqual"`
	Changes   []FieldChange  `json:"changes,omitempty"`
	Degraded  string         `json:"degraded,omitempty"`
}

RunDiffTask is one paired task-name comparison in a RunDiff.

type RunDiffVerdict

type RunDiffVerdict string

RunDiffVerdict is the per-task verdict for an explicit left -> right run comparison.

const (
	// RunDiffVerdictWouldCacheHit means the compared task HashInput blobs carry
	// the same identity hash; relative to the left run, the right run would have
	// cache-hit.
	RunDiffVerdictWouldCacheHit RunDiffVerdict = "WOULD_CACHE_HIT"
	// RunDiffVerdictReran means the compared task HashInput blobs differ; the
	// field changes explain why the right-side task re-ran relative to the left.
	RunDiffVerdictReran RunDiffVerdict = "RERAN"
	// RunDiffVerdictDegraded means the task pair could not be diffed
	// field-by-field because one side's persisted blob is missing or degraded.
	RunDiffVerdictDegraded RunDiffVerdict = "DEGRADED"
)

type RunState

type RunState struct {
	// contains filtered or unexported fields
}

RunState is the run owner's authoritative in-memory DAG state for one run.

It reproduces the advancement semantics of the local executor (internal/job): a completion decrements each successor's outstanding predecessor count, and when a successor reaches zero it either becomes ready (its trigger rule is satisfied by the predecessor outcomes) or is skipped and the skip propagates downstream. The terminal vocabulary and trigger-rule evaluation are shared with the local path (run.IsTerminal, run.SatisfiesTriggerRule) so the two cannot diverge.

Branch selection (a branch task choosing which successors run) is applied by the caller passing the non-selected successor IDs to ApplyCompletion as branchSkipped; resolving branch names to task IDs is the owner integration's responsibility, not this engine's.

RunState is not safe for concurrent use; the owner serializes mutations.

func NewRunState

func NewRunState(topo RunTopology, startSeq int64) *RunState

NewRunState builds a fresh RunState for a run that has not started executing: every task is pending with indegree equal to its predecessor count, and tasks with no predecessors are seeded into the ready queue. startSeq is the terminal_sequence to count up from (0 for a new run; the checkpoint's sequence_high when seeding before replay).

func Restore

func Restore(topo RunTopology, blob []byte) (*RunState, error)

Restore rebuilds a RunState from a checkpoint blob produced by Snapshot, rehydrating the immutable topology from topo (reloaded from the catalog).

func (*RunState) AnyLeaseOverdue

func (rs *RunState) AnyLeaseOverdue(nowMs int64) bool

AnyLeaseOverdue reports whether some in-flight task's dispatch lease has lapsed — or was never recorded — according to the owner's own bookkeeping.

It is the cheap, allocation-free precondition on the owner-side claim reaper: a run with nothing in flight, or with every in-flight lease still fresh, skips the reap query entirely. It is a NECESSARY condition, never a sufficient one. The owner's copy of a lease is set once at dispatch and never renewed, while the worker renews the durable claim_expires_at without telling the owner — so this can say "overdue" about a task that is alive and well (harmless: the query then finds nothing), but it cannot say "fresh" about a task whose durable claim has already lapsed, because both start from the same deadline. A task with no recorded lease at all is treated as overdue so an unbookkept dispatch can never wedge the run.

func (*RunState) ApplyCompletion

func (rs *RunState) ApplyCompletion(taskID uuid.UUID, status TaskStatus, branchSkipped []uuid.UUID) CompletionResult

ApplyCompletion records a worker-reported terminal outcome for taskID and advances the DAG. branchSkipped lists the task's immediate successors that a branch decision excluded (nil for non-branch tasks); they are skipped and the skip propagates. Remaining successors have their predecessor count decremented and, on reaching zero, are pushed ready or skipped per their trigger rule. Returns the sequence stamped on taskID plus the newly ready and skipped tasks, with Applied true.

A completion can be delivered more than once for the same task: a worker re-POSTs the identical /internal/complete envelope when the owner answers 503 after transient dqlite contention, and by then the DAG has already advanced in memory even though the durable write did not land. Such a repeat delivery does not advance anything (Applied false) but still returns the sequence and skips the first delivery decided, so the caller re-persists the *same* terminal rows. Returning a zero sequence here would let the caller stamp terminal_sequence = 0, which recovery's `terminal_sequence > ?` replay filters out — the row would be invisible to a future takeover. Ready is deliberately not replayed: the ready queue lives in this state and the dispatch loop polls it, so a re-delivery must not look like fresh work.

A completion for a task this state does not know, or one made terminal by recovery replay (which adopts the row's stored sequence rather than stamping one), has no recorded effect: the result is not Durable and must not be persisted.

func (*RunState) ApplyExpansion

func (rs *RunState) ApplyExpansion(exp *FanOutExpansion) []SkippedTask

ApplyExpansion materializes fanned successor groups and in-group adjacency.

It rewrites each instance's OutstandingPredecessors in place before materializing it (see rebaseExpansionOutstanding), so the caller's payload — which CompleteTaskOwner persists — carries the same counts this state applies.

func (*RunState) ApplyTerminalRow

func (rs *RunState) ApplyTerminalRow(taskID uuid.UUID, status TaskStatus, storedSeq int64) []uuid.UUID

ApplyTerminalRow applies a terminal task_runs row observed during recovery replay. Unlike ApplyCompletion it does NOT allocate a new sequence (it adopts the row's stored sequence) and does NOT auto-skip unsatisfied successors — every skip was itself persisted as a terminal row and arrives in sequence order, so re-deriving skips here would double-handle them. It sets the task's terminal status, advances the cursor, and pushes any successor whose trigger rule is now satisfied. Returns the newly-ready successors. Idempotent for a task already terminal in the restored snapshot.

func (*RunState) CatalogTaskID

func (rs *RunState) CatalogTaskID(id uuid.UUID) (uuid.UUID, bool)

CatalogTaskID maps a ready/running identity back to its catalog task. ok is false when the identity *is* a catalog task (an unfanned step), which is how callers tell an instance from a plain task without reaching into the state.

func (*RunState) Clone

func (rs *RunState) Clone() *RunState

Clone returns a deep copy of the mutable state: every map, every slice, and every *OwnerTaskState is duplicated, so a mutation applied to the copy is invisible to the original. The topology is shared by reference — it is immutable for the run's lifetime.

It exists for one reason: the owner must not PUBLISH a DAG transition that its durable write then fails to commit. CompleteInstance used to apply ApplyExpansion + ApplyCompletion straight to the authoritative state and only then call CompleteTaskOwner. When that transaction failed, the producer was already terminal in memory, so the worker's redelivery took the already-terminal branch, skipped expansion re-planning, and re-persisted the completion WITHOUT the expansion — while the owner's ready queue held instance ids that exist in no row. Those dispatch forever against a missing row. Staging in a clone and swapping it in only after the commit makes the two converge: either both the rows and the state advanced, or neither did.

func (*RunState) CompletionIdentity

func (rs *RunState) CompletionIdentity(taskID, taskRunID uuid.UUID) uuid.UUID

CompletionIdentity resolves the key THIS state stores for a completion that names both a catalog task and the row that executed it.

The two sides of the wire disagree about identity on purpose, and this is the single place that reconciles them:

  • Dispatch is precise. ReadyForDispatch sets DispatchableTask.TaskRunID only for a real instance and leaves it uuid.Nil for an unfanned step, because that is exactly how this state is keyed — ExpandTask deletes the template node and inserts one node per TaskRunID, and everything else stays keyed by its catalog task id.
  • Completion is not. ownerSink.send stamps req.TaskRunID = taskRun.ID for every route and every task (internal/worker/completion_sink.go), which is right for the SQL fallback — loadTaskRunByIDOrUnique takes a primary key or a catalog id interchangeably — and is what makes a fanned sibling's completion unambiguous.

Preferring TaskRunID unconditionally therefore looked an unfanned task's primary key up in a map keyed by its catalog id, missed, and ApplyCompletion reported Applied=false with sequence 0: nothing persisted, no successor released, and the run hung until its harness timed out. Every job has an unfanned task, so that stalled the entire owner in-memory lane.

Asking the state rather than trusting either side keeps both properties: a fanned instance still resolves to its own row (falling back to the catalog id there would resolve an arbitrary sibling), and an unfanned task resolves to the catalog id this state actually stored. The completions map is consulted too so a RE-DELIVERED completion lands on the same key the first delivery stamped and replays its sequence instead of starting a second one.

func (*RunState) ExpandTask

func (rs *RunState) ExpandTask(taskID uuid.UUID, instances []ExpandedInstance)

ExpandTask replaces the single catalog-keyed entry for taskID with N instance-keyed entries (TaskRun IDs), raising total in the same critical section. Unfanned runs never call this.

func (*RunState) HasFailures

func (rs *RunState) HasFailures() bool

HasFailures reports whether any task reached the failed terminal state — used to decide the run's final status when the DAG completes.

func (*RunState) IsComplete

func (rs *RunState) IsComplete() bool

IsComplete reports whether every task in the run has reached a terminal state.

func (*RunState) MarkDispatched

func (rs *RunState) MarkDispatched(taskID uuid.UUID, claimedBy string, attempt int, leaseExpiresAtMs int64)

MarkDispatched records that a ready task was pushed to a worker: it leaves the ready queue and becomes running with the given claim metadata.

func (*RunState) MarkStarted

func (rs *RunState) MarkStarted(taskID uuid.UUID)

MarkStarted records that a dispatched task is genuinely executing — a container exists for it, so cancelling it can no longer prevent it from running. Idempotent; a no-op for an unknown or terminal task.

Deliberately unwired in production, and the reason is the whole point of SyncStartedFromRows below: the owner never observes a start. MarkDispatched fires when a PEER accepts the push, and the worker that eventually creates the container reports back only when the task finishes, so there is no callback this would hang off. The single reader of Started (cancellableBeforeStart, on the fail_fast branch) syncs from the durable rows immediately before it reads, which is what makes the flag authoritative exactly when it matters. This states the transition directly, for tests and for any future seam that does observe one.

func (*RunState) ReadyTasks

func (rs *RunState) ReadyTasks() []uuid.UUID

ReadyTasks returns a copy of the current ready queue in dispatch order, applying per-group fanOut.maxParallel so in-flight instances never exceed the cap.

func (*RunState) RehydrateInGroupEdges

func (rs *RunState) RehydrateInGroupEdges(rows []models.TaskRun, catalog []models.Task)

RehydrateInGroupEdges rebuilds fan-out maps from durable instance rows. Called on recovery after Restore, before replaying the terminal tail.

catalog is the run's catalog Task rows; they carry the FanOutConfig that fanOut.maxParallel is re-seeded from (the cap lives only in memory, so without this a takeover silently drops it for the rest of the run) and the step names the group-duration metric is labelled with. It may be nil, in which case only the edges and partition keys are rebuilt.

func (*RunState) RequeueExpiredRows

func (rs *RunState) RequeueExpiredRows(rows []models.TaskRun) []uuid.UUID

RequeueExpiredRows returns the tasks named by rows — in-flight rows whose worker claim lease the store just reclaimed — to the ready queue with an incremented attempt, and reports which it actually re-queued in dispatch order.

It is the in-memory half of owner-side claim reaping. A worker that dies mid-task leaves its row `running` with a lapsed claim_expires_at, and the claimer's reaper deliberately skips rows belonging to a LIVE-owned run (the owner is supposed to re-dispatch them). Nothing did: the owner counted the instance in flight forever, so a fanOut.maxParallel group wedged and the run never completed. Rows are matched by instance identity first, falling back to the catalog task id for an unfanned step, exactly as SyncStartedFromRows does. Only tasks this state still believes are running are re-queued, so a row reset underneath a completion that already landed is ignored.

func (*RunState) RunningTasks

func (rs *RunState) RunningTasks() []uuid.UUID

RunningTasks returns the IDs of tasks currently in the running state — used during recovery to identify in-flight work a dead owner had dispatched, which the new owner must re-dispatch.

func (*RunState) Sequence

func (rs *RunState) Sequence() int64

Sequence returns the current terminal_sequence cursor (the highest stamped).

func (*RunState) SetGroupFailurePolicy

func (rs *RunState) SetGroupFailurePolicy(catalogID uuid.UUID, policy string)

SetGroupFailurePolicy records a fanned step's fanOut.failurePolicy. The owner learns it from the catalog (at expansion and again on recovery), never from the checkpoint: two copies of one config can disagree after a partial write, and the catalog row is authoritative.

func (*RunState) Snapshot

func (rs *RunState) Snapshot() ([]byte, error)

Snapshot serializes the mutable run state to a checkpoint blob (JSON in v1). The active-only / incremental size optimizations from the design are layered by the checkpoint writer; this produces a complete, self-contained snapshot.

func (*RunState) SyncStartedFromRows

func (rs *RunState) SyncStartedFromRows(rows []models.TaskRun)

SyncStartedFromRows refreshes the Started flag from durable TaskRun rows.

The owner never observes a start directly: MarkDispatched is called when a peer ACCEPTS the push, and the worker that eventually creates the container reports back only when the task finishes. The row's runtime_id is the durable record of "a container exists" (StartTask / StartTaskClaimed write it with the engine's atom id, and only after Create returned), so it is the marker both lanes agree on — deliberately not started_at, which ClaimTaskForDispatch stamps at CLAIM time and which therefore cannot tell a queued task from a running one on the owner-push path.

Callers sync immediately before a decision that depends on the distinction (fail_fast cancellation), so the flag is authoritative exactly when it is read rather than continuously. Rows are matched by instance identity first, falling back to the catalog task id for an unfanned step, mirroring how this state keys its nodes.

func (*RunState) TakeResolvedGroups

func (rs *RunState) TakeResolvedGroups() []ResolvedGroup

TakeResolvedGroups returns every fan-out group that has become fully terminal and has not been reported before, in deterministic dispatch order. It is the owner engine's observation point for caesium_fanout_group_duration_seconds: the caller records the duration and this state never reports that group again.

func (*RunState) TaskState

func (rs *RunState) TaskState(id uuid.UUID) (OwnerTaskState, bool)

TaskState returns a copy of a task's current state, or false if unknown.

type RunTopology

type RunTopology struct {
	Adjacency    map[uuid.UUID][]uuid.UUID // task -> direct successors
	Predecessors map[uuid.UUID][]uuid.UUID // task -> direct predecessors
	TriggerRule  map[uuid.UUID]string      // task -> trigger rule ("" = all_success)
	Order        map[uuid.UUID]int         // task -> deterministic dispatch order
}

RunTopology is the immutable DAG shape for a run, loaded once at construction. Every task ID must appear as a key in Order (it is the authoritative task set); Adjacency/Predecessors/TriggerRule may omit tasks with no edges/rule.

type SkippedTask

type SkippedTask struct {
	TaskID           uuid.UUID `json:"task_id"`
	TerminalSequence int64     `json:"terminal_sequence"`
	Reason           string    `json:"reason,omitempty"`
}

SkippedTask records a task the owner transitioned to skipped (a DAG decision, never reported by a worker) along with the terminal_sequence stamped on it.

type StartOption

type StartOption func(*StartOptions)

func WithStartParams

func WithStartParams(params map[string]string) StartOption

func WithStartPriority

func WithStartPriority(priority string) StartOption

type StartOptions

type StartOptions struct {
	Params   map[string]string
	Priority string
}

type StartParamsEnricher

type StartParamsEnricher func(ctx context.Context, db *gorm.DB, jobID uuid.UUID, params map[string]string, fromQueue bool) (map[string]string, error)

StartParamsEnricher augments the params a run is created with. It is invoked inside the run-creation path, BEFORE the job_runs row is built and inserted, so whatever it records is written atomically with the row.

That timing is the whole point of the seam: a subsystem that instead observes run_started asynchronously reads its view after the run is already executing (and reads nothing at all if the non-blocking bus drops the event), so it can credit a run with state the run never had. Returning the enriched map from here makes the view part of the run record itself.

It runs on every run creation, so it must be cheap, and it must not mutate the map it is handed.

db is the handle this store creates runs on, and every read the enricher makes MUST go through it. That is not a convenience: a run can be created by a store built over an open transaction (internal/trigger/event/router.go passes its tx), and a read issued on any other connection while that transaction is open deadlocks the database until the request's context is cancelled.

fromQueue reports that this creation is the PROMOTION of a run that was already admitted once and parked in run_queue. A queue-strategy run is enriched twice — at enqueue, whose params ride the run_queue row, and here, where it actually starts — so an enricher whose value is a point-in-time observation must re-take it when this is set, or it records a view the run had while it was still waiting rather than the one it began with.

On failure an enricher returns an error and, optionally, a corrected map. That second return is not decoration: params are handed down between runs — off the run_queue row on a promotion, off the retried run's own row on a retry — so they may already carry a param this enricher wrote for a DIFFERENT run, which a failed re-read has just left it unable to confirm. Returning the params with that param REMOVED is how an enricher retracts a value it can no longer stand behind; returning nil means "nothing to correct, use the caller's params".

type Status

type Status string
const (
	StatusRunning   Status = "running"
	StatusSucceeded Status = "succeeded"
	StatusFailed    Status = "failed"
	StatusCancelled Status = Status(models.JobRunStatusCancelled)
)

type Store

type Store struct {
	// contains filtered or unexported fields
}

func Default

func Default() *Store

func NewStore

func NewStore(conn *gorm.DB) *Store

func (*Store) AbandonPartitionRetries

func (s *Store) AbandonPartitionRetries(runID uuid.UUID, taskRunIDs []uuid.UUID, reason string) (int, error)

AbandonPartitionRetries resolves the given retry-reset instances, if still pending and unstarted, as skipped with the given reason and clears their retry provenance. It is the bounded end of the completion fence: a replacement engine that could not dispatch the retries it was started for must not spawn yet another engine for them. Each row is resolved explicitly so the run can finalize and the operator can see why.

The guard lives in the UPDATE itself, not in a prior read: an executor can claim and start one of these rows at any moment, and a skip that reached a running instance would clear its provenance and let the run finalize under a container that keeps producing side effects. Returns the number of instances actually resolved.

func (*Store) AbandonPendingPartitionRetries

func (s *Store) AbandonPendingPartitionRetries(runID uuid.UUID, reason string) (int, error)

AbandonPendingPartitionRetries is AbandonPartitionRetries over every retry-reset instance of the run that is still pending. It is for an engine that failed before it could execute anything, where no retry can be told apart from another.

func (*Store) AdmitRun

func (s *Store) AdmitRun(jobID uuid.UUID, triggerID *uuid.UUID, opts ...StartOption) (*JobRun, bool, error)

func (*Store) AdoptStartedRun

func (s *Store) AdoptStartedRun(runID uuid.UUID)

AdoptStartedRun records that this process should clear active-run bookkeeping when Complete sees runID. It is used when a run is created transactionally by a short-lived store instance but executed by the default runtime store.

func (*Store) Bus

func (s *Store) Bus() event.Bus

func (*Store) CacheHitTask

func (s *Store) CacheHitTask(runID, taskID uuid.UUID, source CacheHitSource, result string, output map[string]string, branchSelections []string) (*CompleteTaskResult, error)

CacheHitTask marks a task as completed via cache hit (local mode). It mirrors the CompleteTaskWithResult flow but sets status to "cached".

func (*Store) CacheHitTaskClaimed

func (s *Store) CacheHitTaskClaimed(runID, taskID uuid.UUID, source CacheHitSource, result, claimedBy string, output map[string]string, branchSelections []string) error

CacheHitTaskClaimed marks a claimed task as completed via cache hit (distributed mode).

func (*Store) CacheHitTaskClaimedWithPartitions

func (s *Store) CacheHitTaskClaimedWithPartitions(runID, taskID uuid.UUID, source CacheHitSource, result, claimedBy string, output map[string]string, branchSelections []string, partitions []pkgtask.Partition) error

CacheHitTaskClaimedWithPartitions is the claim-fenced twin of CacheHitTaskWithPartitions, and the method internal/worker/completion_sink.go and internal/dispatch/dispatch.go resolve by INTERFACE ASSERTION (cacheHitPartitionStore). Because the binding is an assertion rather than a compile-time call, a signature drift here does not fail the build — it makes the assertion miss, which those sites report as "run store cannot persist producer partitions; fan-out group will not expand" and then continue without expanding. Keep the signature in lockstep with those declarations.

func (*Store) CacheHitTaskWithPartitions

func (s *Store) CacheHitTaskWithPartitions(runID, taskID uuid.UUID, source CacheHitSource, result string, output map[string]string, branchSelections []string, partitions []pkgtask.Partition) (*CompleteTaskResult, error)

CacheHitTaskWithPartitions is CacheHitTask plus the producer's parsed partition list, expanded inside the SAME transaction that writes the cached terminal row.

A cache hit is a completion, and cacheHitTask is a different function from completeTask — so an expansion hook placed only on the completion route is unreachable whenever a producer's own work cache-hits, and the group silently collapses to its single template row. That is the *common* path, not an edge case: with per-unit fingerprints the whole point is that repeated work hits the cache, and a cached producer still replays its partition list out of the cache entry (internal/worker/runtime_executor.go reads entry.Partitions).

The expansion runs the identical rules as the completion route — it calls expandFanOutSuccessorsTx, so validation (cycles, dangling dependsOn keys, caps), onEmpty handling, in-group indegree seeding and producer-list persistence are one implementation, not a second copy that can drift.

func (*Store) CancelQueuedRun

func (s *Store) CancelQueuedRun(ctx context.Context, jobID, queueID uuid.UUID) error

func (*Store) CancelRun

func (s *Store) CancelRun(ctx context.Context, runID uuid.UUID) error

func (*Store) CheckpointDeltasSince

func (s *Store) CheckpointDeltasSince(runID uuid.UUID, fromSeq int64) ([]models.RunCheckpoint, error)

CheckpointDeltasSince returns the incremental checkpoints with sequence_high strictly greater than fromSeq, ascending, so recovery can apply them in order after the most recent full snapshot. (v1 writes only full snapshots, so this normally returns empty; it exists so the recovery path is delta-ready.)

func (*Store) ClaimTaskForDispatch

func (s *Store) ClaimTaskForDispatch(runID, taskID uuid.UUID, workerNode string, ownerGeneration int64, leaseTTL time.Duration, trustOwnerReadiness bool) error

ClaimTaskForDispatch is the Phase 2 dispatch-side equivalent of ClaimNext for a specific task. It atomically transitions a pending task from (status=pending, claimed_by="") → (status=running, claimed_by=workerNode) in a single UPDATE, mirroring what ClaimNext does but targeting a known task rather than picking the next available one.

The ownerGeneration argument is stamped onto owner_generation so subsequent coordination writes can fence against a stale owner. The WHERE clause includes `AND owner_generation <= ?` to encode the monotonic-generation invariant: a row last touched by the current owner or any *older* generation is claimable (this covers pre-Phase-2A rows at implicit generation 0, normal re-claims at the same generation, and — critically — failover, where a new owner at generation N+1 must re-claim an in-flight task its predecessor stamped at generation N). A row stamped by a *newer* generation means the claimer is itself stale, so the claim is rejected.

Returns ErrTaskClaimMismatch if the task was not in the expected state (already claimed, wrong status, wrong run, stale generation). The caller should fall back to writing the task with claimed_by="" and letting ClaimNext pick it up.

func (*Store) ClaimedTaskRunIDs

func (s *Store) ClaimedTaskRunIDs(ctx context.Context, nodeID string, ids []uuid.UUID) ([]uuid.UUID, error)

ClaimedTaskRunIDs returns the subset of ids whose task_run row is STILL claimed by nodeID. It is the read half of claim-loss detection: the worker asks "of the tasks I am executing, which do I still own?" and stops the containers of the rest.

A read rather than the batched RenewLeases UPDATE, because the two answer different questions on different clocks. Renewal is due only when a claim is within lease_ttl/2 of expiry, and the OWNER's push path stamps claim_expires_at from its dispatch deadline (internal/dispatch/dispatch.go, CAESIUM_RUN_OWNER_DISPATCH_DEADLINE, 5m by default) rather than from CAESIUM_WORKER_LEASE_TTL — so on a run-owner lane a freshly claimed task is not renewal-due for minutes, and a worker that only learned about lost claims from RenewLeases' RowsAffected learned about them minutes late. A cancelled run's container must not outlive the cancel by the renewal cadence, so the question is asked on every tick with a single indexed SELECT instead.

The two ways a claim is lost both surface here: cancelRunTx blanks claimed_by on every non-terminal row of a cancelled run, and a reclaim after lease expiry overwrites claimed_by with the new owner's node id.

func (*Store) Complete

func (s *Store) Complete(runID uuid.UUID, result error) error

func (*Store) CompleteIfActive

func (s *Store) CompleteIfActive(runID uuid.UUID, result error) (bool, error)

CompleteIfActive is Complete that also reports whether THIS call finalized the run: false when the run was already terminal (the idempotent no-op). Callers that dispatch completion callbacks on their own — an engine finalizing a resume that failed before its normal completion path — need the distinction so a run another path finalized first is not notified twice.

func (*Store) CompleteTask

func (s *Store) CompleteTask(runID, taskID uuid.UUID, result string, output map[string]string, branchSelections []string) error

func (*Store) CompleteTaskClaimed

func (s *Store) CompleteTaskClaimed(runID, taskID uuid.UUID, result, claimedBy string, output map[string]string, branchSelections []string) error

func (*Store) CompleteTaskClaimedWithPartitions

func (s *Store) CompleteTaskClaimedWithPartitions(runID, taskID uuid.UUID, result, claimedBy string, output map[string]string, branchSelections []string, partitions []pkgtask.Partition) error

func (*Store) CompleteTaskInstance

func (s *Store) CompleteTaskInstance(taskRunID uuid.UUID, result string, output map[string]string, branchSelections []string, partitions []pkgtask.Partition) (*CompleteTaskResult, error)

CompleteTaskInstance completes a specific TaskRun (fan-out instance) by primary key.

func (*Store) CompleteTaskOwner

func (s *Store) CompleteTaskOwner(
	runID, taskRef uuid.UUID,
	status TaskStatus,
	result, errMsg, claimedBy string,
	output map[string]string,
	branchSelections []string,
	completedSeq, ownerGen int64,
	skips []SkippedTask,
	expansion *FanOutExpansion,
) error

CompleteTaskOwner is the run-owner in-memory path's durable terminal write. The owner has already advanced the DAG in memory (run.RunState), so this only persists terminal rows — it does NOT decrement predecessors, evaluate trigger rules, or resolve branches in SQL. It writes the completed task's terminal row (succeeded/failed/cached) plus each owner-decided skip, stamping terminal_sequence and owner_generation so a recovering owner can replay in order. Claim-fenced by claimedBy.

Cache hits DO travel this path: a cache hit is a completion, and under per-partition fingerprints a cache-hit prerequisite is the common case in an ordered group, so the owner's Cached sink carries its TaskRunID through here like any other terminal transition. (An earlier docstring claimed cache hits stayed on CacheHitTaskClaimed; that stopped being true when the owner sink gained instance identity.)

func (*Store) CompleteTaskWithPartitions

func (s *Store) CompleteTaskWithPartitions(runID, taskID uuid.UUID, result string, output map[string]string, branchSelections []string, partitions []pkgtask.Partition) (*CompleteTaskResult, error)

CompleteTaskWithPartitions is CompleteTaskWithResult plus the producer's parsed partition list, which is expanded inside the completion transaction.

func (*Store) CompleteTaskWithResult

func (s *Store) CompleteTaskWithResult(runID, taskID uuid.UUID, result string, output map[string]string, branchSelections []string) (*CompleteTaskResult, error)

CompleteTaskWithResult completes a task and returns details about branch skips so the local executor can update its in-memory state.

func (*Store) CountActive

func (s *Store) CountActive(jobID uuid.UUID) (int64, error)

func (*Store) DB

func (s *Store) DB() *gorm.DB

func (*Store) DeleteCheckpoints

func (s *Store) DeleteCheckpoints(runID uuid.UUID) error

DeleteCheckpoints removes all checkpoints for a run, called when a terminal run is archived (the durable task_runs rows remain the system of record).

func (*Store) DeleteQueuedRun

func (s *Store) DeleteQueuedRun(ctx context.Context, queued *models.RunQueue) error

func (*Store) DequeueNextRun

func (s *Store) DequeueNextRun(ctx context.Context, jobID uuid.UUID, claimedBy string) (*models.RunQueue, error)

func (*Store) DiffRuns

func (s *Store) DiffRuns(ctx context.Context, jobID, leftRunID, rightRunID uuid.UUID) (*RunDiff, error)

DiffRuns compares the latest terminal task-runs in rightRunID against leftRunID for one job. It pairs rows by task name, diffs each pair's persisted HashInput blob via DiffHashInputBlobs, and returns a JSON-ready read model for API/CLI layers to render.

Pairing considers terminal task-runs only (Succeeded/Failed/Skipped/Cached). A task with no terminal task-run on one side is treated as absent there and reported in TasksAdded/TasksRemoved; this read model is intended for two completed runs, where every task has a terminal attempt.

func (*Store) EnsureTaskRunStartable

func (s *Store) EnsureTaskRunStartable(runID, taskRef uuid.UUID, claimedBy string) error

EnsureTaskRunStartable reports whether a claimed task may still be started, returning ErrTaskClaimMismatch when it may not.

A worker holds its claimed task for as long as its pool takes to free a slot, and in that window the row can be resolved out from under it — fail_fast cancelling a sibling of a group that has already failed is the case this exists for (markInstanceCancelledBeforeStartTx revokes the claim as part of the cancel). The executor calls this immediately before creating the container, because engine.Create both creates AND starts it: checking only afterwards, in StartTaskClaimed, would mean the cancelled work had already run.

Two conditions, either of which means the task is no longer this worker's to start: the row reached a terminal status, or its claim now belongs to someone else (including "" after a release). The check is advisory by nature — it is not the same transaction as the container create — so StartTaskClaimed's guarded UPDATE remains the authoritative fence; this only keeps a doomed container from being created in the first place.

func (*Store) EventStore

func (s *Store) EventStore() *event.Store

func (*Store) FailTask

func (s *Store) FailTask(runID, taskID uuid.UUID, failure error) error

func (*Store) FailTaskClaimed

func (s *Store) FailTaskClaimed(runID, taskID uuid.UUID, failure error, claimedBy string) error

func (*Store) FailTaskInstance

func (s *Store) FailTaskInstance(runID, taskRunID uuid.UUID, failure error) error

FailTaskInstance marks exactly one TaskRun row failed by its primary key, runs the transitive in-group skip cascade, and advances the group's cross-step successors once every sibling is terminal.

It is now a thin alias for failTask: that function's taskRef parameter follows the TaskRun-primary-key-or-catalog-task-ID contract and is no longer reassigned mid-flight, so passing an instance's primary key addresses that row and nothing else. This wrapper survives as the name that says "by instance", and for its nil-id guard.

func (*Store) FanOutInstanceIdentities

func (s *Store) FanOutInstanceIdentities(ctx context.Context, runID, taskID uuid.UUID) ([]InstanceIdentity, error)

FanOutInstanceIdentities returns every instance row of (runID, taskID) in partition-index order with its persisted output and effective identity hash.

Ordering is load-bearing: GroupIdentityHash folds its input in partition-index order, so a caller may pass the successful entries straight through.

func (*Store) FindRunning

func (s *Store) FindRunning(jobID uuid.UUID) (*JobRun, error)

func (*Store) Get

func (s *Store) Get(runID uuid.UUID) (*JobRun, error)

func (*Store) GetTaskLogSnapshot

func (s *Store) GetTaskLogSnapshot(runID, taskID uuid.UUID) (*TaskLogSnapshot, error)

func (*Store) HasAnyFanOutConsumerForRun

func (s *Store) HasAnyFanOutConsumerForRun(runID uuid.UUID) (bool, error)

HasAnyFanOutConsumerForRun reports whether ANY task in runID's job declares a fanOut config at all. It is the cheap pre-filter HasFanOutSuccessor's callers (internal/job/job.go, internal/worker/runtime_executor.go) run first: on an ordinary run that never uses fan-out — the overwhelming majority of cache hits — this single indexed join is the ENTIRE cost of the F7 cache-hit gate, and HasFanOutSuccessor's fuller per-producer check never runs at all.

func (*Store) HasFanOutSuccessor

func (s *Store) HasFanOutSuccessor(runID, producerTaskID uuid.UUID) (bool, error)

func (*Store) InvalidateRunCheckpoints

func (s *Store) InvalidateRunCheckpoints(runID uuid.UUID) error

InvalidateRunCheckpoints drops every owner checkpoint of a run outside a retry transaction. The in-memory owner uses it when the store refused its completion for a pending per-partition retry: the snapshots it wrote since that retry committed describe a run with nothing left to do, and a recovery that restored one would never discover the reset instance.

func (*Store) Latest

func (s *Store) Latest(jobID uuid.UUID) (*JobRun, error)

func (*Store) LatestFullCheckpoint

func (s *Store) LatestFullCheckpoint(runID uuid.UUID) (*models.RunCheckpoint, error)

LatestFullCheckpoint returns the highest-sequence full (non-incremental) snapshot for runID, or (nil, nil) when the run has no checkpoint yet.

func (*Store) LatestSuccessfulCronRun

func (s *Store) LatestSuccessfulCronRun(jobID uuid.UUID) (*JobRun, error)

LatestSuccessfulCronRun returns the most recent cron-triggered run for a job that completed with status "succeeded". It returns gorm.ErrRecordNotFound when no such run exists.

func (*Store) LeaseStore

func (s *Store) LeaseStore() *LeaseStore

LeaseStore returns the run lease store, or nil when owner mode is disabled.

func (*Store) List

func (s *Store) List(jobID uuid.UUID) ([]*JobRun, error)

func (*Store) LoadDispatchedTaskRun

func (s *Store) LoadDispatchedTaskRun(runID, taskID uuid.UUID, claimedBy string) (*models.TaskRun, error)

LoadDispatchedTaskRun loads the full task_runs row for a task that was just claimed for dispatch. The (claimedBy, status=running) predicate ensures the row really is the one this node claimed via ClaimTaskForDispatch and not a row another node has since reclaimed. Returns ErrTaskClaimMismatch if no matching running row exists. The dispatch handler uses this to obtain the full execution spec (image/command/engine/etc.) to hand to the worker pool.

func (*Store) LoadRunTopology

func (s *Store) LoadRunTopology(runID uuid.UUID) (RunTopology, error)

LoadRunTopology reads a run's immutable DAG shape and builds the RunTopology the owner's in-memory RunState needs. Normal runs use the live job catalog; quarantined replay runs use the per-task execution descriptors captured on the TaskRun rows so a later apply cannot change replay dispatch order.

The two sources differ ONLY in how they enumerate nodes and edges; both feed the same buildRunTopology.

func (*Store) PendingPartitionRetries

func (s *Store) PendingPartitionRetries(runID uuid.UUID) ([]models.TaskRun, error)

PendingPartitionRetries lists the retry-reset instances of a run that are still pending — exactly the rows Complete's fence refuses on. Only id and task_id are populated.

func (*Store) PendingTasksForDispatch

func (s *Store) PendingTasksForDispatch(ctx context.Context, runID uuid.UUID, limit int) ([]models.TaskRun, error)

PendingTasksForDispatch returns up to limit task_runs rows for runID that are ready for owner-push dispatch: status=pending, claimed_by="", and outstanding_predecessors=0. The caller (the dispatch loop) uses this to find the next batch of tasks to push to workers each tick.

The result is ordered by created_at ASC so earlier-registered tasks are dispatched first, preserving FIFO ordering within a run. The limit cap prevents a huge fan-out from saturating a single tick.

func (*Store) PlanFanOutExpansion

func (s *Store) PlanFanOutExpansion(runID, producerTaskID uuid.UUID, partitions []pkgtask.Partition) (*FanOutExpansion, error)

PlanFanOutExpansion validates partitions and assigns instance IDs without writing rows. The owner in-memory path applies this to RunState first, then persists the same IDs inside CompleteTaskOwner.

The producer is addressed by its catalog task id, which resolves its TaskRun only while that task is UNFANNED. Use PlanFanOutExpansionForRow when the producer may itself be a fanned instance.

func (*Store) PlanFanOutExpansionForRow

func (s *Store) PlanFanOutExpansionForRow(runID, producerTaskID, producerRowRef uuid.UUID, partitions []pkgtask.Partition) (*FanOutExpansion, error)

PlanFanOutExpansionForRow is PlanFanOutExpansion with the producer's two identities stated separately, because the function genuinely needs both: producerTaskID is the CATALOG task (the Task row whose name fanOut.from matches, and the from_task_id the successor edges hang off), while producerRowRef names the TaskRun that actually ran.

Collapsing them broke every fanned producer. A fanned step that emits partitions of its own — and, more commonly, ANY instance completing through OwnerManager.CompleteInstance, which plans an expansion on every success — resolves its catalog id to N rows, so the single-row load failed with ErrAmbiguousTaskRun. The owner treats a planning error as the task having FAILED, so a partition that succeeded was recorded failed, and under the default fail_fast policy that killed its pending siblings and the run. The error surfaced only as a `record not found` line in the query log.

producerRowRef follows the usual TaskRun-primary-key-or-catalog-task-ID contract, so an unfanned producer may still pass its catalog id.

func (*Store) PredecessorDescriptorInputs

func (s *Store) PredecessorDescriptorInputs(runID, taskID uuid.UUID) (map[uuid.UUID]map[string]string, map[uuid.UUID]string, error)

PredecessorDescriptorInputs returns predecessor outputs and effective hashes keyed by predecessor task id for immutable execution-descriptor capture.

Group-aware for the same reason PredecessorOutputs and PredecessorHashes are: the descriptor records the inputs a task actually consumed, so it must record the same aggregate the cache key was computed from. Unfanned predecessors are byte-identical to the pre-fan-out behavior (one row, its own output map, its own effective hash).

func (*Store) PredecessorHashes

func (s *Store) PredecessorHashes(runID, taskID uuid.UUID) ([]string, error)

PredecessorHashes returns the execution hashes recorded on predecessor task runs that completed successfully in the current run. This keeps distributed cache hashing aligned with local execution, including transitive cache hits.

The hash returned per predecessor is its EFFECTIVE identity: effective_hash when a value-verified short-circuit was proven for that predecessor (its code changed but it produced byte-identical output, see cache.EquivalentPriorHash and TaskRun.EffectiveHash), otherwise its own hash. Reading the effective hash is what stops a no-op upstream change from cascading a re-run downstream: the predecessor presents its prior, proven-equivalent identity, so a downstream whose only changed input was this predecessor sees an unchanged hash and cache-hits. Falling back to hash (effective_hash empty) is the common case and is byte-identical to the pre-D2 behavior.

func (*Store) PredecessorOutputs

func (s *Store) PredecessorOutputs(runID, taskID uuid.UUID) (map[string]map[string]string, error)

PredecessorOutputs returns a map of step-name → output key-values for all predecessors of the given task within a run. This is used by the distributed executor to inject CAESIUM_OUTPUT_* env vars before starting a task. PredecessorOutputs returns each predecessor step's outputs keyed by step name. One entry per PREDECESSOR (never per row): a fanned predecessor contributes the group aggregate, see predecessorGroupOutput.

func (*Store) PruneCheckpoints

func (s *Store) PruneCheckpoints(runID uuid.UUID, keepFulls int) error

PruneCheckpoints retains the most recent keepFulls full snapshots for runID (and every checkpoint at or after the oldest retained full's sequence), deleting anything older. A no-op until more than keepFulls fulls exist.

func (*Store) PublishEvents

func (s *Store) PublishEvents(events ...event.Event)

func (*Store) RateLimitTask

func (s *Store) RateLimitTask(ctx context.Context, runID, taskRef uuid.UUID, retryAfter time.Time) error

RateLimitTask leaves a task pending until retryAfter so rate-limit rejections do not hold worker capacity or spin through immediate reclaims.

taskRef follows the TaskRun-primary-key-or-catalog-task-ID contract, so a fan-out instance parks by its own TaskRun ID and its siblings keep running.

G1 open question — settled here: the `status IN (pending, running)` predicate is KEPT, and it is deliberate rather than a leftover. Both claim paths flip a row to `running` at CLAIM time (ClaimTaskForDispatch, and the claimer's atomic UPDATE), and the rate-limit rejection is only discovered afterwards, before any container exists — so the row this parks is legitimately `running` and has nothing in flight to orphan. What made the predicate dangerous was never the status set; it was the `WHERE job_run_id = ? AND task_id = ?` predicate, which re-pended every RUNNING sibling of a fanned group and orphaned their live containers. Now that the write is keyed to one instance's primary key, matching `running` affects exactly the row whose own rate-limit acquisition was rejected. Callers must therefore pass the instance's TaskRun ID; passing a catalog task ID for an expanded group now fails loudly with ErrAmbiguousTaskRun instead of silently parking a sibling. RateLimitTask parks ONE task instance until retryAfter, releasing its claim so the next tick can re-acquire the rate-limit token.

taskRef follows the TaskRun-primary-key-or-catalog-task-ID contract; a fanned group must be addressed by instance, and a catalog task ID naming N siblings is refused (ErrAmbiguousTaskRun) rather than parking an arbitrary one.

The `status IN (pending, running)` predicate was G1's one open question — it is KEPT, deliberately. Matching `running` looks like it could re-pend a live instance and orphan its container, but the rate-limit rejection is discovered AFTER the claim has already flipped the row to running and BEFORE any container exists (acquireTaskRateLimit runs at dispatch, not mid-execution), so the running row this matches has nothing in flight. What was a real bug is now closed by the re-key: the old (job_run_id, task_id) predicate fanned the re-pend across every sibling, so parking one instance re-pended its RUNNING siblings and did orphan their containers. Pinned by TestRateLimitTaskParksOneInstance.

func (*Store) ReclaimOwnerExpiredClaims

func (s *Store) ReclaimOwnerExpiredClaims(runID uuid.UUID, ownerGeneration int64) ([]models.TaskRun, error)

ReclaimOwnerExpiredClaims returns this run's in-flight rows whose worker claim lease has lapsed to the dispatchable pending state and reports the rows it reset.

It is the OWNER's half of expired-claim reaping, and it exists because the worker-side reaper deliberately declines to do it. Claimer.ReclaimExpired guards on `NOT EXISTS (... run_leases ... lease_expires_at > now)`: a task belonging to a live-owned run is left alone so the reaper can never race the owner's dispatch loop into double-executing it. In run-owner in-memory mode nothing then completed the thought — a worker that died mid-task left its row `running` with a dead claim, the owner counted it in flight forever, and a fanOut.maxParallel group wedged behind a slot that never came back. This runs under the owner's own per-run lock, so it is the one implementation of the reset and cannot race the loop it serves.

Fencing mirrors ClaimTaskForDispatch's: `owner_generation <= ownerGeneration` accepts rows this owner stamped and legacy generation-0 rows, and rejects rows a NEWER owner has already taken over — a lease this node lost must not have its claims reset from under the node that now holds it. The reset columns are exactly Claimer.ReclaimExpired's, including leaving `attempt` alone: the retry-policy attempt counter is not what a lease expiry consumes (the owner's in-memory attempt is bumped by RunState.RequeueExpiredRows for dispatch bookkeeping).

func (*Store) RecordEventTx

func (s *Store) RecordEventTx(tx *gorm.DB, evt *event.Event) error

func (*Store) RegisterTask

func (s *Store) RegisterTask(runID uuid.UUID, task *models.Task, atom *models.Atom, outstanding int) error

func (*Store) RegisterTasks

func (s *Store) RegisterTasks(runID uuid.UUID, inputs []RegisterTaskInput) error

func (*Store) ReleaseQueuedRun

func (s *Store) ReleaseQueuedRun(ctx context.Context, queueID uuid.UUID, claimedBy string) error

func (*Store) ReleaseTaskClaim

func (s *Store) ReleaseTaskClaim(runID, taskID uuid.UUID, claimedBy string, ownerGeneration int64) error

ReleaseTaskClaim reverts a task this node claimed for dispatch back to the dispatchable pending state (status=running → pending, claimed_by="", claim_expires_at=nil, runtime_id="", started_at=nil). It is the rollback used by HandleDispatch when the local worker cannot accept a just-claimed task (buffer full / worker not running): rather than leave the task claimed-but-orphaned, the owner returns it to the pool so the next dispatch tick re-dispatches it (to this or another peer).

The owner_generation predicate keeps the release fenced: only the owner that stamped the row (or a legacy generation-0 row) can release it. The status and claimed_by predicates make the release a no-op (zero rows, no error) if the task already advanced — e.g. a completion landed in the race window.

func (*Store) RenewLeases

func (s *Store) RenewLeases(ctx context.Context, nodeID string, ids []uuid.UUID, newExpiresAt time.Time) (int64, error)

RenewLeases extends claim_expires_at for all task runs identified by ids that are still claimed by nodeID. The WHERE clause on claimed_by ensures that any task whose claim was reassigned after expiry is not accidentally extended. An empty ids slice is a no-op (no database round-trip). Returns the number of rows actually updated so callers can credit metrics accurately and detect the case where a claim was reassigned between the renewal decision and the write.

func (*Store) ResetInFlightTasks

func (s *Store) ResetInFlightTasks(runID uuid.UUID) error

func (*Store) ResolveBranchSkips

func (s *Store) ResolveBranchSkips(runID, taskID uuid.UUID, branchSelections []string) ([]uuid.UUID, error)

ResolveBranchSkips returns the immediate successor task IDs a branch task excluded at runtime via its branch selections — i.e. the successors to skip. It returns nil for non-branch tasks (which skip nothing here). It errors if a selection names a step that is not a valid successor, matching completeTask's validation.

G7: live and replay differ ONLY in how they enumerate a branch's successors, so that is the one thing that forks (branchTargetsTx) and the selection algorithm below exists once. Previously both halves carried their own copy of "build the name map, validate each selection, invert the selection into skips", which is the shape where a fix lands on one path and quietly does not on the other.

func (*Store) RetryFromFailure

func (s *Store) RetryFromFailure(runID uuid.UUID) (*JobRun, error)

RetryFromFailure resets a failed run so that previously-succeeded and cached tasks are preserved and only failed/pending/skipped tasks are re-executed. This is the manual/human retry entry point (caesium run retry); it performs no concurrency re-admission and does not consult Job.Paused — a human retrying a paused job's run is a deliberate human decision.

func (*Store) RetryFromFailureAdmitted

func (s *Store) RetryFromFailureAdmitted(runID uuid.UUID) (*JobRun, error)

RetryFromFailureAdmitted is the admit-aware retry entry point used by the incident action executor's retry actions (retry_from_failure, snooze_retry, retry_callbacks re-run). It adds the two safety valves the plain RetryFromFailure store call lacks (design-agent-in-the-loop.md):

  1. It refuses while the job is Paused (returns ErrJobPaused) — a human pause outranks an agent retry.
  2. It re-admits against metadata.concurrency using QUEUE semantics regardless of the job's declared strategy: the run is flipped back to running only if a concurrency slot is free (returns ErrMaxConcurrentRunsReached otherwise). An agent retry must never replace-cancel a live run, nor race the next cron tick for a slot admission never granted.

func (*Store) RetryPartition

func (s *Store) RetryPartition(ctx context.Context, runID, taskRunID uuid.UUID) (*TaskRun, bool, error)

RetryPartition resets ONE fan-out instance of a run for re-execution.

It is the store half of `POST …/tasks/:task_id/partitions/:index/retry`. The controller previously did a bare `db.Model(&row).Updates(...)`: no transaction, no terminal-only guard (so a RUNNING instance could be reset mid-flight, orphaning its container), no reset of claimed_by / started_at / runtime_id / claim_expires_at / the cache columns, no outstanding_predecessors re-seed (so an ordered instance came back ready even though its in-group dependency had also been reset), no run re-open (so retrying an instance of an already-terminal run left the run terminal and nothing ever dispatched it), and no event.

Semantics, mirroring retryFromFailure for exactly one row:

  • terminal instances only (ErrTaskRunNotTerminal otherwise)
  • the full retryResetColumns reset
  • outstanding_predecessors re-seeded over NON-TERMINAL dependencies only
  • the run re-opened when it had already finished
  • checkpoints invalidated so owner in-memory mode sees the reset
  • a task_ready event when the instance is immediately dispatchable

The group is deliberately NOT re-expanded (the producer is terminal and the recorded instances are reused), and dependents that already succeeded are NOT cascaded — E2 requires the CLI to say so rather than silently re-running a subtree.

The bool is true iff this transaction reopened a terminal run. Callers that start an in-process engine (the HTTP handler) MUST use this flag rather than a pre-tx status snapshot: a running local run can complete after that read and be reopened here, and kicking off from the stale "running" status would leave the reset instance pending forever. If the tx still saw the run running it does not reopen — the in-process loop is alive — and a second Run() would race it.

The returned TaskRun is loaded inside that same transaction after the reset. A post-commit refresh must not be the source of the kickoff signal: if it failed, returning (nil, false, err) would discard a committed reopen and the handler would skip kickoff.

func (*Store) RetryTask

func (s *Store) RetryTask(runID, taskRef uuid.UUID, attempt int) error

RetryTask resets a failed task run back to pending and increments its Attempt counter. It is the LOCAL lane's in-run retry: internal/job calls it for each `retries:` attempt while it drives the DAG itself.

There is deliberately no claim-fenced twin. RetryTaskClaimed used to be one and was unusable: it re-pends the row, while StartTaskClaimed only starts a row whose status is `running`, so the attempt that followed a claimed retry could never start. The distributed lane uses RetryTaskClaimedInstance (internal/run/store_instance.go), which keeps the row `running` under the same claim — the truthful state for a worker that never released the task and is about to launch the next container itself.

func (*Store) RetryTaskClaimedInstance

func (s *Store) RetryTaskClaimedInstance(runID, taskRunID uuid.UUID, attempt int, claimedBy string) error

RetryTaskClaimedInstance resets one instance for its next attempt WITHOUT releasing it, for a worker that is retrying a task it still holds.

It differs from RetryTaskClaimed in the one respect that made that method unusable: RetryTaskClaimed re-pends the row, and StartTaskClaimed will only start a row whose status is `running`, so the attempt that followed a retry could never start — it hit ErrTaskClaimMismatch, tore its container down and abandoned the task mid-budget, whatever the step's `retries` said. The worker never released the claim and is about to launch the next container itself, so `running` + the same claim is the truthful state between attempts; re-pending described a task waiting to be picked up that nothing was going to pick up.

The guard is the claim fence: only a row still RUNNING and still claimed by this worker is reset. Anything else (a fail_fast cancellation, a lease that expired and was re-claimed elsewhere) yields ErrTaskClaimMismatch, which the executor already treats as "abandon quietly".

func (*Store) RetryTaskInstance

func (s *Store) RetryTaskInstance(runID, taskRunID uuid.UUID, attempt int) error

RetryTaskInstance resets one fan-out instance for another attempt.

It is the instance-keyed analogue of RetryTask, which resolves its row via loadUniqueTaskRun and therefore cannot address a group member. Only a non-terminal-or-failed row is reset: the guard keeps a retry from resurrecting an instance that a fail_fast cancellation or an in-group skip cascade has already resolved (the same class of bug as the local replace-cancel resurrection fixed in #275).

func (*Store) SaveSchemaViolations

func (s *Store) SaveSchemaViolations(runID, taskRef uuid.UUID, violations []pkgtask.SchemaViolation) error

SaveSchemaViolations persists schema validation violations onto exactly one task run. taskRef follows the TaskRun-primary-key-or-catalog-task-ID contract: a fan-out instance must be addressed by its TaskRun ID. The old `WHERE job_run_id = ? AND task_id = ?` predicate broadcast one instance's violations across every sibling row, so a single bad partition made all N look schema-invalid. Resolving through loadTaskRunByIDOrUnique means an ambiguous catalog task ID now fails loudly (ErrAmbiguousTaskRun) instead of fanning the write.

func (*Store) SaveTaskLogSnapshot

func (s *Store) SaveTaskLogSnapshot(runID, taskRef uuid.UUID, snapshot *TaskLogSnapshot) error

SaveTaskLogSnapshot persists the captured log snapshot onto exactly one task run. taskRef follows the TaskRun-primary-key-or-catalog-task-ID contract: a fan-out instance must be addressed by its TaskRun ID, otherwise the snapshot would be broadcast across every sibling row sharing (job_run_id, task_id).

func (*Store) SetBus

func (s *Store) SetBus(bus event.Bus)

func (*Store) SetRunStateCache

func (s *Store) SetRunStateCache(inv runStateInvalidator)

SetRunStateCache registers the in-memory run state layered over this store. Called by NewOwnerManager so the wiring lives next to the thing being wired rather than in the server bootstrap, where forgetting it would look like nothing at all.

func (*Store) SetTaskEffectiveHash

func (s *Store) SetTaskEffectiveHash(runID, taskRef uuid.UUID, effectiveHash string) error

SetTaskEffectiveHash records the proven-equivalent prior identity a task presents to its downstream consumers when a value-verified short-circuit was proven (design Component 5 / D2). It writes ONLY the effective_hash column; the task's own Hash, output, and result are untouched, so its receipt and `caesium why` still reflect its true identity. Passing an empty effectiveHash is a no-op (the common case — no short-circuit), keeping the column nil and PredecessorHashes falling back to the true hash. This is the only writer of effective_hash, so a downstream reader observes either the proven prior identity or nothing. taskRef follows the TaskRun-primary-key-or-catalog-task-ID contract: a value-verified short-circuit is proven per INSTANCE (its own output was byte-identical), so it must be recorded on that instance's row.

func (*Store) SetTaskExitCode

func (s *Store) SetTaskExitCode(runID, taskRef uuid.UUID, exitCode *int) error

SetTaskExitCode persists the raw process exit code the engine reported at task completion onto the task run. The incident classifier reads this alongside SchemaViolations/Result to bucket a failure into a failure_class. Best-effort: a nil-safe no-op when the row is gone. taskRef follows the TaskRun-primary-key-or-catalog-task-ID contract so a fan-out instance records its own exit code instead of overwriting its siblings'.

func (*Store) SetTaskHash

func (s *Store) SetTaskHash(runID, taskRef uuid.UUID, hash string) error

SetTaskHash persists a task's identity hash. taskRef follows the TaskRun-primary-key-or-catalog-task-ID contract, so a fan-out instance is addressed by its own TaskRun ID.

func (*Store) SetTaskHashWithBlob

func (s *Store) SetTaskHashWithBlob(runID, taskRef uuid.UUID, hash, resolvedImageDigest string, hashInputBlob []byte) error

SetTaskHashWithBlob persists the task identity hash, the resolved image digest folded into it, and the canonical secret-redacted decomposition of the HashInput (the blob) on the same write — the existing hash write-path. The digest and blob are optional: an empty digest or a nil/empty blob leaves the corresponding column untouched (so a literal-tag, blob-less run stays consistent). The blob lets `caesium why` later diff two runs field-by-field rather than only observing that the opaque hashes differ. taskRef follows the TaskRun-primary-key-or-catalog-task-ID contract. This matters more here than almost anywhere else: with loadUniqueTaskRun, a fanned step's per-instance identity write returned ErrAmbiguousTaskRun and the hash was NEVER persisted, so `caesium why --partition`, `receipt get` and `run retry --partition` had no identity to match and the local lane published no per-partition cache entry.

func (*Store) SetTaskHashWithDigest

func (s *Store) SetTaskHashWithDigest(runID, taskRef uuid.UUID, hash, resolvedImageDigest string) error

SetTaskHashWithDigest persists the task identity hash together with the resolved image digest that was folded into it. The digest may be empty when pinning is off or resolution failed; in that case only the hash is written and the existing digest column (if any) is left untouched, keeping the row consistent with the literal-tag cache key.

func (*Store) SkipTask

func (s *Store) SkipTask(runID, taskID uuid.UUID, reason string) error

SkipTask skips a STEP: every caller uses it to skip a successor whose trigger rule was not satisfied, so under fan-out it means the whole group. It routes through markInstanceSkippedTx per instance, giving each its own terminal_sequence and its own task_skipped event rather than one UPDATE across the group.

func (*Store) SkipTaskInstance

func (s *Store) SkipTaskInstance(runID, taskRunID uuid.UUID, reason string) error

SkipTaskInstance resolves one still-pending instance as skipped with a reason.

fanOut.failurePolicy: fail_fast needs this: on the first sibling failure the pending siblings must be resolved, not merely left undispatched. Leaving them pending would hang the run until its timeout, the same failure mode the in-group skip cascade exists to prevent. A row that is already terminal is left untouched.

func (*Store) Start

func (s *Store) Start(jobID uuid.UUID, triggerID *uuid.UUID, opts ...StartOption) (*JobRun, error)

func (*Store) StartForBackfill

func (s *Store) StartForBackfill(jobID, backfillID uuid.UUID, params map[string]string) (*JobRun, error)

StartForBackfill creates a JobRun pre-linked to a backfill ID. The caller should then execute the job with run.WithContext(ctx, r.ID) so the executor resumes from this pre-created record rather than creating a new one.

func (*Store) StartQueuedRun

func (s *Store) StartQueuedRun(ctx context.Context, queued *models.RunQueue) (*JobRun, error)

StartQueuedRun creates a fresh JobRun from an already-claimed run_queue row. If a slot disappears between dequeue and insert, the caller should release the queue row so a later drain can retry it.

func (*Store) StartTask

func (s *Store) StartTask(runID, taskRef uuid.UUID, runtimeID string) error

StartTask marks a task run as running. taskRef is resolved by loadTaskRunByIDOrUnique, so it may be either a TaskRun primary key (required for a fan-out instance, where the catalog task ID matches N sibling rows) or a catalog task ID (unfanned steps, which still have exactly one row).

func (*Store) StartTaskClaimed

func (s *Store) StartTaskClaimed(runID, taskRef uuid.UUID, runtimeID, claimedBy string) error

StartTaskClaimed is the distributed-lane counterpart of StartTask. taskRef follows the same TaskRun-primary-key-or-catalog-task-ID contract, so a worker executing a fan-out instance must pass the instance's TaskRun ID.

func (*Store) StartWithContext

func (s *Store) StartWithContext(ctx context.Context, jobID uuid.UUID, triggerID *uuid.UUID, opts ...StartOption) (*JobRun, error)

func (*Store) TaskExecutionDescriptor

func (s *Store) TaskExecutionDescriptor(ctx context.Context, runID, taskRef uuid.UUID) (*models.TaskExecutionDescriptor, error)

TaskExecutionDescriptor returns the frozen descriptor for one TaskRun.

taskRef takes the usual TaskRun-primary-key-or-catalog-task-id contract, and callers holding a row MUST pass its primary key. The previous `(job_run_id, task_id) … Take` form silently picked an arbitrary sibling once a step could be fanned, and the one caller is the worker's quarantined-replay path — which executes the container the descriptor describes. Now that quarantined replay re-materializes fanned groups, that lookup would hand instance `bravo`'s container the descriptor recorded for `alpha`.

func (*Store) TaskLogSnapshotForInstance

func (s *Store) TaskLogSnapshotForInstance(ctx context.Context, runID, taskRunID uuid.UUID) (*TaskLogSnapshot, error)

TaskLogSnapshotForInstance returns the persisted log snapshot of ONE instance, addressed by TaskRun primary key. GetTaskLogSnapshot's (run, task) predicate returns an arbitrary sibling's log for a fanned group; this one cannot.

A row with no captured log yields (nil, nil), matching GetTaskLogSnapshot.

func (*Store) TaskQuarantine

func (s *Store) TaskQuarantine(ctx context.Context, runID, taskID uuid.UUID) (bool, error)

func (*Store) TaskRunInstances

func (s *Store) TaskRunInstances(ctx context.Context, runID, taskID uuid.UUID) ([]*TaskRun, error)

TaskRunInstances returns every TaskRun row for (runID, taskID) in stable partition order (partition_index, then created_at, then id). An unfanned task yields exactly one element; a fanned group yields N, including instances that were skipped for an in-group dependency failure.

Unlike the collapsed run-detail payload (collapseFanOutGroups), the returned rows keep their own TaskRun primary key in ID — the caller can address a single instance afterwards.

func (*Store) TaskRunsForTask

func (s *Store) TaskRunsForTask(runID, taskID uuid.UUID) ([]models.TaskRun, error)

TaskRunsForTask returns every TaskRun instance of (runID, taskID) ordered by partition_index. It is the group-aware read every caller that used to arbitrarily `.First()` a sibling must use instead: an unfanned task yields exactly one row (identical to the old behavior), a fanned task yields all N so the caller can choose the instance it means (e.g. the failed one) or summarize the group. Returns an empty slice, never an error, when the task has no rows.

func (*Store) TerminalTaskRunsSince

func (s *Store) TerminalTaskRunsSince(runID uuid.UUID, afterSeq int64) ([]models.TaskRun, error)

TerminalTaskRunsSince returns the run's terminal task_runs rows with a terminal_sequence strictly greater than afterSeq, ordered by terminal_sequence ascending — the "post-checkpoint tail" a recovering owner replays on top of the latest checkpoint. Uses the (job_run_id, terminal_sequence) index.

func (*Store) UpdateTaskExecutionDescriptorInputs

func (s *Store) UpdateTaskExecutionDescriptorInputs(runID, taskID uuid.UUID, predecessorOutputs map[uuid.UUID]map[string]string, predecessorHashes map[uuid.UUID]string, computedHash, resolvedImageDigest string, hashInputBlob []byte) error

func (*Store) UpdateTaskExecutionDescriptorSecretRefs

func (s *Store) UpdateTaskExecutionDescriptorSecretRefs(runID, taskID uuid.UUID, refs []models.TaskExecutionSecretRef) error

func (*Store) WhyTask

func (s *Store) WhyTask(ctx context.Context, runID uuid.UUID, taskRef string) (*WhyExplanation, error)

WhyTask explains why the task identified by taskRef (a task UUID or a task name) in run runID executed / hit cache / re-ran. taskRef is matched first as a UUID against task_id, then as a task name within the run's job.

On a FANNED step it returns the group summary (see WhyTaskPartition for the per-instance answer).

func (*Store) WhyTaskPartition

func (s *Store) WhyTaskPartition(ctx context.Context, runID uuid.UUID, taskRef, partition string) (*WhyExplanation, error)

WhyTaskPartition is WhyTask with an explicit fan-out instance selector.

  • partition == "": an unfanned task is explained exactly as before; a fanned step returns the aggregate WhyGroup summary rather than an arbitrary sibling's explanation (the pre-fan-out code path `.First()`-ed one row of N, so the answer depended on database row order).
  • partition != "": the named instance is explained with the full single-instance diff. ErrPartitionNotFound (listing the available values) when the group has no such partition.

func (*Store) WithLeaseStore

func (s *Store) WithLeaseStore(ls *LeaseStore) *Store

WithLeaseStore enables run-owner lease writing. Call this from startup code when CAESIUM_RUN_OWNER_ENABLED=true.

func (*Store) WriteCheckpoint

func (s *Store) WriteCheckpoint(runID uuid.UUID, sequenceHigh, ownerGeneration int64, blob []byte, incremental bool) error

WriteCheckpoint persists a run_checkpoints row for runID. sequenceHigh is the highest terminal_sequence the snapshot covers; ownerGeneration fences the write against a stale owner; blob is the serialized state (RunState.Snapshot); incremental marks a delta vs a full snapshot. Re-writing the same (run_id, sequence_high) overwrites — checkpoints are idempotent at a sequence.

type TaskLogSnapshot

type TaskLogSnapshot struct {
	Text      string
	Truncated bool
}

type TaskRun

type TaskRun struct {
	ID               uuid.UUID                 `json:"id"`
	JobRunID         uuid.UUID                 `json:"job_run_id"`
	TaskID           uuid.UUID                 `json:"task_id"`
	JobAlias         string                    `json:"job_alias,omitempty"`
	JobLabels        map[string]string         `json:"job_labels,omitempty"`
	AtomID           uuid.UUID                 `json:"atom_id"`
	Engine           models.AtomEngine         `json:"engine"`
	Image            string                    `json:"image"`
	Command          []string                  `json:"command"`
	RuntimeID        string                    `json:"runtime_id,omitempty"`
	Status           TaskStatus                `json:"status"`
	Priority         int                       `json:"priority"`
	NodeSelector     map[string]string         `json:"node_selector,omitempty"`
	ClaimedBy        string                    `json:"claimed_by,omitempty"`
	ClaimExpiresAt   *time.Time                `json:"claim_expires_at,omitempty"`
	ClaimAttempt     int                       `json:"claim_attempt"`
	Attempt          int                       `json:"attempt"`
	MaxAttempts      int                       `json:"max_attempts"`
	Result           string                    `json:"result,omitempty"`
	Output           map[string]string         `json:"output,omitempty"`
	SchemaViolations []pkgtask.SchemaViolation `json:"schema_violations,omitempty"`
	BranchSelections []string                  `json:"branch_selections,omitempty"`
	Quarantine       bool                      `json:"quarantine"`
	CacheHit         bool                      `json:"cache_hit"`
	ReplaySafe       bool                      `json:"replay_safe"`
	// The remaining frozen execution inputs. They are `json:"-"` because they
	// are not API surface — they exist so the LOCAL executor can run a task from
	// the same row the distributed worker runs it from (issue #354). The
	// scheduler resolves them once at RegisterTasks; both lanes must then read
	// them here rather than re-deriving them from a live catalog that a
	// `job apply` may have moved since the run was registered.
	//
	// CacheEnabled..CacheTTLNever are the seven columns the worker rebuilds
	// jobdefschema.CacheConfig from (internal/worker/runtime_executor.go).
	CacheEnabled    bool          `json:"-"`
	CacheTTL        time.Duration `json:"-"`
	CacheVersion    int           `json:"-"`
	CachePinDigests bool          `json:"-"`
	CacheDigestTTL  time.Duration `json:"-"`
	CacheChain      string        `json:"-"`
	CacheTTLNever   bool          `json:"-"`
	// OutputSchema / SchemaValidation are what the worker validates a task's
	// output against (runtimeExecutor.runSchemaValidation).
	OutputSchema            []byte     `json:"-"`
	SchemaValidation        string     `json:"-"`
	CacheOriginRunID        *uuid.UUID `json:"cache_origin_run_id,omitempty"`
	CacheCreatedAt          *time.Time `json:"cache_created_at,omitempty"`
	CacheExpiresAt          *time.Time `json:"cache_expires_at,omitempty"`
	RateLimitRetryAfter     *time.Time `json:"rate_limit_retry_after,omitempty"`
	StartedAt               *time.Time `json:"started_at,omitempty"`
	CompletedAt             *time.Time `json:"completed_at,omitempty"`
	Error                   string     `json:"error,omitempty"`
	OutstandingPredecessors int        `json:"outstanding_predecessors"`
	PartitionValue          string     `json:"partition_value,omitempty"`
	PartitionIndex          int        `json:"partition_index,omitempty"`
	PartitionCount          int        `json:"partition_count,omitempty"`
	PartitionFingerprint    string     `json:"partition_fingerprint,omitempty"`
	PartitionDependsOn      []string   `json:"partition_depends_on,omitempty"`
	// PartitionStatusCounts is the per-status histogram of a COLLAPSED fan-out
	// group: {"succeeded":2,"failed":1,…}. Set only on the collapsed group entry
	// that run-detail payloads return in place of N instance rows (see
	// collapseFanOutGroups) — omitted for unfanned tasks and for the individual
	// instance rows the partition endpoints return. Without it the UI can render
	// partition_count but not the mix, so a 1000-instance group with one failure
	// looks identical to one with 500.
	PartitionStatusCounts map[string]int `json:"partition_status_counts,omitempty"`
	CreatedAt             time.Time      `json:"created_at"`
	UpdatedAt             time.Time      `json:"updated_at"`
}

type TaskStatus

type TaskStatus string
const (
	TaskStatusPending   TaskStatus = "pending"
	TaskStatusRunning   TaskStatus = "running"
	TaskStatusSucceeded TaskStatus = "succeeded"
	TaskStatusFailed    TaskStatus = "failed"
	TaskStatusSkipped   TaskStatus = "skipped"
	TaskStatusCached    TaskStatus = "cached"
	TaskStatusCancelled TaskStatus = TaskStatus(models.TaskRunStatusCancelled)
)

func CollectPredecessorStatuses

func CollectPredecessorStatuses(predIDs []uuid.UUID, taskOutcomes map[uuid.UUID]TaskStatus) []TaskStatus

CollectPredecessorStatuses returns the known statuses for the given set of predecessor task IDs using the in-memory outcomes map. Predecessors with no recorded outcome yet are omitted.

This and SatisfiesTriggerRule are the single source of truth for trigger-rule evaluation, shared by the local executor (internal/job) and the run-owner in-memory state machine (RunState) so DAG advancement semantics cannot drift between the two paths.

type WhyBaseline

type WhyBaseline struct {
	// Kind is "cache_origin" (the run that populated the matched cache entry),
	// "prior_run" (the most-recent earlier run of the same task), "per_partition"
	// (the subject is a fanned GROUP, so the baseline is per-instance and only a
	// --partition selection can name one), or "none".
	Kind string `json:"kind"`
	// RunID is the baseline run, when applicable.
	RunID *uuid.UUID `json:"runId,omitempty"`
	// TaskRunID is the baseline task-run, when applicable.
	TaskRunID *uuid.UUID `json:"taskRunId,omitempty"`
	// StartedAt is when the baseline run started, when known.
	StartedAt *time.Time `json:"startedAt,omitempty"`
}

WhyBaseline describes which run/entry the subject was diffed against, so the answer is auditable.

type WhyExplanation

type WhyExplanation struct {
	RunID    uuid.UUID `json:"runId"`
	JobID    uuid.UUID `json:"jobId"`
	TaskID   uuid.UUID `json:"taskId"`
	TaskName string    `json:"taskName"`
	// TaskRunID is the explained instance's TaskRun primary key. It is nil — and
	// omitted from the JSON — for a fanned GROUP summary, which has no single
	// instance; select one with a partition to get it back.
	TaskRunID *uuid.UUID `json:"taskRunId,omitempty"`
	// Partition is the selected instance's partition value. Empty (omitted) for
	// an unfanned task and for a group summary.
	Partition string `json:"partition,omitempty"`

	Verdict WhyVerdict `json:"verdict"`
	Status  string     `json:"status"`
	// CacheEnabled reflects whether caching applied to this task.
	CacheEnabled bool `json:"cacheEnabled"`
	// Hash is this task-run's identity hash.
	Hash string `json:"hash,omitempty"`

	// Summary is a one-line human-readable explanation, e.g.
	// "CACHE_MISS — predecessor `extract.row_count` changed 1.2M→1.4M; image,
	// command, env identical".
	Summary string `json:"summary"`

	Trigger  WhyTrigger  `json:"trigger"`
	Baseline WhyBaseline `json:"baseline"`
	Diff     *BlobDiff   `json:"diff,omitempty"`

	// Remediation lists the APPROVED remediation actions that changed this task's
	// outcome — a `skip_task` that skipped it, or an `override_schema_gate` that
	// suppressed its output-schema enforcement. Without it, a task that reads
	// "skipped" or a schema violation that never fired is an unexplainable state:
	// the DAG says nothing happened and the reason lives in an incident timeline
	// the operator has no pointer to. Omitted when there is none.
	Remediation []WhyRemediation `json:"remediation,omitempty"`

	// Group is populated ONLY for a fanned step explained without a partition
	// selector: the aggregate answer over all N instances. It is omitted for an
	// unfanned task and for a single selected instance, so unfanned output is
	// byte-identical to the pre-fan-out shape.
	Group *WhyGroup `json:"group,omitempty"`
}

WhyExplanation is the full, machine-readable answer the why service returns. It is rendered as JSON by the API and as both a table and JSON by the CLI.

type WhyGroup

type WhyGroup struct {
	// PartitionCount is the number of live instance rows in the group.
	PartitionCount int `json:"partitionCount"`
	// StatusCounts is the status histogram over the instances, keyed by task
	// status ("succeeded", "failed", "cached", "skipped", "running", ...).
	StatusCounts map[string]int `json:"statusCounts"`
	// CacheHits counts instances served from cache.
	CacheHits int `json:"cacheHits"`
	// Partitions lists the group's partition values in emission order, capped at
	// whyGroupPartitionListCap so a 10k-instance group does not bloat the
	// response; PartitionsTruncated reports the cap.
	Partitions          []string `json:"partitions,omitempty"`
	PartitionsTruncated bool     `json:"partitionsTruncated,omitempty"`
	// FirstFailure is the lowest-index failed instance — the one whose cause
	// explains the group's failed status. Nil when no instance failed.
	FirstFailure *WhyGroupFailure `json:"firstFailure,omitempty"`
	// StartedAt / CompletedAt are the aggregate envelope: the earliest instance
	// start and the latest instance end. DurationMS is their span (0 while the
	// group is still running).
	StartedAt   *time.Time `json:"startedAt,omitempty"`
	CompletedAt *time.Time `json:"completedAt,omitempty"`
	DurationMS  int64      `json:"durationMs,omitempty"`
	// Notes are qualifiers about HOW the group's instance keys were computed, the
	// group-level counterpart of BlobDiff.Notes. A group carries no Diff — it has
	// N hashes and N baselines — so without this channel the flagship shape of
	// this feature (spec 5.5 puts `fanOut` AND `chain: values` on the same step)
	// would answer "6 cached, 1 succeeded" and never mention that predecessor
	// hashes were excluded from those six keys. That is the unexplainable skip
	// spec 4.3 exists to prevent, and an operator who does not yet know the
	// partition keys reaches the group form first.
	Notes []string `json:"notes,omitempty"`
}

WhyGroup is the aggregate explanation for a fanned step. `caesium why --task <name>` on a fanned step cannot answer "why did THE task run" — there are N instances with N identity hashes and possibly N different verdicts — so it answers about the group and points at the per-instance selector.

type WhyGroupFailure

type WhyGroupFailure struct {
	Partition      string    `json:"partition"`
	PartitionIndex int       `json:"partitionIndex"`
	TaskRunID      uuid.UUID `json:"taskRunId"`
	Status         string    `json:"status"`
	Attempt        int       `json:"attempt"`
	Error          string    `json:"error,omitempty"`
}

WhyGroupFailure names the instance a fanned group's failure is attributed to.

type WhyRemediation

type WhyRemediation struct {
	// ActionID is the AgentAction audit row.
	ActionID uuid.UUID `json:"actionId"`
	// IncidentID is the incident whose timeline carries the full evidence.
	IncidentID uuid.UUID `json:"incidentId"`
	// Type is the catalog action type ("skip_task", "override_schema_gate").
	Type string `json:"type"`
	// Tier is the action's tier (3 for everything approval-gated).
	Tier int `json:"tier"`
	// ApprovedBy is the operator identity recorded on the approval decision.
	ApprovedBy string `json:"approvedBy,omitempty"`
	// ApprovedAt is when the human decided.
	ApprovedAt *time.Time `json:"approvedAt,omitempty"`
	// ExecutedAt is when the approved action actually ran.
	ExecutedAt *time.Time `json:"executedAt,omitempty"`
	// Reason is the operator-supplied justification, when one was given.
	Reason string `json:"reason,omitempty"`
	// Scope is "task" when the action targeted this specific task and "run" when
	// it applied to the whole run (an override_schema_gate covers every task).
	Scope string `json:"scope"`
}

WhyRemediation is one approved, executed remediation action attributed to the explained task or its run — the "approved by <decider>, executed at <ts>" provenance (trust-the-substrate C8).

Every field is read from already-persisted state: the AgentAction audit row and the ApprovalRequest that authorised it. Nothing here is inferred.

type WhyTrigger

type WhyTrigger struct {
	// Type is the trigger type that fired the run (e.g. "cron", "http",
	// "manual"), as recorded on the run.
	Type string `json:"type,omitempty"`
	// Alias is the trigger alias, when set.
	Alias string `json:"alias,omitempty"`
	// Params are the run parameters captured at trigger time; these feed into the
	// HashInput (RunParams), so a changed param is also a possible miss cause and
	// will appear in the diff under "runParams.<key>".
	Params map[string]string `json:"params,omitempty"`
	// FiredAt is the run's start time.
	FiredAt time.Time `json:"firedAt"`
}

WhyTrigger captures the trigger-side causation for the run, read from the run row and the run_started ExecutionEvent.

type WhyVerdict

type WhyVerdict string

WhyVerdict is the high-level cache outcome for the explained task.

const (
	// VerdictCacheHit — the task did not execute; its identity hash matched a
	// live cache entry and the prior result was reused.
	VerdictCacheHit WhyVerdict = "CACHE_HIT"
	// VerdictCacheMiss — caching was enabled but the task's identity hash did not
	// match any live cache entry, so the task executed. The diff names what
	// changed versus the prior run (had it been unchanged, this would have
	// skipped).
	VerdictCacheMiss WhyVerdict = "CACHE_MISS"
	// VerdictCacheOff — caching was not enabled for this task, so it executed
	// unconditionally and no hit/miss attribution applies. A field diff versus a
	// prior run is still offered when a blob exists.
	VerdictCacheOff WhyVerdict = "CACHE_DISABLED"
	// VerdictUnknown — the task run is not in a terminal/decided state (e.g. still
	// pending or running), so no cache verdict can be given yet.
	VerdictUnknown WhyVerdict = "UNKNOWN"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL