store#
import "github.com/jbcom/radioactive-ralph/internal/store"
Package store owns the single user-level SQLite database that is the durable memory for every project the supervisor knows about: project identity (accumulated fingerprints, §5b of the supervisor-architecture design), DB-resident project config, the plan DAG, the append-only event log, worker/session tracking with heartbeats, and spend accounting.
There is exactly ONE database per user (XDG data dir), not one per repo. Repos stay clean — nothing is committed by default.
The schema is embedded under schema/*.sql and applied in lexical order by Migrate. This package is a fresh port of internal/plandag’s store/task/plan machinery onto the new schema (project_id instead of repo_path, workers instead of session_variants, no persona/variant columns) with the PR #63 safety properties (DSN _txlock=immediate + synchronous(NORMAL), checked RowsAffected on claims, error-checked terminal writes) carried forward.
Index#
-
func Open(ctx context.Context, opts Options) (*Store, error)
func (s *Store) AddDep(ctx context.Context, planID, taskID, dependsOn string) error
func (s *Store) AppendMessage(ctx context.Context, o AppendMessageOpts) error
func (s *Store) Backup(ctx context.Context, destDir string) (string, error)
func (s *Store) ClearWorkerTask(ctx context.Context, workerID, status string) error
func (s *Store) CloseSession(ctx context.Context, sessionID string) error
func (s *Store) CountRunningWorkers(ctx context.Context) (int, error)
func (s *Store) CreatePlan(ctx context.Context, o CreatePlanOpts) (string, error)
func (s *Store) CreateSession(ctx context.Context, o SessionOpts) (string, error)
func (s *Store) CreateTask(ctx context.Context, o CreateTaskOpts) error
func (s *Store) CreateWorker(ctx context.Context, o WorkerOpts) (string, error)
func (s *Store) GetPlan(ctx context.Context, id string) (*Plan, error)
func (s *Store) GetProjectConfig(ctx context.Context, projectID string) (map[string]string, error)
func (s *Store) GetTask(ctx context.Context, planID, id string) (*Task, error)
func (s *Store) HeartbeatSession(ctx context.Context, sessionID string) error
func (s *Store) HeartbeatWorker(ctx context.Context, workerID string) error
func (s *Store) HeartbeatWorkerAndSession(ctx context.Context, workerID string) error
func (s *Store) ListMessages(ctx context.Context, planID, taskID string) ([]A2AMessage, error)
func (s *Store) ListProjectEvents(ctx context.Context, projectID string, limit int) ([]Event, error)
func (s *Store) ListRunningWorkers(ctx context.Context) ([]RunningWorker, error)
func (s *Store) ListTasks(ctx context.Context, planID string, statuses []TaskStatus) ([]Task, error)
func (s *Store) MaxEventID(ctx context.Context, projectID string) (int64, error)
func (s *Store) Ready(ctx context.Context, planID string) ([]Task, error)
func (s *Store) ReclaimWorker(ctx context.Context, workerID string) (found bool, err error)
func (s *Store) RecordSpend(ctx context.Context, o RecordSpendOpts) error
func (s *Store) ReleaseClaim(ctx context.Context, planID, taskID, sessionID, reason string) error
func (s *Store) SetPlanStatus(ctx context.Context, id string, status PlanStatus) error
func (s *Store) SetProjectConfig(ctx context.Context, projectID, key, value string) error
func (s *Store) SetWorkerTask(ctx context.Context, workerID, planID, taskID string) error
func (s *Store) StatusCounts(ctx context.Context) (StatusCounts, error)
func (s *Store) TouchProjectLastSeen(ctx context.Context, projectID string) error
Constants#
Fingerprint identity kinds. A project is identified by the SET of these rows in project_identifiers, not by a single fragile path — see §5b of the supervisor-architecture design.
const (
FingerprintKindAbsPath = "abs_path"
FingerprintKindGitRootCommit = "git_root_commit"
FingerprintKindGitRemote = "git_remote"
)
Variables#
ErrDuplicateSlug is returned when a plan with (project_id, slug) already exists. Callers either pick a new slug or update.
var ErrDuplicateSlug = errors.New("store: plan with slug already exists in this project")
ErrDuplicateTask is returned by CreateTask when a task with the same (plan_id, id) already exists. It is the ONE benign CreateTask failure a concurrent dispatcher expects (two dispatchers racing to materialize the same step); callers match it via errors.Is and fall through to the claim attempt. Every OTHER CreateTask error (disk full, DB busy, I/O, a validation failure) is a real fault that must NOT be silently swallowed.
var ErrDuplicateTask = errors.New("store: task with this id already exists in this plan")
ErrNoReadyTask indicates ClaimNextReady found nothing claimable.
var ErrNoReadyTask = errors.New("store: no ready task")
ErrPlanNotFound is returned when an operation targets a plan id that no row matches. It is a typed sentinel so callers match with errors.Is rather than scraping the formatted message (the drive API maps it to CodeNotFound).
var ErrPlanNotFound = errors.New("store: plan not found")
ErrTaskNotOwnedRunning is returned by MarkFailed* when the task is no longer both running AND claimed by the reporting session — i.e. it was reclaimed by the reaper and (possibly) reassigned to another worker between dispatch and this failure report. It is a BENIGN outcome, not a hard error: the stale report is correctly dropped instead of stomping the current owner. Callers distinguish it via errors.Is and treat it as “someone else owns this now; nothing to do.”
var ErrTaskNotOwnedRunning = errors.New("store: task not running under the reporting session (stale failure report)")
func DSN#
func DSN(dbPath string) string
DSN builds the canonical modernc.org/sqlite DSN for the user-level store database at dbPath. Every process that opens the store (the durable supervisor, the TUI, and the CLI) MUST use this so they share identical locking and durability semantics.
_txlock=immediate makes every transaction take the write lock up front, so a SELECT-then-UPDATE (e.g. ClaimNextReady) can never race another process into SQLITE_BUSY_SNAPSHOT — which busy_timeout does NOT retry. busy_timeout then actually serializes the concurrent writers instead of failing them immediately. synchronous=NORMAL is the documented-safe pairing with WAL and avoids an fsync on every heartbeat/tick write.
The path is percent-encoded per the SQLite file: URI rules so a dbPath containing ‘?’, ‘#’, or ‘%’ is not misparsed as URI syntax and pointed at the wrong database.
func Migrate#
func Migrate(db *sql.DB) error
Migrate brings db up to currentSchemaVersion by applying any pending *.up.sql migrations in lexical order.
Migrations are idempotent per-version via SQLite’s user_version PRAGMA. Each migration runs inside a transaction.
type A2AMessage#
A2AMessage is one durable row in the a2a_messages evidence/message log. content_json holds the serialized a2a.Message (see internal/a2a); this package treats it as an opaque string so internal/store has no import dependency on internal/a2a.
type A2AMessage struct {
ID int64
WorkerID string
PlanID string
TaskID string
Role string
ContentJSON string
OccurredAt string
}
type AppendMessageOpts#
AppendMessageOpts configures AppendMessage.
type AppendMessageOpts struct {
WorkerID string // optional
PlanID string
TaskID string
Role string // e.g. "ROLE_AGENT" | "ROLE_USER"
ContentJSON string // the serialized a2a.Message
}
type CreatePlanOpts#
CreatePlanOpts configures plan creation. The caller supplies slug + title; ID is generated via Store.uuid().
type CreatePlanOpts struct {
ProjectID string
Slug string
Title string
SourceMarkdown string
TagsJSON string
}
type CreateTaskOpts#
CreateTaskOpts configures task creation.
type CreateTaskOpts struct {
PlanID string
ID string // stable slug (operator-chosen or fixit-emitted)
Description string
ParallelGroup *int64
SequenceOrdinal *int64
AcceptanceJSON string
ParentTaskID string
// RequiresApproval materializes the task in 'ready_pending_approval'
// instead of the default 'pending', holding it out of dispatch until an
// operator approves it (ApproveTask → 'ready' → claimable). This is the
// producer for the human-in-the-loop approval gate; a plan step carries it
// via the [approval] marker (see internal/plan).
RequiresApproval bool
}
type EmitOpts#
EmitOpts configures Emit.
type EmitOpts struct {
ProjectID string
PlanID string
TaskID string
Kind string
Stream string
Actor string
PayloadJSON string
}
type Event#
Event is one append-only audit-log row. Project-scoped; may additionally be plan/task-scoped. Both the old per-repo event log and task_events collapse into this single table (see schema §events).
type Event struct {
ID int64
ProjectID string
PlanID string
TaskID string
Kind string
Stream string
Actor string
PayloadJSON string
OccurredAt time.Time
}
type EventPayload#
EventPayload keeps event payloads structured so the CLI, TUI, and tests can reason about approvals, handoffs, retries, and provider context without string scraping.
type EventPayload struct {
Summary string `json:"summary,omitempty"`
Reason string `json:"reason,omitempty"`
Evidence []string `json:"evidence,omitempty"`
HandoffTo string `json:"handoff_to,omitempty"`
Retryable bool `json:"retryable,omitempty"`
NeedsContext []string `json:"needs_context,omitempty"`
ApprovalRequired bool `json:"approval_required,omitempty"`
Provider string `json:"provider,omitempty"`
ProviderSessionID string `json:"provider_session_id,omitempty"`
OperatorAction string `json:"operator_action,omitempty"`
}
type Fingerprint#
Fingerprint is one accumulated identity signal for a project.
type Fingerprint struct {
Kind string
Value string
}
func Fingerprints#
func Fingerprints(ctx context.Context, dir string) ([]Fingerprint, error)
Fingerprints computes the identity fingerprints for a directory: always the cleaned absolute path, plus — best-effort, if dir is a git repository — the root-commit sha and any “origin” remote URL. A fingerprint whose git command fails (e.g. no origin remote configured) is silently skipped rather than failing the whole call.
type Options#
Options configures Open.
type Options struct {
// DSN is a modernc.org/sqlite DSN. Prefer building it with DSN() so
// every opener shares identical locking/durability pragmas.
DSN string
// Clock is swappable for tests. Nil defaults to clockwork.NewRealClock().
Clock clockwork.Clock
// UUID is swappable for tests. Nil defaults to uuid.NewV7().
UUID func() string
}
type Plan#
Plan is the durable row, keyed by project (not repo_path — see §5b).
type Plan struct {
ID string
ProjectID string
Slug string
Title string
Status PlanStatus
SourceMarkdown string
CreatedAt time.Time
UpdatedAt time.Time
LastSessionAt sql.NullTime
TagsJSON string // JSON array
}
type PlanStatus#
PlanStatus enumerates the lifecycle states a plan can be in.
type PlanStatus string
const (
PlanStatusDraft PlanStatus = "draft"
PlanStatusActive PlanStatus = "active"
PlanStatusPaused PlanStatus = "paused"
PlanStatusDone PlanStatus = "done"
PlanStatusFailedPartial PlanStatus = "failed_partial"
PlanStatusArchived PlanStatus = "archived"
PlanStatusAbandoned PlanStatus = "abandoned"
)
type RecordSpendOpts#
RecordSpendOpts configures RecordSpend.
type RecordSpendOpts struct {
ProjectID string
WorkerID string // optional
Provider string
Model string
InputTokens int
OutputTokens int
CachedTokens int
CostUSD float64
}
type RunningWorker#
RunningWorker is one currently-running worker row, projected for the observe surface (status/GUI): the worker’s own id — which WorkerKill/ReclaimWorker key on — plus what it is working on. Distinct from WorkerOpts (create-time input) and from any provider-session id.
type RunningWorker struct {
ID string
Provider string
PlanID string
TaskID string
}
type SessionOpts#
SessionOpts configures CreateSession.
type SessionOpts struct {
ID string // optional; caller may pass an existing UUID. Empty → auto.
Role string // supervisor|client
PID int
PIDStartTime string
Host string
}
type StatusCounts#
StatusCounts is the aggregate view the status reply exposes: how many active plans exist and how many tasks sit in each status a client surfaces. Sourced from one pass over the store so `status` reports real numbers rather than the zeros the reply carried before (the task/plan counters were never populated).
type StatusCounts struct {
ActivePlans int
Ready int
Running int
Approval int // ready_pending_approval
Blocked int
Failed int
}
type Store#
Store is the user-level store handle. It wraps a *sql.DB plus a deterministic clock + UUID provider (test-swappable).
type Store struct {
// contains filtered or unexported fields
}
func Open#
func Open(ctx context.Context, opts Options) (*Store, error)
Open returns a migrated, ready-to-use Store.
func (*Store) AddDep#
func (s *Store) AddDep(ctx context.Context, planID, taskID, dependsOn string) error
AddDep wires task → depends_on for the same plan. Rejects cycles.
func (*Store) AddProjectIdentifiers#
func (s *Store) AddProjectIdentifiers(ctx context.Context, projectID string, fps []Fingerprint) error
AddProjectIdentifiers accumulates additional fingerprints onto an existing project. Idempotent and safe to call repeatedly: re-adding a fingerprint THIS project already owns refreshes its added_at (so most-recent-wins ordering stays correct after a move); a fingerprint owned by a DIFFERENT project is left untouched (never stolen), since (kind, value) is the primary key and the UPSERT is guarded on the owning project_id — see insertIdentifiers.
func (*Store) AppendMessage#
func (s *Store) AppendMessage(ctx context.Context, o AppendMessageOpts) error
AppendMessage records one worker<->orchestrator A2A message (most importantly, evidence a worker submits when it believes a task is done). This ONLY logs the message — it never changes task status. Only internal/orch.VerifyAndComplete may transition a task to done.
func (*Store) ApproveTask#
func (s *Store) ApproveTask(ctx context.Context, planID, taskID string) (found, changed bool, err error)
ApproveTask clears the approval gate on a task: it transitions a ready_pending_approval task to ready so dispatch can pick it up. Idempotent on the desired end state — approving a task that is already ready (or past it) returns found=true, changed=false rather than erroring. found=false (no error) when the task doesn’t exist, so the caller can surface CodeNotFound.
func (*Store) Backup#
func (s *Store) Backup(ctx context.Context, destDir string) (string, error)
Backup writes a consistent point-in-time copy of the store database into destDir using SQLite’s `VACUUM INTO`, which — unlike a raw file copy — is safe to run against a live database (it takes a read transaction and produces a compacted, fully-consistent snapshot even while other connections are writing under WAL). Returns the path to the backup file.
func (*Store) ClaimNextReady#
func (s *Store) ClaimNextReady(ctx context.Context, planID, sessionID, workerID string) (*Task, error)
ClaimNextReady is the atomic “claim the next ready task for this worker” operation. Returns the claimed task, or ErrNoReadyTask if none. Uses BEGIN (with _txlock=immediate at the DSN level) + UPDATE with a checked RowsAffected so two parallel workers never claim the same task.
func (*Store) ClearWorkerTask#
func (s *Store) ClearWorkerTask(ctx context.Context, workerID, status string) error
ClearWorkerTask clears the active task from one worker row and marks it idle or terminated (status defaults to “idle” when empty).
func (*Store) Close#
func (s *Store) Close() error
Close releases DB resources.
func (*Store) CloseSession#
func (s *Store) CloseSession(ctx context.Context, sessionID string) error
CloseSession removes a session row. FK cascades clear its workers.
func (*Store) CountRunningWorkers#
func (s *Store) CountRunningWorkers(ctx context.Context) (int, error)
CountRunningWorkers returns how many worker rows are currently status=’running’ — i.e. actively assigned a task, not idle/terminated/ crashed. Used by the supervisor’s HandleStatus to report a real ActiveWorkers count sourced from the store rather than an in-process map that only the dispatcher owning the pty could ever populate.
func (*Store) CreatePlan#
func (s *Store) CreatePlan(ctx context.Context, o CreatePlanOpts) (string, error)
CreatePlan inserts a fresh plan in draft status and returns the newly generated UUID v7 id.
func (*Store) CreateProject#
func (s *Store) CreateProject(ctx context.Context, displayName string, fps []Fingerprint) (string, error)
CreateProject inserts a new project row and seeds it with the given fingerprints. Returns the newly generated UUID v7 id.
func (*Store) CreateSession#
func (s *Store) CreateSession(ctx context.Context, o SessionOpts) (string, error)
CreateSession inserts a session row. Returns the session id. The row lifetime matches one supervisor or client process attached to this DB.
func (*Store) CreateTask#
func (s *Store) CreateTask(ctx context.Context, o CreateTaskOpts) error
CreateTask inserts a new task — ‘pending’ by default, or ‘ready_pending_approval’ when o.RequiresApproval is set (held behind the approval gate). Callers wire dependencies via AddDep.
func (*Store) CreateWorker#
func (s *Store) CreateWorker(ctx context.Context, o WorkerOpts) (string, error)
CreateWorker registers a newly-spawned agent subprocess against a session. Returns the worker row id. Successor to plandag’s CreateSessionVariant — no persona; carries the provider capability (native_fanout) instead of a variant name (§9/§10 of the design).
func (*Store) DB#
func (s *Store) DB() *sql.DB
DB returns the underlying *sql.DB for callers that need raw access. Business-logic callers should use Store’s typed methods instead.
func (*Store) Emit#
func (s *Store) Emit(ctx context.Context, o EmitOpts) error
Emit appends one event row. Used for events not already covered by a more specific transactional helper (e.g. task claim/done/failed emit their own events inline so the status transition and audit row commit atomically).
func (*Store) EventsAfter#
func (s *Store) EventsAfter(ctx context.Context, projectID string, afterID int64, limit int) ([]Event, error)
EventsAfter returns a project’s events with id strictly greater than afterID, in ascending id order, capped at limit. It is the tail query backing the Attach event stream: a client resumes from its last-seen id and each call returns the next contiguous batch. Ascending order is deliberate — events are delivered oldest-first so a live view applies them in the order they occurred (the opposite of ListProjectEvents, which is newest-first for a backlog snapshot). Pass afterID=0 to start from the beginning. Scoping includes plan-linked events (see eventProjectScope).
func (*Store) GetPlan#
func (s *Store) GetPlan(ctx context.Context, id string) (*Plan, error)
GetPlan loads a plan by id.
func (*Store) GetProjectConfig#
func (s *Store) GetProjectConfig(ctx context.Context, projectID string) (map[string]string, error)
GetProjectConfig returns all DB-resident config key/value pairs for a project. Values are stored as JSON-encoded scalars/arrays/objects (the caller decodes); this layer treats them as opaque strings.
func (*Store) GetTask#
func (s *Store) GetTask(ctx context.Context, planID, id string) (*Task, error)
GetTask loads one task by (plan_id, id).
func (*Store) HeartbeatSession#
func (s *Store) HeartbeatSession(ctx context.Context, sessionID string) error
HeartbeatSession refreshes last_heartbeat for a session. Called periodically by the durable supervisor. The reaper uses staleness to detect dead sessions.
func (*Store) HeartbeatWorker#
func (s *Store) HeartbeatWorker(ctx context.Context, workerID string) error
HeartbeatWorker refreshes one worker row’s heartbeat.
func (*Store) HeartbeatWorkerAndSession#
func (s *Store) HeartbeatWorkerAndSession(ctx context.Context, workerID string) error
HeartbeatWorkerAndSession refreshes both a worker row AND its owning session row in one call. A worker’s session (spawnWorkerRows) is not heartbeated by anything else — only the supervisor’s own session is (HeartbeatSession) — so without this a live worker’s session goes stale and the reaper’s step-2 session delete would CASCADE-delete the still-running worker, NULLing its task’s claim and letting branch (b) re-dispatch a live task (double execution). Beating both keeps the session as fresh as the worker for the reaper’s staleness math. The two UPDATEs share one tx so a mid-call failure never leaves the pair inconsistent.
func (*Store) ListMessages#
func (s *Store) ListMessages(ctx context.Context, planID, taskID string) ([]A2AMessage, error)
ListMessages returns every message logged for one task, oldest first.
func (*Store) ListPlans#
func (s *Store) ListPlans(ctx context.Context, projectID string, statuses []PlanStatus) ([]Plan, error)
ListPlans returns plans for a project matching filter. Empty filter → active + paused. Empty projectID lists across all projects.
func (*Store) ListProjectEvents#
func (s *Store) ListProjectEvents(ctx context.Context, projectID string, limit int) ([]Event, error)
ListProjectEvents returns the most recent events for one project, newest first. Scoping includes plan-linked events (see eventProjectScope) — the headline lifecycle rows (task.claimed/done/failed/…) carry only plan_id and no project_id, so a bare `project_id = ?` filter would silently drop exactly the events a backlog/overview view exists to show, and would DISAGREE with the live tail (EventsAfter uses the same scope). Keeping both on eventProjectScope means the macro/backlog snapshot and the live stream show the same set.
func (*Store) ListRunningWorkers#
func (s *Store) ListRunningWorkers(ctx context.Context) ([]RunningWorker, error)
ListRunningWorkers returns every worker row currently status=’running’, with the worker id and its current plan/task. It is the read backing the status reply’s per-worker detail so a client (the GUI) can name a specific worker to kill. Workers with no assigned task are still listed (PlanID/TaskID empty).
func (*Store) ListTaskEvents#
func (s *Store) ListTaskEvents(ctx context.Context, planID, taskID string, limit int) ([]Event, error)
ListTaskEvents returns the most recent events for one task first.
func (*Store) ListTasks#
func (s *Store) ListTasks(ctx context.Context, planID string, statuses []TaskStatus) ([]Task, error)
ListTasks returns tasks for one plan, optionally filtered by status.
func (*Store) MarkBlocked#
func (s *Store) MarkBlocked(ctx context.Context, planID, taskID, sessionID string, payload EventPayload) error
MarkBlocked releases a running task into the blocked set so an operator can later requeue or otherwise intervene.
func (*Store) MarkDone#
func (s *Store) MarkDone(ctx context.Context, planID, taskID, sessionID string, evidenceJSON string) ([]Task, error)
MarkDone transitions a running task to done, logs the event, and returns the set of newly-ready downstream tasks.
func (*Store) MarkFailed#
func (s *Store) MarkFailed(ctx context.Context, planID, taskID, sessionID, reason string, maxRetries int) (retried bool, err error)
MarkFailed transitions a running task, owned by sessionID, to failed or retries. See MarkFailedWithPayload.
func (*Store) MarkFailedWithPayload#
func (s *Store) MarkFailedWithPayload(ctx context.Context, planID, taskID, sessionID string, payload EventPayload, maxRetries int) (retried bool, err error)
MarkFailedWithPayload transitions a running task to failed or retries while preserving structured payload details in the event log.
Both UPDATEs are guarded by `status = ‘running’ AND claimed_by_session = sessionID`, the same owner guard MarkDone and MarkBlocked carry. Without the owner guard a stale failure report from a worker whose claim the reaper already reclaimed (and possibly reassigned to a new worker) would flip the task back to pending / failed and clear the NEW owner’s claim — double execution plus a dropped completion. When the guard matches nothing the task has moved on under a different session; MarkFailed returns ErrTaskNotOwnedRunning so the caller can drop the stale report rather than resurrect the task.
func (*Store) MaxEventID#
func (s *Store) MaxEventID(ctx context.Context, projectID string) (int64, error)
MaxEventID returns the highest event id for a project, or 0 if the project has no events. It gives an Attach client the initial cursor to resume from (the client reads this — or the backlog’s max id — then attaches with it), so the client owns a single monotonic cursor and no event slips through the gap between a backlog read and the live stream. Scoping matches EventsAfter (it includes plan-linked rows) so the two agree on project membership.
func (*Store) ProjectAbsPath#
func (s *Store) ProjectAbsPath(ctx context.Context, projectID string) (path string, found bool, err error)
ProjectAbsPath returns the most recently recorded absolute-path fingerprint for a project, or found=false when the project has no abs_path identifier. The orchestrator uses this to launch workers in the project’s own checkout: supervisor mode’s working directory is deliberately irrelevant (§4), so a dispatch must resolve the target repo explicitly rather than inheriting the supervisor process’s cwd.
A project can accumulate more than one abs_path (the same repo cloned to two locations, or moved); the most recently added one is returned as the best current guess at where the operator is working now.
func (*Store) ProjectSpendByProvider#
func (s *Store) ProjectSpendByProvider(ctx context.Context, projectID string) (map[string]float64, error)
ProjectSpendByProvider sums cost_usd for one project, grouped by provider. Used to enforce a per-provider spend cap before dispatch.
func (*Store) Ready#
func (s *Store) Ready(ctx context.Context, planID string) ([]Task, error)
Ready returns tasks that are ready to run — every dependency is in a terminal-satisfied state (`done`, `skipped`, or `decomposed`) — and are in a claimable status: `pending` (ungated) or `ready` (a task that WAS gated behind approval and has since been approved via ApproveTask). A task still in `ready_pending_approval` is deliberately NOT returned: the approval gate holds it until an operator approves it. Result is ordered by created_at for stable test output.
func (*Store) ReclaimStale#
func (s *Store) ReclaimStale(ctx context.Context, staleAfter time.Duration) (reclaimed int, err error)
ReclaimStale is the in-store reaper: the old daemon never implemented this, so a crashed worker permanently wedged its claimed task. It:
Requeues tasks whose claiming worker’s last_heartbeat is older than staleAfter — increments reclaim_count, sets status back to pending, and clears the claim so ClaimNextReady can hand it to a live worker.
Deletes worker and session rows stale beyond a longer window (staleSessionMultiplier * staleAfter), once any of their claims have already been reclaimed by step 1.
Returns the number of tasks reclaimed in step 1.
func (*Store) ReclaimWorker#
func (s *Store) ReclaimWorker(ctx context.Context, workerID string) (found bool, err error)
ReclaimWorker forcibly reclaims a worker’s in-flight task and marks the worker terminated — the store side of an operator/GUI “kill this worker” action. It mirrors the reaper’s reclaim: the worker’s running task (if any) goes back to ‘pending’ with its claim cleared (so it re-dispatches), and the worker row is marked terminated. found=false (no error) when workerID is unknown, so a kill of an already-gone worker is a benign no-op the caller can surface as CodeNotFound. The actual subprocess is killed by the orchestrator/provider layer that owns the pty; this only does the store-side bookkeeping.
func (*Store) RecordSpend#
func (s *Store) RecordSpend(ctx context.Context, o RecordSpendOpts) error
RecordSpend appends one spend row for a completed provider turn. Used by the orchestrator to accumulate provider.Usage.CostUSD per project so spend caps (configured per provider/variant) can be enforced before the next dispatch.
func (*Store) ReleaseClaim#
func (s *Store) ReleaseClaim(ctx context.Context, planID, taskID, sessionID, reason string) error
ReleaseClaim requeues a running task owned by sessionID back to pending WITHOUT charging a retry, for SYSTEM-level aborts (e.g. a fan-out group whose later claim failed) as opposed to task-execution failures. Using MarkFailed here would penalize the retry budget for something the task never got a chance to attempt, and could terminally fail an otherwise-valid task after a few transient orchestrator hiccups. Owner-guarded like MarkFailed: a task no longer running under sessionID yields ErrTaskNotOwnedRunning (benign — someone else owns it now).
func (*Store) ResolveProject#
func (s *Store) ResolveProject(ctx context.Context, fps []Fingerprint) (projectID string, found bool, err error)
ResolveProject looks up an existing project by matching ANY of the given fingerprints against project_identifiers. Returns found=false (no error) when no fingerprint matches.
func (*Store) SetPlanStatus#
func (s *Store) SetPlanStatus(ctx context.Context, id string, status PlanStatus) error
SetPlanStatus updates the plan’s status column.
func (*Store) SetProjectConfig#
func (s *Store) SetProjectConfig(ctx context.Context, projectID, key, value string) error
SetProjectConfig upserts one DB-resident config key/value pair for a project.
func (*Store) SetWorkerTask#
func (s *Store) SetWorkerTask(ctx context.Context, workerID, planID, taskID string) error
SetWorkerTask updates the currently assigned plan/task for one worker row and refreshes its heartbeat.
func (*Store) StatusCounts#
func (s *Store) StatusCounts(ctx context.Context) (StatusCounts, error)
StatusCounts returns the plan/task aggregates for the status reply, across all projects (the supervisor is project-agnostic). Counts are derived from the live rows so they stay accurate without any in-process bookkeeping.
func (*Store) TouchProjectLastSeen#
func (s *Store) TouchProjectLastSeen(ctx context.Context, projectID string) error
TouchProjectLastSeen updates last_seen_at to now. Called whenever a project is resolved/used so operators can see recency.
type Task#
Task is a DAG node. No variant/persona columns — the orchestrator assigns a worker from plan structure, not from a baked persona (§10 of the supervisor-architecture design).
type Task struct {
ID string
PlanID string
Description string
Status TaskStatus
ParallelGroup sql.NullInt64
SequenceOrdinal sql.NullInt64
AcceptanceJSON string
ClaimedBySession string
ClaimedByWorkerID string
RetryCount int
ReclaimCount int
ParentTaskID string
CreatedAt time.Time
UpdatedAt time.Time
}
type TaskStatus#
TaskStatus enumerates valid task lifecycle states.
type TaskStatus string
const (
TaskStatusPending TaskStatus = "pending"
TaskStatusReady TaskStatus = "ready"
TaskStatusReadyPendingApproval TaskStatus = "ready_pending_approval"
TaskStatusBlocked TaskStatus = "blocked"
TaskStatusRunning TaskStatus = "running"
TaskStatusDone TaskStatus = "done"
TaskStatusFailed TaskStatus = "failed"
TaskStatusSkipped TaskStatus = "skipped"
TaskStatusDecomposed TaskStatus = "decomposed"
)
type WorkerOpts#
WorkerOpts configures CreateWorker.
type WorkerOpts struct {
SessionID string
Provider string
Model string
NativeFanout bool
SubprocessPID int
SubprocessStartTime string
}
Generated by gomarkdoc