Documentation
¶
Overview ¶
Package taskrun is the durable ledger of autonomous task executions.
A Run is IMMUTABLE HISTORY. It snapshots everything that decided what an execution meant — the definition's revision and full text, the resolved project, the trigger occurrence, the expanded authority, the chosen runtime, the limits — at the moment it is created. Nothing re-reads the YAML mid-run, so editing a task file while it executes cannot retroactively change what that run was authorized to do or why it did it.
It lives in its own database rather than the gateway's for two reasons: `memcode task run` has to work on a machine where the daemon has never started (gateway state.OpenShared refuses when there is no gateway.db), and the run ledger is an audit trail that must outlive the gateway's operational state, which is pruned.
Index ¶
- Constants
- Variables
- func DBPath() (string, error)
- func LeaseKey(t task.Task, project string) string
- func LeaseKeys(t task.Task, project string) []string
- func NewID(now time.Time) string
- func OccurrenceID(tr task.Trigger, at time.Time) string
- func Spec(tr task.Trigger) occurrence.Spec
- func Summarize(results []CheckResult) string
- func TriggerKey(tr task.Trigger) string
- func Verify(ctx context.Context, dir string, commands []string, env ...string) ([]CheckResult, VerificationStatus)
- type AuthSource
- type CheckResult
- type Decision
- type ErrLeaseHeld
- type Escalation
- type ExecutionStatus
- type Lease
- type Outcome
- type Pause
- type PollResult
- type Result
- type Run
- func Freeze(t task.Task, root, triggerKind, triggerID string, now time.Time) (Run, error)
- func FreezeAt(t task.Task, root, triggerKind, triggerID string, occurredAt, now time.Time, ...) (Run, error)
- func FreezeWith(t task.Task, root, triggerKind, triggerID string, occurredAt, now time.Time, ...) (Run, error)
- type Runner
- func (r *Runner) Execute(ctx context.Context, run Run, t task.Task) (Run, error)
- func (r *Runner) Poll(ctx context.Context, tasks []task.Task, root string, now time.Time) PollResult
- func (r *Runner) Run(ctx context.Context, t task.Task, root, triggerKind, triggerID string) (Run, error)
- func (r *Runner) Start(ctx context.Context, t task.Task, root, triggerKind, triggerID string, ...) (Run, error)
- type Seen
- type SpawnFunc
- type SpawnRequest
- type SpawnResult
- type State
- type Store
- func (s *Store) Acknowledge(ctx context.Context, id string) error
- func (s *Store) Acquire(ctx context.Context, key, runID, taskName string, now time.Time) error
- func (s *Store) Active(ctx context.Context, task string) ([]Run, error)
- func (s *Store) Claim(ctx context.Context, id string, now time.Time) (bool, error)
- func (s *Store) Close() error
- func (s *Store) Create(ctx context.Context, r Run) (Run, error)
- func (s *Store) Finish(ctx context.Context, id string, outcome Outcome, ...) error
- func (s *Store) FinishResult(ctx context.Context, id string, r Result, now time.Time) error
- func (s *Store) Get(ctx context.Context, id string) (Run, error)
- func (s *Store) Heartbeat(ctx context.Context, id string, now time.Time) error
- func (s *Store) MarkSeen(ctx context.Context, ids []string) error
- func (s *Store) Note(ctx context.Context, id, note string) error
- func (s *Store) Pause(ctx context.Context, p Pause) error
- func (s *Store) Paused(ctx context.Context) ([]Pause, error)
- func (s *Store) PausedTask(ctx context.Context, taskName, project string) (Pause, bool, error)
- func (s *Store) Recent(ctx context.Context, task string, limit int) ([]Run, error)
- func (s *Store) Reconcile(ctx context.Context, now time.Time) (int, error)
- func (s *Store) Release(ctx context.Context, key, runID string) error
- func (s *Store) Renew(ctx context.Context, key, runID string, now time.Time) error
- func (s *Store) Resume(ctx context.Context, taskName, project string) (bool, error)
- func (s *Store) SetWatermark(ctx context.Context, taskName, triggerKey string, at, now time.Time) error
- func (s *Store) Unseen(ctx context.Context, limit int) ([]Run, error)
- func (s *Store) Watermark(ctx context.Context, taskName, triggerKey string) (time.Time, error)
- type VerificationStatus
Constants ¶
const Fresh = 2 * time.Minute
Fresh bounds what counts as "this just happened" rather than "this was missed". Generous relative to the poll interval so a slow tick is never mistaken for downtime.
const StaleAfter = 2 * time.Minute
StaleAfter is how long a running row may go without a heartbeat before Reconcile calls it dead. Generous relative to the heartbeat interval so a loaded machine is never mistaken for a crashed one.
const TriggerManual = "manual"
TriggerManual is the trigger kind for a run started by hand.
Variables ¶
var ErrOccupied = fmt.Errorf("this occurrence already has a run")
ErrOccupied means this trigger occurrence already has a run. The caller must NOT execute: someone else owns it.
Functions ¶
func LeaseKey ¶
LeaseKey is the resource a run needs exclusive use of.
Read-only runs return "" and take no lease at all: they change nothing, so two of them in a repo cannot interfere, and serializing them would only make an audit task block a dependency upgrade for no reason.
A mutating run keys on its RESOLVED PROJECT, so every mutating task in a repo serializes against the others. Worktree isolation may safely relax this later, but one mutating run per project is the conservative default and the right place to start.
func LeaseKeys ¶
LeaseKeys is every resource one run must hold exclusively.
A cross-project run mutates several checkouts, and leasing only the one it was anchored in leaves the others open to a second run working in them at the same time — which is the failure the lease exists to prevent, just moved one project to the left. A custom concurrency key is one key by definition and stays one key.
func NewID ¶
NewID returns a sortable, unique run identity. Time-prefixed so a directory listing or an ORDER BY id reads chronologically without a join.
func OccurrenceID ¶
OccurrenceID is the ledger identity of one firing of one trigger.
func Spec ¶
func Spec(tr task.Trigger) occurrence.Spec
Spec converts a task trigger into the timing form the occurrence package understands.
func Summarize ¶
func Summarize(results []CheckResult) string
Summarize renders check results for a run record.
func TriggerKey ¶
TriggerKey identifies the SCHEDULE, for storing its watermark.
func Verify ¶
func Verify(ctx context.Context, dir string, commands []string, env ...string) ([]CheckResult, VerificationStatus)
Verify runs a task's checks in order, in dir. Every check runs even after one fails: "which of the four broke" is more useful than "the first one broke", and the cost is bounded by checkTimeout.
Types ¶
type AuthSource ¶
type AuthSource func() runtimes.Authorizations
Authorizations supplies the machine's recorded runtime permissions. Injectable so tests do not depend on whatever is installed on the developer's laptop, and so the daemon and the CLI read the same store.
type CheckResult ¶
type CheckResult struct {
Command string
ExitCode int
Duration time.Duration
// Output is the tail of combined stdout/stderr, kept small: enough to see
// what failed, not enough to bury the run record.
Output string
// Err is set when the command could not be run at all, as distinct from
// running and failing.
Err string
}
CheckResult is one verification command's outcome.
type Decision ¶
type Decision struct {
// Fire are the logical occurrences to run, oldest first. Usually zero or one.
Fire []time.Time
// Advance is the new watermark: the newest occurrence now accounted for,
// whether it ran or was deliberately dropped. Persisting it is what stops a
// dropped occurrence being rediscovered as missed on the next poll.
Advance time.Time
// Dropped counts occurrences deliberately not run. Surfaced on the resulting
// run so a collapsed backlog is visible rather than silently discarded.
Dropped int
// Why explains a non-obvious decision, for the run record.
Why string
}
Decision is what a poll concluded for one trigger.
func Due ¶
Due computes what to run for one trigger, given the watermark of the last occurrence already accounted for.
A zero `last` means the trigger has never been seen. It is then PLACED at its most recent past occurrence rather than treated as having missed everything since the epoch — adding a weekly task on a Friday must not immediately fire it for every Monday in history.
type ErrLeaseHeld ¶
type ErrLeaseHeld struct{ Holder Lease }
ErrLeaseHeld means someone else is working on this resource right now.
func (ErrLeaseHeld) Error ¶
func (e ErrLeaseHeld) Error() string
type Escalation ¶
type Escalation string
Escalation is what a run concluded about the task's future, as distinct from what it concluded about itself.
const ( // EscalateNone: nothing to say. The default, and the common case. EscalateNone Escalation = "" // EscalateRetry: a transient condition — an upstream outage, a flaky // network. The task is fine; this run was unlucky. Never pauses, because a // temporary problem that suspends an automation forever is its own bug. EscalateRetry Escalation = "retry_later" // EscalateAttention: this run needs a human to look, but the next one is // still worth running. EscalateAttention Escalation = "needs_attention" // EscalatePause: the condition will recur until somebody decides something. EscalatePause Escalation = "pause_task" )
func ParseEscalation ¶
func ParseEscalation(out string) (Escalation, string)
ParseEscalation reads the escalation line a run was asked to end with.
This is a DECLARED protocol, not error-string matching: the agent is told to state its conclusion in a fixed form and this reads that form. The judgement — is this transient, or does it need a person, or will it recur forever — is the model's, made with the whole situation in view. Guessing it from the shape of an error message is what this exists to avoid.
type ExecutionStatus ¶
type ExecutionStatus string
ExecutionStatus is whether the agent's half of the run completed.
const ( ExecCompleted ExecutionStatus = "completed" ExecFailed ExecutionStatus = "failed" ExecTimedOut ExecutionStatus = "timed_out" ExecInterrupted ExecutionStatus = "interrupted" ExecBlocked ExecutionStatus = "blocked" )
type Lease ¶
type Lease struct {
Key string
RunID string
Task string
Host string
PID int
AcquiredAt time.Time
HeartbeatAt time.Time
}
Lease is a held claim on a resource.
type Outcome ¶
type Outcome string
Outcome is what a finished run actually achieved. "The agent stopped" is never by itself "the task succeeded", which is the failure mode this distinction exists to prevent.
const ( OutcomeSuccess Outcome = "success" // OutcomeNoChange is a successful run that found nothing to do. Worth its own // value: a weekly upgrade task reporting no_change is healthy, and collapsing // it into success hides whether the task is still finding anything. OutcomeNoChange Outcome = "no_change" OutcomeFailed Outcome = "failed" // OutcomeNeedsAttention: the run completed correctly and decided a human must // choose. An ambiguous migration it declined to guess at is a SUCCESSFUL // execution and an incomplete task. OutcomeNeedsAttention Outcome = "needs_attention" // OutcomeBlocked: the run could not start its real work (lock held, project // missing, no authorized runtime). OutcomeBlocked Outcome = "blocked" // OutcomeInterrupted: the process vanished mid-run — killed, crashed, or the // machine went down. Deliberately NOT retried automatically; see Reconcile. OutcomeInterrupted Outcome = "interrupted" )
func Decide ¶
func Decide(exec ExecutionStatus, verify VerificationStatus, changed bool, agentNeedsAttention bool) Outcome
Decide derives the final outcome from FACTS, never from the agent's prose.
The precedence is deliberate and one-directional: a blocked run cannot be talked up to failed, a failed verification cannot be talked up to success, and an agent's needs_attention signal can only ever make the verdict more cautious. The agent participates in this decision at exactly one point — it may raise needs_attention — and has no path to lower it.
type PollResult ¶
type PollResult struct {
Started []Run
// Skipped counts occurrences deliberately not run, across all triggers.
Skipped int
// Errs holds per-task failures; one bad task never stops the others.
Errs []error
}
PollResult reports what one poll did.
type Result ¶
type Result struct {
Outcome Outcome
Summary string
Detail string
LogPath string
ExecStatus ExecutionStatus
VerifyStatus VerificationStatus
Checks string
Worktree string
Branch string
BaseRev string
ResultRev string
Changed bool
Remote string
CommitSHA string
PRNumber int
PRURL string
CreatedBranch bool
CreatedCommit bool
CreatedPR bool
// Escalation is what the run concluded about FUTURE runs, as distinct from
// this one. Not persisted on the run row: its consequence is a task pause,
// which is its own durable record.
Escalation Escalation
EscalationWhy string
}
Finish closes a run. Terminal and idempotent: a run already done stays as it was, so a late finisher cannot overwrite an interrupted verdict. Result is everything a finished run records beyond its outcome.
type Run ¶
type Run struct {
ID string
Task string
// Frozen definition. Revision identifies it; Definition is the full YAML, so
// a run stays readable even after the file is deleted.
Revision string
Definition string
// TriggerKind is manual/cron/every/at. TriggerID identifies the specific
// OCCURRENCE and is the idempotency key: empty for manual runs (asking twice
// means twice), unique per firing otherwise.
TriggerKind string
TriggerID string
Project string
Grants []string
Timeout time.Duration
MaxCostUSD float64
// Where the inference ran. Requested and resolved are both frozen at
// creation, so the choice stays explainable after the machine's state and
// the user's authorizations have moved on.
RuntimeRequested string
RuntimeResolved string
ModelRequested string
ModelResolved string
CredSource string
AuthID string
AuthScope string
State State
Outcome Outcome
Summary string
Detail string
LogPath string
// OccurredAt is the LOGICAL time this run stands for, which is not when it
// started: a run_once recovery of Monday's occurrence executed on Wednesday
// occurred-at Monday. That is what makes a recovered run explainable.
OccurredAt time.Time
// Backlog counts occurrences dropped when this one was created.
Backlog int
// Execution and verification, kept separable from Outcome.
ExecStatus ExecutionStatus
VerifyStatus VerificationStatus
Checks string
// Where the work happened. BaseRev is resolved at EXECUTION time, so a
// recovered occurrence is honest about building on today's HEAD rather than
// implying the repository was frozen when it was due.
Worktree string
Branch string
BaseRev string
ResultRev string
Changed bool
// What was published, and which artifacts this run created rather than
// found already there.
Remote string
CommitSHA string
PRNumber int
PRURL string
CreatedBranch bool
CreatedCommit bool
CreatedPR bool
Host string
PID int
StartedAt time.Time
HeartbeatAt time.Time
FinishedAt time.Time
Seen Seen
}
Run is one execution, with its inputs frozen at creation.
func Freeze ¶
Freeze captures a task's execution inputs as a Run, resolving everything that could otherwise drift: the definition's revision AND full text, the absolute project path, the expanded authority, the runtime, the limits.
Nothing downstream reads the task file again. That is what makes a run mean one fixed thing forever, and it is why the definition is stored whole rather than by reference — a run stays readable after its file is edited or deleted.
func FreezeAt ¶
func FreezeAt(t task.Task, root, triggerKind, triggerID string, occurredAt, now time.Time, backlog int) (Run, error)
FreezeAt is Freeze with the LOGICAL occurrence this run stands for, and the size of the backlog collapsed into it. A run_once recovery of Monday's occurrence executed on Wednesday is occurredAt Monday: the ledger then says which firing was recovered, instead of only when someone got round to it.
func FreezeWith ¶
func FreezeWith(t task.Task, root, triggerKind, triggerID string, occurredAt, now time.Time, backlog int, rt runtimes.Resolution, ) (Run, error)
FreezeWith is FreezeAt plus the resolved runtime, frozen with everything else.
type Runner ¶
type Runner struct {
Store *Store
// Spawn runs the agent and blocks until it finishes. Injectable so tests
// exercise the whole lifecycle — including crashes — without a model call.
Spawn SpawnFunc
// HeartbeatEvery bounds how stale a live run's heartbeat can get.
HeartbeatEvery time.Duration
// Auth and Available answer "what is permitted" and "what is present".
// Injectable so a test's answer does not depend on the developer's laptop.
Auth AuthSource
Available func() []string
}
Runner turns a task definition into a durable, executed Run.
The order matters and is the point of the whole design:
freeze inputs -> create the row (claims the occurrence) -> claim the work -> execute -> persist the outcome
The row exists before any work starts, so a duplicate dispatch is rejected by the ledger rather than by hoping two executions do not overlap.
func NewRunner ¶
func NewRunner(store *Store, auth AuthSource) *Runner
NewRunner builds a runner backed by real detached memcode processes.
func (*Runner) Execute ¶
Execute claims and runs an already-created run to completion, persisting the outcome. Safe to call only once per run; the conditional claim enforces that.
func (*Runner) Poll ¶
func (r *Runner) Poll(ctx context.Context, tasks []task.Task, root string, now time.Time) PollResult
Poll fires everything due. It returns once every started run has finished, so a caller that wants concurrency across tasks runs Poll in a goroutine.
type Seen ¶
type Seen string
Seen tracks whether a human has dealt with a run's result. Three states, not a boolean: a needs_attention run that scrolled past in a session banner has been SEEN, not dealt with, and collapsing those loses the distinction exactly where it matters most.
type SpawnFunc ¶
type SpawnFunc func(ctx context.Context, req SpawnRequest) (SpawnResult, error)
SpawnFunc executes a task's work and reports what happened.
type SpawnRequest ¶
type SpawnRequest struct {
RunID string
// Project is the repository that owns the run's bookkeeping — its job log
// outlives a disposable worktree because of this.
Project string
// WorkDir is where the work actually happens: the isolated worktree, or the
// project itself when there is nothing to isolate.
WorkDir string
Instructions string
Mode permissions.Mode
ReadOnly bool
// Env selects the resolved runtime for the child.
Env []string
// DenyTools and DenyCommands are the capability ceiling, projected from the
// task's grants. Both are real restrictions on the child.
DenyTools []string
DenyCommands []string
Timeout time.Duration
}
SpawnRequest is everything the executor needs, taken from the FROZEN run rather than from the task file, so a mid-run YAML edit cannot reach it.
type SpawnResult ¶
SpawnResult is the executor's report.
type State ¶
type State string
State is where a run is in its lifecycle. Terminal state is always Done; what actually happened is the Outcome.
const ( // StatePending: the row exists and owns its occurrence, but no process has // claimed the work yet. Creating the row before doing anything is what makes // at-least-once dispatch safe — the unique occurrence index rejects a second // row before a second execution can start. StatePending State = "pending" // StateRunning: a live process holds it, identified by host and pid and // proven by a heartbeat. StateRunning State = "running" // StateDone: terminal. Read Outcome. StateDone State = "done" )
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is the run ledger.
func Open ¶
Open creates or opens the ledger. There is deliberately NO exclusive lock: the CLI, the daemon and spawned children all touch this concurrently, and WAL plus a busy timeout is what makes that safe. The gateway's own singleton lock exists to keep one inbox worker, which is a different problem.
func OpenDefault ¶
OpenDefault opens the ledger at its standard location.
func (*Store) Acknowledge ¶
Acknowledge records that a human actually dealt with a run.
func (*Store) Acquire ¶
Acquire takes a lease, stealing one whose owner is gone. An empty key is a no-op success: a read-only run needs no exclusivity.
func (*Store) Claim ¶
Claim moves a pending run to running and stamps the owning process. The UPDATE is conditional on the row still being pending, so two processes racing for the same run cannot both win: the loser sees claimed=false.
func (*Store) Create ¶
Create records a new run in StatePending, claiming its occurrence. It returns ErrOccupied when a triggered occurrence is already accounted for — that is the idempotency boundary doing its job, and the caller must treat it as "someone else has this", never as an error to retry through.
func (*Store) FinishResult ¶
FinishResult closes a run with its full structured verdict.
func (*Store) Heartbeat ¶
Heartbeat proves the owning process is still alive. Reconcile uses its absence to tell a crashed run from a slow one.
func (*Store) MarkSeen ¶
MarkSeen advances unseen runs to seen. It never touches an acknowledged run and never moves backwards: showing someone a banner is not the same as them dealing with it, which is the whole reason this is not a boolean.
func (*Store) Note ¶
Note records why a run exists — a recovered occurrence, a collapsed backlog — before it executes, so the explanation survives even if the run then crashes. Appends rather than replaces: a run may accumulate more than one note.
func (*Store) Pause ¶
Pause suspends a task's unattended execution. Idempotent on the FIRST reason: re-pausing an already-paused task must not overwrite the original diagnosis with a later, vaguer one.
func (*Store) Paused ¶
Paused lists every suspended task, oldest first: a responsibility memcode stopped fulfilling longest ago is the one most worth raising.
func (*Store) PausedTask ¶
PausedTask reports a task's suspension, if it has one.
func (*Store) Reconcile ¶
Reconcile settles runs left behind by a process that died — a killed daemon, a crash, a machine that went down mid-run.
The semantic is deliberate and narrow: such a run is FAILED as interrupted, never silently resumed and never silently re-run. A partially finished mutating run cannot be safely continued by a new process that did not see what the old one did, and re-running it automatically is exactly how at-least-once dispatch turns into duplicate side effects. If the work still needs doing, that is a NEW run with a new identity, which the ledger shows.
A run is considered dead when its heartbeat is older than StaleAfter, or when it is owned by this host and its pid is gone (which is immediate and does not wait out the timeout).
func (*Store) Release ¶
Release drops a lease, but only if this run still holds it — a run that was already evicted as dead must not delete its successor's claim.
func (*Store) SetWatermark ¶
func (s *Store) SetWatermark(ctx context.Context, taskName, triggerKey string, at, now time.Time) error
SetWatermark records an occurrence as accounted for — whether it ran or was deliberately dropped. Advancing on a drop is what stops a skipped occurrence being rediscovered as missed on the next poll, forever.
It never moves backwards, so an out-of-order poll cannot rewind the series.
type VerificationStatus ¶
type VerificationStatus string
VerificationStatus is the verdict over all checks, kept separate from whether the agent's execution completed. A run whose agent finished cleanly and whose tests then failed is an execution success and a task failure, and collapsing those two into one enum is how that distinction gets lost.
const ( // VerifyNone: the task declared no checks. Not a pass — an absence. VerifyNone VerificationStatus = "none" VerifyPass VerificationStatus = "passed" VerifyFail VerificationStatus = "failed" // VerifySkipped: the run never got far enough to verify. VerifySkipped VerificationStatus = "skipped" )