queue

package module
v0.3.3 Latest Latest
Warning

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

Go to latest
Published: Oct 7, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package queue is the store CONTRACT module: the Store interface, Filter, and the Queue facade that embedders program against (ADR-0012).

Drivers live in separate modules and implement this contract: internal/queue/sqlite (embedded, one file, single serialized writer) and internal/queue/postgres (shared across machines, SKIP LOCKED claims); both are held to identical semantics by mirrored test suites (ADR-0007). Every mutating operation also appends a fact to the journal in the same transaction, so the journal is always a complete, consistent history of the queue.

Index

Constants

View Source
const (
	// BacklogMax is the ceiling backlog computations clamp to: no scorer,
	// keyword rule, or importance value reaches the hot band.
	BacklogMax = 99
	// HotMin is the floor of the hot band.
	HotMin = 100
	// HotMax is the ceiling of the hot band.
	HotMax = 149
	// MachineMin is the floor of the machine band.
	MachineMin = 150
)

Band boundaries of the priority scale (ADR-0015 §1).

View Source
const (
	// PriorityAgingDaysPerPoint is the task age — keyed on created_at, the
	// only immutable timestamp in the row (updated_at moves on every
	// heartbeat) — that earns one priority point.
	PriorityAgingDaysPerPoint = 3
	// PriorityAgingMaxBonus caps the age bonus. It stays below the smallest
	// marker-level gap (20, ADR-0015 §2) so aging can reorder within a band
	// but never across marker levels or bands.
	PriorityAgingMaxBonus = 10
)

Priority aging (ADR-0015 §4): ClaimDue orders by an EFFECTIVE priority — stored priority plus a bounded age bonus computed inside the claim query (scheduling, not state; the stored priority never changes). Defined once here, referenced by both backends, so the semantics cannot drift.

View Source
const (
	RequeueClassPreflight = "preflight"  // executor refused to start (dirty tree, missing autonomy)
	RequeueClassGate      = "gate"       // verify gate failed without judging the task (dead/slow)
	RequeueClassRateLimit = "rate-limit" // provider 429 / usage window
	RequeueClassQuestion  = "question"   // owner question parked the run
	RequeueClassBudget    = "budget"     // claim-time spend gate blocked the paid turn
	RequeueClassUnknown   = "unknown"    // legacy task.requeued facts predating the field
)

RequeueEvidence is the structured detail on task.requeued facts: why the executor refused to start and how long the task waits before it becomes claimable again (01:48 report f2: the reason was a plain error string). ResumeCloseout marks the rate-limited-close-out park: the work turn is DONE and its session alive, so the re-claim resumes at close-out (16-00 report f31) — without the flag the journal cannot distinguish a parked work turn from an owed close-out. RequeueClass values for RequeueEvidence.Class: WHICH not-the-task's- fault refusal returned the task to Pending, so downstream readers (stats, sweepers, drift audits) can distinguish the environmental classes without re-parsing the reason text. Readers normalize a missing class (legacy facts predate the field) to RequeueClassUnknown; no journal backfill rewrites history.

View Source
const (
	QuestionTypeInfo         = "info"
	QuestionTypeApproval     = "approval"
	QuestionTypeConfirmation = "confirmation"
	QuestionTypeInput        = "input"
)

Valid question types, mirroring PapDashboard's question kinds: the ask surface validates against these so a forwarded question can never be rejected by the ingest route for an unknown type.

View Source
const ClaimAnomalyThreshold = 20

ClaimAnomalyThreshold is the loop-suspect line: a task claimed more often than this is churning (the P1 class — a 164-claim task drained a 30/day budget for two days before anyone looked). tq stats and the webui nowband flag suspects at this count; advisory, never a gate.

View Source
const PrioritySourceUnblock = "unblock"

PrioritySourceUnblock labels task.reprioritized facts written by the unblock bump (the ADR-0015 source enum).

View Source
const UnblockBumpPriority = 15

UnblockBumpPriority is the priority bump a task gains when ALL its dependencies have completed (ADR-0015 unblock bump): half a marker level — meaningful against importance spacing, never a full ladder jump. Clamped to the backlog band on use.

Variables

