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#

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

Plan lifecycle states.

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:

  1. 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.

  2. 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

Task lifecycle states.

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