Documentation
¶
Overview ¶
Package automation implements jcode Automations: scheduled and manual agent runs. The package is a leaf domain layer — it owns the data model, validation, scheduling math, persistence (two-file + flock), built-in templates, and the single-owner scheduler loop. It does NOT depend on web/tui/runner; callers inject a Runner to actually execute a run. See internal-doc/automations-prd.md.
Index ¶
- Constants
- Variables
- func Badge(t Trigger) string
- func ComputeNextRun(after time.Time, t Trigger) (time.Time, bool)
- func ExecuteRun(ctx context.Context, store *Store, runner Runner, a *Automation, kind string) (string, error)
- func HumanSchedule(t Trigger) string
- func IsLocalPath(p string) bool
- func NextCronFire(after time.Time, c *CronExpr) (time.Time, bool)
- func SlotKey(t time.Time) string
- func ValidateAutomation(a *Automation) error
- type Automation
- type Cadence
- type ContextPolicy
- type CronExpr
- type RunState
- type Runner
- type RunnerReadiness
- type Scheduler
- type SessionAutomationPolicy
- type SessionAutomationSnapshot
- type SkipNotifier
- type Store
- func (s *Store) Create(a Automation) (*Automation, error)
- func (s *Store) Delete(id string) error
- func (s *Store) Get(id string) *Automation
- func (s *Store) List() []*Automation
- func (s *Store) ListByOwnerSession(sessionID string) ([]*Automation, error)
- func (s *Store) ResolveOwnerSession(sessionID string, policy SessionAutomationPolicy) (SessionAutomationSnapshot, error)
- func (s *Store) RestoreSessionAutomations(snapshot SessionAutomationSnapshot) error
- func (s *Store) SetEnabled(id string, enabled bool) (*Automation, error)
- func (s *Store) State(id string) RunState
- func (s *Store) TryClaimScheduledOnce(id, expectedAt string) (*Automation, bool, error)
- func (s *Store) TryMarkRunning(id string) (bool, error)
- func (s *Store) Update(id string, mutate func(*Automation)) (*Automation, error)
- func (s *Store) UpdateState(id string, mutate func(*RunState)) error
- func (s *Store) UpdateStateAndMaybeDisable(id string, mutate func(*RunState)) (bool, error)
- type Template
- type Trigger
- type TriggerType
Constants ¶
const ( StatusRunning = "running" StatusSuccess = "success" StatusError = "error" StatusInterrupted = "interrupted" StatusSkipped = "skipped" )
Run terminal-status values (mirrored onto session.SessionMeta.TerminalStatus).
const ( KindScheduled = "scheduled" KindManual = "manual" )
Trigger-kind values stamped onto a run's session.
const ( SourceManual = "manual" SourceAgent = "agent" )
Source values record how an automation was created.
const AutoDisableThreshold = 5
AutoDisableThreshold is the number of consecutive failures (missing project, provider gone, etc.) after which a scheduled automation auto-disables so it stops re-failing — and re-notifying — every night. (PRD §11 open item N.)
const ScheduledRunCeiling = 30 * time.Minute
ScheduledRunCeiling bounds a scheduled (headless) run's wall-clock time. This is a liveness bound, not a safety guardrail: with ask_user/automation_create excluded from headless runs, the remaining hang vector is an agent loop, and an unbounded run would hold an engine forever. Manual runs are not capped.
Variables ¶
var ErrNotFound = errors.New("automation not found")
ErrNotFound is returned (wrapped) when an operation targets an automation id that does not exist, so HTTP handlers can map it to 404 rather than 400.
var ErrOwnerAutomationRunning = errors.New("related automation is running")
ErrOwnerAutomationRunning prevents a conversation deletion from detaching or deleting a related automation after it has already claimed a run.
Functions ¶
func Badge ¶
Badge renders a short cadence label for cards: "Daily" / "Weekly" / "Hourly" / "Manual" / "Once" / "Cron".
func ComputeNextRun ¶
ComputeNextRun returns the next instant strictly after `after` that matches the trigger, evaluated in after's own location (the host's local tz). It is a pure function (no time.Now) so it is fully unit-testable, including DST transitions — see schedule_test.go.
DST handling: each candidate is built with time.Date, which normalizes non-existent wall-clock times (spring-forward, e.g. a 02:30 daily on the day the clock jumps 02:00→03:00 lands on a real instant). Fall-back (a slot that occurs twice) is deduped by the caller via SlotKey/LastFiredSlot, not here.
Returns ok=false for non-schedule triggers (manual never auto-fires) and for a once trigger whose pinned time is not strictly in the future.
func ExecuteRun ¶
func ExecuteRun(ctx context.Context, store *Store, runner Runner, a *Automation, kind string) (string, error)
ExecuteRun runs an automation through the runner and records terminal state. It blocks until completion. Shared by the scheduler (scheduled fires) and the manual ▶ path so state bookkeeping is identical. For scheduled runs, repeated errors increment ConsecutiveFails and auto-disable past the threshold.
func HumanSchedule ¶
HumanSchedule renders a trigger for display, e.g. "Daily at 09:00", "Weekly on Mon at 14:30", "Hourly at :05", "Manual", "Once at 2026-09-04 15:00", or `Cron "*/15 * * * *"`.
func IsLocalPath ¶
IsLocalPath reports whether p is a usable local project path (non-empty, not a remote scheme). Used at fire time to skip+disable an automation pointed at a vanished or remote target.
func NextCronFire ¶ added in v0.13.4
NextCronFire returns the next instant strictly after `after` that matches the expression, evaluated in after's own location. ok=false when no match exists within the search window (legal-but-never-fires expression).
The scan walks day-by-day and builds each candidate with time.Date, which normalizes non-existent wall-clock times on spring-forward days (a 02:30 fire lands on the normalized real instant). Fall-back duplicates are deduped by the caller via SlotKey/LastFiredSlot, not here.
func SlotKey ¶
SlotKey is a stable dedup key for a fire instant: the local calendar minute. Two fires at the same wall-clock minute (e.g. a DST fall-back repeat) share a key, so LastFiredSlot can suppress the duplicate.
func ValidateAutomation ¶
func ValidateAutomation(a *Automation) error
ValidateAutomation is the single validation rule shared by every creation path (HTTP API, agent tool, CLI) — mirroring tools.ValidateGoalObjective. It mutates nothing; callers assign ID/timestamps/defaults around it.
Key invariants (PRD §7.5, §10.4): a local non-empty ProjectPath is required (no-project headless runs are unsafe and unsupported); remote (ssh://docker://) targets are rejected for v1.
Types ¶
type Automation ¶
type Automation struct {
ID string `json:"id"`
Name string `json:"name"`
Prompt string `json:"prompt"`
Trigger Trigger `json:"trigger"`
ProjectPath string `json:"project_path"` // required, must be a local path
ContextPolicy ContextPolicy `json:"context_policy,omitempty"`
OwnerSessionID string `json:"owner_session_id,omitempty"`
Mode string `json:"mode"` // approval|plan|full_access
Provider string `json:"provider,omitempty"`
Model string `json:"model,omitempty"`
RunInCloud bool `json:"run_in_cloud"` // reserved; always false in v1
Enabled bool `json:"enabled"`
Source string `json:"source"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
Automation is the user-edited definition. It persists in ~/.jcode/automations.json (low-frequency, human writes). Volatile scheduler bookkeeping lives separately in RunState (automation-state.json) so the scheduler's frequent writes never collide with human edits.
type Cadence ¶
type Cadence string
Cadence is the recurrence granularity for a scheduled trigger.
const ( CadenceHourly Cadence = "hourly" CadenceDaily Cadence = "daily" CadenceWeekly Cadence = "weekly" // CadenceCron fires on a 5-field cron expression (Trigger.Expr), evaluated // in the host's local timezone. Covers schedules the structured cadences // cannot express (weekday sets, sub-hour intervals, day-of-month pins). CadenceCron Cadence = "cron" )
type ContextPolicy ¶ added in v0.13.4
type ContextPolicy string
ContextPolicy controls which conversation context an automation uses.
const ( // ContextIsolated starts each run in a fresh automation session. ContextIsolated ContextPolicy = "isolated" // ContextConversation injects each run into OwnerSessionID so the task sees // that conversation's current (possibly compacted) history. ContextConversation ContextPolicy = "conversation" )
type CronExpr ¶ added in v0.13.4
type CronExpr struct {
// contains filtered or unexported fields
}
CronExpr is a parsed cron expression. Opaque; pass it back to NextCronFire.
func ParseCronExpr ¶ added in v0.13.4
ParseCronExpr parses a 5-field cron expression. The returned error names the offending field so agent-facing validation messages are actionable.
type RunState ¶
type RunState struct {
LastRunAt string `json:"last_run_at,omitempty"`
LastStatus string `json:"last_status,omitempty"` // running|success|error|interrupted|skipped
LastError string `json:"last_error,omitempty"`
LastSessionID string `json:"last_session_id,omitempty"`
NextRunAt string `json:"next_run_at,omitempty"`
LastFiredSlot string `json:"last_fired_slot,omitempty"` // SlotKey dedup guard (DST fall-back)
ConsecutiveFails int `json:"consecutive_fails,omitempty"`
}
RunState is the volatile per-automation scheduler bookkeeping. It persists in ~/.jcode/automation-state.json, written frequently by the scheduler and the run-completion callback — kept apart from Automation so the two write paths don't clobber each other.
type Runner ¶
type Runner interface {
StartRun(ctx context.Context, a *Automation, kind string) (sessionID string, err error)
}
Runner executes one automation run to completion. Implementations (internal/web) reuse the Engine: build a headless engine for the automation's project + mode, inject the prompt, and block until the agent is done. The returned sessionID identifies the recorded session.
type RunnerReadiness ¶ added in v0.13.4
type RunnerReadiness interface {
CanStart(a *Automation) bool
}
RunnerReadiness is an optional pre-claim gate. Conversation-backed runners use it to keep a due trigger pending while its owner conversation is busy, matching cron reinjection semantics without consuming a once trigger.
type Scheduler ¶
type Scheduler struct {
// contains filtered or unexported fields
}
Scheduler owns periodic firing for the process that wins the election lock. Non-owner processes can still manage definitions and trigger manual runs (which bypass the scheduler entirely).
func NewScheduler ¶
NewScheduler builds a scheduler. interval<=0 defaults to 30s.
func (*Scheduler) Run ¶
Run blocks until ctx is cancelled. It first contends for the election lock; if another process owns it, Run returns immediately (this process won't fire scheduled runs, but manual runs still work). The flock is released by the OS on process exit, so a crashed owner never deadlocks the election.
type SessionAutomationPolicy ¶ added in v0.13.4
type SessionAutomationPolicy string
SessionAutomationPolicy selects how conversation-bound automations are handled when their owner conversation is deleted.
const ( SessionAutomationDelete SessionAutomationPolicy = "delete" SessionAutomationDetach SessionAutomationPolicy = "detach" )
type SessionAutomationSnapshot ¶ added in v0.13.4
type SessionAutomationSnapshot struct {
Definitions []Automation
State map[string]RunState
}
SessionAutomationSnapshot is the rollback payload returned when resolving a conversation deletion. Definitions and run state are copied by value.
type SkipNotifier ¶
type SkipNotifier func(a *Automation, reason string)
SkipNotifier is called when the scheduler skips a fire without running (e.g. the bound project is gone). Optional.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store persists automations across processes. Writes take an OS file lock and re-read from disk before mutating, so concurrent jcode processes (web, TUI, CLI) never lose updates. Definitions and volatile run-state live in separate files so the scheduler's frequent state writes don't collide with human edits.
func NewStoreDir ¶
NewStoreDir opens a store rooted at an explicit directory (used by tests).
func (*Store) Create ¶
func (s *Store) Create(a Automation) (*Automation, error)
Create validates, assigns id/timestamps/defaults, and persists a new automation. The input is copied; the stored automation is returned.
func (*Store) Get ¶
func (s *Store) Get(id string) *Automation
Get returns a copy of the automation, or nil if not found.
func (*Store) List ¶
func (s *Store) List() []*Automation
List returns all automations sorted by creation time.
func (*Store) ListByOwnerSession ¶ added in v0.13.4
func (s *Store) ListByOwnerSession(sessionID string) ([]*Automation, error)
ListByOwnerSession returns a fresh cross-process view of every automation bound to sessionID. Disabled and already-fired definitions are included: the relationship, not the current armed state, determines deletion impact.
func (*Store) ResolveOwnerSession ¶ added in v0.13.4
func (s *Store) ResolveOwnerSession(sessionID string, policy SessionAutomationPolicy) (SessionAutomationSnapshot, error)
ResolveOwnerSession applies the user's explicit conversation-deletion policy atomically within the automation store. delete removes matching definitions and state; detach preserves their enabled state as isolated tasks and clears the owner.
func (*Store) RestoreSessionAutomations ¶ added in v0.13.4
func (s *Store) RestoreSessionAutomations(snapshot SessionAutomationSnapshot) error
RestoreSessionAutomations rolls back ResolveOwnerSession when the subsequent session-file deletion fails. It is idempotent and restores the exact captured definitions and run state.
func (*Store) SetEnabled ¶
func (s *Store) SetEnabled(id string, enabled bool) (*Automation, error)
SetEnabled flips the enabled flag (re-enabling clears ConsecutiveFails via Update's shared reset, so a recovered automation isn't immediately re-disabled by the next single failure).
func (*Store) TryClaimScheduledOnce ¶ added in v0.13.4
func (s *Store) TryClaimScheduledOnce(id, expectedAt string) (*Automation, bool, error)
TryClaimScheduledOnce conditionally claims and consumes a scheduled once run under one cross-process lock. expectedAt is the pin observed by the scheduler; if a concurrent edit changed that pin, the stale run is refused. On success it returns the current definition snapshot that was atomically claimed, marks the run as running, stamps the consumed slot, clears NextRunAt, and disarms the persisted definition before any agent work starts.
func (*Store) TryMarkRunning ¶
TryMarkRunning atomically claims a run for id: it sets LastStatus=running (clearing LastError, stamping LastRunAt) only if a run is not ALREADY in progress, returning whether the claim succeeded. This is the single authoritative guard against overlapping runs across the scheduler, manual "Run Now", and other processes — the local in-flight maps are only fast-path hints that can't see each other or another process. A crashed run left at "running" is cleared by the scheduler's reconcileStale on the next election.
func (*Store) Update ¶
func (s *Store) Update(id string, mutate func(*Automation)) (*Automation, error)
Update applies a mutation to an existing automation under lock, re-validating the result. The mutate callback receives a pointer it may modify in place.
Re-enabling (Enabled false -> true) also clears ConsecutiveFails so a recovered automation isn't immediately re-disabled by the next single failure. Centralizing the reset here means EVERY enable path gets it — the web UI's partial-patch PUT (handleUpdateAutomation), the CLI's SetEnabled, and any future caller — not just SetEnabled.
func (*Store) UpdateState ¶
UpdateState mutates only the volatile run-state file (never the definitions), so scheduler/run-completion writes can never clobber a concurrent human edit.
func (*Store) UpdateStateAndMaybeDisable ¶
UpdateStateAndMaybeDisable mutates the run-state (e.g. recording a failure and bumping ConsecutiveFails) and, in the SAME lock scope, disables the definition when ConsecutiveFails has reached AutoDisableThreshold. Folding the disable into the run-state mutation closes the TOCTOU window that exists when the disable is a separate SetEnabled(false) call: a concurrent successful run can no longer reset ConsecutiveFails between the threshold check and the disable. Returns whether the definition was disabled by this call.
type Template ¶
type Template struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Badge string `json:"badge"` // Daily|Weekly|Manual (display)
Prompt string `json:"prompt"`
Trigger Trigger `json:"trigger"`
SuggestMode string `json:"suggest_mode"`
}
Template is a built-in starting point shown on the Templates page. Selecting one pre-fills the editor; the user picks a project and confirms.
func BuiltinTemplates ¶
func BuiltinTemplates() []Template
BuiltinTemplates returns the curated templates (aligned with the reference UI).
type Trigger ¶
type Trigger struct {
Type TriggerType `json:"type"`
Cadence Cadence `json:"cadence,omitempty"`
Hour int `json:"hour,omitempty"` // 0-23, used by daily/weekly
Minute int `json:"minute,omitempty"` // 0-59, used by all cadences
Weekday int `json:"weekday,omitempty"` // 0=Sun..6=Sat, used by weekly
Expr string `json:"expr,omitempty"` // 5-field cron expression (schedule + cron)
At string `json:"at,omitempty"` // RFC3339 pinned time (once)
}
Trigger describes when an automation fires. Times are interpreted in the host's local timezone (see ComputeNextRun).
type TriggerType ¶
type TriggerType string
TriggerType is how an automation fires.
const ( // TriggerSchedule fires on a recurring wall-clock cadence. TriggerSchedule TriggerType = "schedule" // TriggerManual never fires automatically; only via an explicit run. TriggerManual TriggerType = "manual" // TriggerOnce fires exactly once at a pinned wall-clock time (Trigger.At), // then the scheduler auto-disarms it (Enabled=false) — the definition is // kept so the run record and prompt stay reviewable/editable. TriggerOnce TriggerType = "once" )