View Source
var ErrEmptyAnswer = errors.New("queue: record answer needs a non-empty answer")

ErrEmptyAnswer is returned by RecordAnswer when the answer text is blank (a ruling that says nothing cannot be rendered into the parked prompt).

View Source
var ErrEmptyAnswerRef = errors.New("queue: record answer needs a question ref")

ErrEmptyAnswerRef is returned by RecordAnswer when the answer record carries no question ref (the correlation key of the park/unblock flow).

View Source
var ErrEmptyType = errors.New("queue: task type must not be empty")

ErrEmptyType is returned by Enqueue when New.Type is empty (Normalize cannot invent a type; callers must choose an executor).

View Source
var ErrNoTaskDue = errors.New("queue: no due task")

ErrNoTaskDue is returned by ClaimDue when nothing is claimable right now.

View Source
var ErrTaskDone = errors.New("queue: dedup key already completed; re-dispatch refused (edit the item text to re-arm)")

ErrTaskDone is the enqueue done-guard: the dedup key matched a stored task that already COMPLETED, so a re-dispatch is refused. Without it a dispatcher that treats the returned row as admission would mint a fresh window for already-finished work (the DONE-on-arrival burn class). The guard lives on the Queue wrapper, not the Store: cancelled/dead keys keep their documented suppress-and-return semantics (rescue and re-arm flows read the stored row), and re-arming a completed item deliberately means a fresh key (edit the item text).

Functions

func ClaimCount added in v0.3.1

func ClaimCount(ctx context.Context, s Store, id string) (int, error)

ClaimCount reads one task's claim count (task.claimed facts) from its journal trail. Census callers should expect one facts walk per task.

art-dupl:accept replace-free module boundary: cmd/tq cannot import this package (ADR-0017) and restates the 12-line census helper.

func ClampBacklog added in v0.3.0

func ClampBacklog(priority int) int

ClampBacklog clamps a computed backlog priority into [0, BacklogMax] so no derived score can smuggle a task into the hot or machine band (ADR-0015 §1: crossing a band boundary requires an explicit act).

func CountStuckRunning added in v0.3.1

func CountStuckRunning(ctx context.Context, store Store, now time.Time) int

CountStuckRunning counts Running tasks whose lease expired without a reclaim — work that WOULD run if any worker were claiming. It is the one expired-lease probe shared by `tq doctor` and the webui health dashboard, so the two operator surfaces cannot drift. A list error counts zero: callers surface store failures through their own checks.

func ValidQuestionType added in v0.3.1

func ValidQuestionType(t string) bool

ValidQuestionType reports whether t is one of the PapDashboard question kinds.

Types

type AnswerRecord added in v0.3.1

type AnswerRecord struct {
	// Ref is the question ref being answered. Required.
	Ref string
	// Answer is the owner's decision text. Required.
	Answer string
	// PapID is the PapDashboard question ID, for provenance. Optional.
	PapID string
	// AnsweredAt is when the answer was recorded on PapDashboard. Zero
	// means now.
	AnsweredAt time.Time
	// Question optionally carries the asked text (the PapDashboard body
	// carries it); when empty, RecordAnswer copies it from the matching
	// task.question-asked fact so the answered fact stays self-contained.
	Question string
}

AnswerRecord is the RecordAnswer input: the owner's decision arriving from PapDashboard, correlated to the asking task by Ref (the question ref from the task.question-asked fact).

type Band added in v0.3.0

type Band string

Band names one range of the single priority scale (ADR-0015 §1). One scale, three reserved bands: backlog computations clamp below hot; crossing upward requires an explicit human/flag act.

const (
	// BandBacklog: 0–99 — humans (markers), importance, AI scores,
	// keyword fallback.
	BandBacklog Band = "backlog"
	// BandHot: 100–149 — same-session urgency (--same-session-priority)
	// only.
	BandHot Band = "hot"
	// BandMachine: 150+ — machine-generated operational tasks (cqa fixes).
	BandMachine Band = "machine"
)

func BandOf added in v0.3.0

func BandOf(priority int) Band

BandOf reports which band a priority value lands in. Values below zero are backlog (the scale's floor is open); the band ladder is total.

type Claim added in v0.3.1

type Claim string

Claim is the handle a claimer presents to finalize a claimed task: the claim token minted at ClaimDue. Finalizes are TOKEN-fenced — the store checks the presented claim against the live lease, so theft detection lives IN the store and a lapsed claim cannot resurrect a task (ADR-0019 S1: worker finalizes moved from owner strings to claim tokens). A Claim is opaque; the only producer is a successful ClaimDue.

func (Claim) String added in v0.3.1

func (c Claim) String() string

String renders the claim for logs.

type EnqueueDetail added in v0.3.1

type EnqueueDetail struct {
	Project  string `json:"project,omitempty"`
	Type     string `json:"type,omitempty"`
	Priority *int   `json:"priority,omitempty"`
	DedupKey string `json:"dedup_key,omitempty"`
	// The task snapshot (plain enqueue only). Payload is the raw payload
	// bytes; NotBefore/CreatedAt are unix millis — the exact task-row
	// storage format, so a transition replay reconstructs the row
	// without precision loss. MaxAttempts is post-Normalize (never 0).
	Payload     string    `json:"payload"`
	Deps        []task.ID `json:"deps,omitempty"`
	MaxAttempts int       `json:"max_attempts,omitempty"`
	NotBefore   int64     `json:"not_before,omitempty"`
	CreatedAt   int64     `json:"created_at,omitempty"`
	Rescue      string    `json:"rescue,omitempty"`
}

EnqueueDetail is the structured detail on task.enqueued facts. The full shape records the task's identity, the two projection fields the journal must be able to re-derive on its own (priority, dedup key — the inputs of the journal-drift audit), and the FULL task snapshot (payload, deps, max_attempts, not_before, created_at): with it the journal alone reconstructs the task row, so replay needs no task-row side-channel (ADR-0019 S1 — the sqlitev4/replay verbatim copy exists only because pre-growth facts lack the snapshot). Facts written before the growth carry none of the snapshot keys, and a plain enqueue always marshals the payload key (even for an empty payload), so the key's presence distinguishes post-growth facts from legacy thin ones. Priority is a pointer so the zero priority stays expressible and distinguishable from a legacy thin fact. RescueDead re-emits task.enqueued with only Rescue set: the marker that a dead task was re-queued with a fresh attempt budget — the rescued row already exists, so it carries no snapshot.

S1 divergence D2 (2026-09-26 companion swap): the engine-backed stores write thin {project,type} details today, so fresh facts do NOT carry the snapshot keys — the conform pins live behind Caps.EnqueuedSnapshot=false (internal/queue/companion/conform) until upstream enriches the detail (M4-gated). cmd/tq's journal-drift audit (cmd/tq/journalaudit_test.go) is the downstream consumer of that cap.

type Filter

type Filter struct {
	Project *string
	Status  *task.Status
	Type    *string
	// Query is a case-insensitive substring search over id, type, project,
	// payload, lease owner and last error — pushed into SQL LIKE, not a
	// post-filter.
	Query string
	// PayloadContains is a case-insensitive substring search over the
	// payload ALONE — pushed into SQL LIKE, so limit/offset semantics
	// survive the filter. `tq tasks --verify-contains` rides it:
	// stale-verify hygiene audits correlate the pinned verify string
	// across the matching set without a `tq show` per task (09-39 §e1).
	PayloadContains string
	Limit           int
	// Offset skips the first Offset matches (pagination); applied after
	// ordering. Meaningful together with Limit.
	Offset int
	// SeverityOrder orders by display severity (dead, running, pending,
	// cancelled, completed), newest first within a status — the dashboard
	// table order, stable across pages. Default order stays priority then
	// age (the queue's fairness order).
	SeverityOrder bool
	// Sort overrides the ordering with an allowlisted column sort for the
	// dashboard's sortable headers: "age-asc", "age-desc",
	// "priority-asc", "priority-desc", "attempts-asc", "attempts-desc".
	// Unknown values fall back to the default order (never interpolated
	// into SQL).
	Sort string
	// Since restricts the listing to tasks created at or after this time
	// (inclusive) — pushed into SQL as a created_at comparison, not a
	// post-filter.
	Since *time.Time
	// Parked restricts the listing to rate-limit-parked tasks: pending
	// with not_before in the future (the "11 tasks parked until 19:40"
	// one-glance view). false or nil leaves the filter off.
	Parked *bool
	// PriorityMin/PriorityMax bound the listing to a STORED-priority
	// range - the band filter's pushdown (hot = [100,149], machine =
	// [150,infinity)). nil leaves the bound open. Aging is scheduling,
	// not state: the range sees the stored value, not the effective rank.
	PriorityMin *int
	PriorityMax *int
}

Filter selects tasks for List.

type PriorityScore added in v0.3.0

type PriorityScore struct {
	ItemKey       string `json:"itemKey"`
	Score         int    `json:"score"`         // 0-100 (clamped to the backlog band on use)
	EffortMinutes int    `json:"effortMinutes"` // estimated agent effort
	Source        string `json:"source"`        // scorer identity, e.g. "ai:<model>"
	Reasoning     string `json:"reasoning"`     // one-line why
	Tokens        int    `json:"tokens"`        // tokens the verdict cost
	ScoredAt      int64  `json:"scoredAt"`      // unix millis
}

PriorityScore is one cached AI verdict for a TODO_LIST item, keyed by the SAME dedup-key derivation the harvester uses (harvest.ItemKey), so a score survives until the item text changes and re-keys (ADR-0015 score cache). Effort feeds budget-aware claims; tokens feed cost measurement.

type QuestionAnsweredDetail added in v0.3.1

type QuestionAnsweredDetail struct {
	Ref        string `json:"ref"`
	Question   string `json:"question,omitempty"`
	Answer     string `json:"answer"`
	PapID      string `json:"pap_id,omitempty"`
	AnsweredAt int64  `json:"answered_at,omitempty"`
}

QuestionAnsweredDetail is the structured detail on task.question-answered facts: the ruling itself plus its provenance (which PapDashboard row, when the human answered). Question echoes the asked text when known so the answered fact is self-contained forensics.

type QuestionAskedDetail added in v0.3.1

type QuestionAskedDetail struct {
	Ref       string   `json:"ref"`
	Type      string   `json:"type"`
	Question  string   `json:"question"`
	Options   []string `json:"options,omitempty"`
	Repo      string   `json:"repo,omitempty"`
	ExpiresAt int64    `json:"expires_at,omitempty"`
}

QuestionAskedDetail is the structured detail on task.question-asked facts: what the agent asked, under which stable ref (the correlation key across journal, PapDashboard row and payload injection — a hash over task + normalized question text, so a re-ask after a crash converges on the same ref), which PapDashboard question kind it rides, and when the question expires (unix millis; the parked task's NotBefore matches it — the safety valve if the answer never comes).

type Queue

type Queue struct {
	Store
}

Queue is the facade most consumers use: a Store plus convenience methods.

func New

func New(s Store) *Queue

New wraps a Store.

func (*Queue) BumpUnblocked added in v0.3.0

func (q *Queue) BumpUnblocked(ctx context.Context, dryRun bool) ([]UnblockChange, error)

BumpUnblocked lifts PENDING tasks whose dependencies have ALL completed by UnblockBumpPriority (source "unblock") — the queue's answer to "the blocker finished, now it matters" (ADR-0015). Designed for repri time: no worker coupling, no new backend SQL — the deps table and its dep_id index already answer the reverse lookup, and enumeration is Go-side over List/Get (deps-carrying tasks are rare).

Rules: hot/machine tasks are protected (backlog band only); a task that already carries an unblock fact is never bumped twice (the fact trail is the idempotency guard); same-value writes append nothing. dryRun reports without writing.

func (*Queue) Enqueue

func (q *Queue) Enqueue(ctx context.Context, n task.New) (task.Task, error)

Enqueue normalized-and-enqueues a task. A dedup key whose stored task already completed returns the stored row PLUS ErrTaskDone — the done-guard refusing the re-dispatch.

type ReprioritizeEvidence added in v0.3.0

type ReprioritizeEvidence struct {
	OldPriority int    `json:"old_priority"`
	NewPriority int    `json:"new_priority"`
	Source      string `json:"source"`
	Reason      string `json:"reason,omitempty"`
}

ReprioritizeEvidence is the structured detail on task.reprioritized facts (ADR-0015 §5): what the priority was, what it became, which source decided, and why.

func ParseReprioritizeEvidence added in v0.3.1

func ParseReprioritizeEvidence(detail jsontext.Value) (ReprioritizeEvidence, bool)

ParseReprioritizeEvidence decodes a reprioritized fact's detail JSON. Malformed or foreign details decode as ok=false, so every consumer of the evidence shares one skip-don't-fail decode policy.

type RequeueEvidence

type RequeueEvidence struct {
	Reason         string `json:"reason"`
	RetryIn        int64  `json:"retry_in_ms"`
	ResumeCloseout bool   `json:"resume_closeout,omitempty"`
	Class          string `json:"class,omitempty"`
}

type Store

type Store interface {
	// Enqueue persists a new task (ID and defaults assigned here) and records
	// the task.enqueued fact.
	Enqueue(ctx context.Context, n task.New) (task.Task, error)
	// ClaimDue atomically claims at most one due task for owner: pending,
	// NotBefore passed, all deps completed. Sets Running + lease and mints
	// the claim token the caller must present on every finalize. Returns
	// ErrNoTaskDue when nothing is claimable.
	ClaimDue(ctx context.Context, owner string, lease time.Duration) (task.Task, Claim, error)
	// Complete marks a Running task Completed (live claim required) and
	// records the task.completed fact.
	Complete(ctx context.Context, id task.ID, claim Claim, result jsontext.Value) error
	// Fail records a failed attempt. When attempts remain the task returns to
	// Pending with NotBefore = now + backoff(attempt); otherwise it is
	// Dead-lettered. Facts: task.failed (+ task.dead-lettered). evidence,
	// when non-empty (executor.FailureEvidence JSON), lands on the
	// task.failed fact's detail — the forensics (exit code, output tail)
	// that make a failed attempt debuggable from the journal alone.
	Fail(
		ctx context.Context,
		id task.ID,
		claim Claim,
		errText string,
		backoff time.Duration,
		evidence jsontext.Value,
	) error
	// FailPermanent dead-letters a Running task immediately, regardless of
	// the attempt budget: the error class makes retrying pointless. The
	// attempt is still counted. Facts: task.failed (carrying evidence)
	// + task.dead-lettered (class "permanent").
	FailPermanent(ctx context.Context, id task.ID, claim Claim, errText string, evidence jsontext.Value) error
	// Requeue returns a claimed task to Pending WITHOUT counting an
	// attempt; it becomes claimable again after delay. For preflight
	// refusals: the environment was not ready, not the task. A
	// rate-limited close-out passes resumeCloseout so the journal records
	// that the re-claim resumes the owed close-out turn instead of
	// re-running the paid work turn. class names the refusal family
	// (RequeueClass* constants) and rides the fact detail; empty is
	// written as absent and read back as RequeueClassUnknown. Facts:
	// task.requeued.
	Requeue(
		ctx context.Context,
		id task.ID,
		claim Claim,
		errText string,
		delay time.Duration,
		resumeCloseout bool,
		class string,
	) error
	// UpdatePendingPriority changes a PENDING task's priority (ADR-0015
	// §5) and records the task.reprioritized fact (old/new, source,
	// reason) in the SAME transaction. Running/terminal tasks are refused
	// with task.ErrInvalidTransition — priority is enqueue-time truth for
	// anything already claimed or finished. A same-value update is a
	// no-op: no error, no fact (idempotency: reruns and racing sweepers
	// never spam the journal).
	UpdatePendingPriority(ctx context.Context, id task.ID, newPriority int, source, reason string) error
	// RecordAnswer records an owner's decision for a parked task's
	// question (PapDashboard questions): appends task.question-answered
	// and — when the task is PENDING — injects the answer into the task's
	// JSON-object payload under the "answered" key and clears NotBefore,
	// all in the SAME transaction, so the next claim runs knowing the
	// decision and "answered but still parked" is unrepresentable.
	// Idempotent per question ref: a replayed record appends nothing and
	// mutates nothing. A task in any other state (running, terminal)
	// records the fact only — the journal keeps the ruling, a live or
	// finished task cannot use it. Non-object payloads get the fact only
	// too: answers cannot be merged into a raw-text payload.
	RecordAnswer(ctx context.Context, id task.ID, ans AnswerRecord) error
	// Heartbeat extends the lease of a Running task held by the claim.
	Heartbeat(ctx context.Context, id task.ID, claim Claim, extend time.Duration) error
	// Cancel withdraws a Pending task. A non-empty reason is stored in the
	// task.cancelled fact's detail ("reason" key) so the journal records WHY
	// the task was withdrawn.
	Cancel(ctx context.Context, id task.ID, reason string) error
	// CancelRunning records a cooperative cancel request for a Running
	// task: the task.cancel-requested fact is the flag. The executing
	// worker observes it at its next heartbeat, stops the execution, and
	// finalizes with CancelOwned; an expired lease finalizes it at
	// reclaim. A non-empty reason is stored on the request fact's detail
	// and carried onto the final task.cancelled fact. Idempotent — a
	// second request appends nothing.
	CancelRunning(ctx context.Context, id task.ID, reason string) error
	// CancelRequested reports whether a cooperative cancel request is
	// pending for the task — the worker's heartbeat observation query.
	CancelRequested(ctx context.Context, id task.ID) (bool, error)
	// CancelOwned finalizes a cooperative cancel: Running -> Cancelled,
	// recorded by the lease-holding worker after it stopped the execution.
	// The claim must still be the task's lease token, but the lease MAY
	// have lapsed: a worker legitimately finishes the stop just after
	// expiry but before a reclaim.
	CancelOwned(ctx context.Context, id task.ID, claim Claim) error
	// MarkOrphaned appends a task.orphaned fact for every Running task
	// whose lease expired before the cutoff and that has no orphaned fact
	// yet (idempotent). It changes no state — orphans stay Running until a
	// reclaim — it records WHY the task is stranded so the journal can
	// explain it. Returns how many facts were appended.
	MarkOrphaned(ctx context.Context, cutoff time.Time) (int, error)
	// RescueDead re-queues a Dead task with a fresh attempt budget.
	RescueDead(ctx context.Context, id task.ID, maxAttempts int) error
	// DismissDead cancels a Dead task with a recorded reason (DLQ dismiss):
	// the autopsy verdict "unfixable" or an operator's ruling. The
	// task.cancelled fact's detail carries the reason and who dismissed it,
	// so the journal keeps the full death evidence plus the WHY of the
	// withdrawal. Dead is terminal otherwise; facts are never deleted.
	DismissDead(ctx context.Context, id task.ID, reason, by string) error
	// Get returns the current task record.
	Get(ctx context.Context, id task.ID) (task.Task, error)
	// List returns tasks matching the filter.
	List(ctx context.Context, f Filter) ([]task.Task, error)
	// Facts exposes the journal (same store, same transaction domain):
	// facts with Seq strictly greater than after, in Seq order. limit
	// bounds the result when > 0; 0 means unbounded (bulk exports).
	Facts(ctx context.Context, after int64, limit int) ([]journal.Fact, error)
	// LastFacts returns the most recent limit facts in ascending Seq
	// order — the bounded read behind feed-style renders. limit <= 0
	// returns the whole journal.
	LastFacts(ctx context.Context, limit int) ([]journal.Fact, error)
	// HeadSeq returns the current highest fact Seq (0 when the journal is
	// empty): the O(1) watermark for tailers, bridges and resume points.
	HeadSeq(ctx context.Context) (int64, error)
	// Watermark returns the persisted read cursor for a journal consumer
	// and whether the consumer ever checkpointed (seq 0 is a valid cursor:
	// "consumed nothing yet"): the resume point for bridges and sweepers
	// after a restart.
	Watermark(ctx context.Context, consumer string) (seq int64, exists bool, err error)
	// SaveWatermark checkpoints a consumer cursor as a monotonic upsert
	// (never regresses). It records consumer progress, not task state, so
	// no fact is appended.
	SaveWatermark(ctx context.Context, consumer string, seq int64) error
	// SavePriorityScore upserts one cached item score (ADR-0015 score
	// cache): an AI verdict persists and re-derives the same priority
	// until the item text changes and re-keys. No fact — the cache is a
	// projection input, not queue history.
	SavePriorityScore(ctx context.Context, score PriorityScore) error
	// PriorityScore returns the cached verdict for an item key, if any.
	PriorityScore(ctx context.Context, itemKey string) (PriorityScore, bool, error)
	// PriorityScores returns every cached verdict (ADR-0015 score cache),
	// ordered by item key — the scan domain of the score-cache prune
	// pass. The cache stays small by construction (one row per scored
	// item key), so the read is unbounded by design.
	PriorityScores(ctx context.Context) ([]PriorityScore, error)
	// DeletePriorityScores removes the cached verdicts for the given item
	// keys — the score-cache prune half: a reworded or vanished item
	// leaves its old key's verdict orphaned forever unless evicted.
	// Unknown keys are not an error; an empty key list deletes nothing.
	// Returns how many rows went away.
	DeletePriorityScores(ctx context.Context, itemKeys []string) (int64, error)
	// FactsForTask returns one task's facts in Seq order, bounded to the
	// most recent limit when > 0 (0 = unbounded).
	FactsForTask(ctx context.Context, id string, limit int) ([]journal.Fact, error)
	// CountFacts counts facts of one type recorded at or after since —
	// the SQL pushdown behind spend projections and stats.
	CountFacts(ctx context.Context, ftype journal.FactType, since time.Time) (int64, error)
	// FactsSince returns the facts of one type recorded at or after
	// since, in Seq order — the read sibling of CountFacts for
	// projections that must sum over the matching facts' detail (budget
	// session-usage projection) rather than only count them. limit bounds
	// the result when > 0; 0 means unbounded.
	FactsSince(ctx context.Context, ftype journal.FactType, since time.Time, limit int) ([]journal.Fact, error)
	// StatusCounts counts tasks per status — the GROUP BY behind dashboard
	// counters: O(statuses) work instead of a full task scan.
	StatusCounts(ctx context.Context) (map[task.Status]int, error)
	// ProjectCounts counts tasks per project per status — the GROUP BY
	// behind the overview chips and per-project views.
	ProjectCounts(ctx context.Context) (map[string]map[task.Status]int, error)
	// CountTasks counts the tasks matching the filter — the COUNT(*)
	// pushdown behind pagination ("page 2 of 14").
	CountTasks(ctx context.Context, f Filter) (int, error)
	// Close releases resources.
	Close() error
}

Store is the persistence boundary for tasks and facts.

type UnblockChange added in v0.3.0

type UnblockChange struct {
	TaskID        task.ID `json:"task_id"`
	OldPriority   int     `json:"old_priority"`
	NewPriority   int     `json:"new_priority"`
	CompletedDeps int     `json:"completed_deps"`
}

UnblockChange records one applied (or would-be) unblock bump.

type Waker added in v0.3.3

type Waker interface {
	// Notify yields the claim-wake channel. It fires after any commit
	// that may have landed a task in PENDING (enqueue, requeue, fail with
	// retries remaining, rescue, a parked question answered).
	Notify() <-chan struct{}
}

Waker is the optional claim-wake seam (M7): a store whose commits can make work claimable implements it so pools claim in microseconds instead of waiting out the idle poll gap. Deliberately a SIDE seam over Store, not a Store method — the ADR-0009 D4 precedent (the dispatcher stayed off Store for the same reason): optional runtime capability must not break every Store implementation, fake, and conform pin.

Contract: the channel is buffered-1 and sends are non-blocking — an already-pending signal coalesces (a burst of enqueues wakes once), a slow consumer never stalls a writer, and a missed signal only costs the poll fallback. The signal is best-effort and process-local: pools in OTHER processes sharing the same database keep their poll loops (the wake is latency polish, not a correctness mechanism — claims stay lease-fenced either way).

type WatermarkEntry

type WatermarkEntry struct {
	Consumer  string
	Seq       int64
	UpdatedAt int64 // unix millis
}

WatermarkEntry is one consumer cursor row (tq watermarks show). Shared by every Store backend's ListWatermarks read.

Directories

Path Synopsis
companion module
postgres module
postgresv4 module
sqlite module
sqlitev4 module

Jump to

Keyboard shortcuts

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