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.
Viewpoint — Consumer: tq's drivers implement this contract; worker, webui, and httpapi program against the facade; Concern: the Store semantics — filters, claim exclusivity (lease TTL + expiry reclaim), same-transaction facts; Non-goals: storage mechanics (drivers own the SQL) and execution (worker/executor own that).
Cross-package map (lifecycle × concern): docs/architecture-understanding/2026-10-23_lifecycle-concern-grid.md
Index ¶
- Constants
- Variables
- func ClaimCount(ctx context.Context, s Store, id string) (int, error)
- func ClampBacklog(priority int) int
- func CountStuckRunning(ctx context.Context, store Store, now time.Time) int
- func ValidQuestionType(t string) bool
- type AnswerRecord
- type Band
- type Claim
- type EnqueueDetail
- type Filter
- type ParkedDetail
- type PriorityScore
- type QuestionAnsweredDetail
- type QuestionAskedDetail
- type Queue
- type ReprioritizeEvidence
- type RequeueEvidence
- type Store
- type UnblockChange
- type Waker
- type WatermarkEntry
Constants ¶
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).
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.
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 // RequeueClassRateLimitGate marks the SIBLING fast-refuse: the pool's // in-process gate (armed by a sibling's observed 429) refused the task // BEFORE spawning the agent binary, so no run happened at all (02-28 // report §e2/§f4). Distinct from RequeueClassRateLimit, where THIS // task's own turn hit the provider refusal — retro classifications // count gate-parked siblings separately. RequeueClassRateLimitGate = "rate-limit-gate" 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.
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.
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.
const PrioritySourceUnblock = "unblock"
PrioritySourceUnblock labels task.reprioritized facts written by the unblock bump (the ADR-0015 source enum).
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 ¶
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).
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).
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).
var ErrNoTaskDue = errors.New("queue: no due task")
ErrNoTaskDue is returned by ClaimDue when nothing is claimable right now.
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
ClaimCount reads one task's claim count (task.claimed facts) from its journal trail — the webui detail page's per-task read. Census callers (stats loop-suspects, dead-letter churn) should use Store.ClaimCounts: one grouped query instead of a facts walk per task.
func ClampBacklog ¶ added in v0.3.0
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
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
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" )
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.
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
// DedupKey is an exact-match filter over the dedup key — the
// forensics pushdown behind `tq tasks --dedup`: which task (if any)
// a dedup key resolved to, without a payload LIKE guess.
DedupKey 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 ParkedDetail ¶ added in v0.4.0
type ParkedDetail struct {
Reason string `json:"reason,omitempty"`
UntilMS int64 `json:"until_ms"`
By string `json:"by,omitempty"`
}
ParkedDetail is the structured detail on task.parked facts: an operator's park ruling (tq park). UntilMS is the instant claims resume — a value in the past reads as the release (tq park --release).
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 (*Queue) BumpUnblocked ¶ added in v0.3.0
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.
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 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)
// ClaimDueTypes is ClaimDue with an executor-type filter: only tasks
// whose type is in types are candidates (nil or empty = any type). The
// bare-worker refusal: a pool whose registry holds only sh never claims
// an agent task — it stays PENDING for a pool that can run it, instead
// of dead-lettering on a permanent unknown-type lookup error. No claim,
// no task.claimed fact: the loop-suspect census stays clean.
ClaimDueTypes(ctx context.Context, owner string, lease time.Duration, types []string) (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. provider
// (empty when the failing output named none) records WHICH provider
// exhausted so the parked surface answers "which provider is
// limited?" in one glance (04-03 §f8). Facts:
// task.requeued.
Requeue(
ctx context.Context,
id task.ID,
claim Claim,
errText string,
delay time.Duration,
resumeCloseout bool,
class string,
provider 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
// Park suppresses claims on a PENDING task until the given instant
// (tq park): NotBefore moves to until (clamped to now) and the
// task.parked fact records reason and by, in the SAME transaction. No
// attempt burns; expiry — or parking with a past until, the release —
// re-arms claims. Running/terminal tasks are refused with
// task.ErrInvalidTransition, unknown ids with task.ErrNotFound.
// Operator rulings are history: every call appends its fact.
Park(ctx context.Context, id task.ID, reason string, until time.Time, by 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)
// ClaimCounts counts task.claimed facts per task id in one GROUP BY —
// the census pushdown behind loop-suspect detection
// (ClaimAnomalyThreshold): one grouped read instead of a facts walk
// per task.
ClaimCounts(ctx context.Context) (map[string]int, 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 ¶
WatermarkEntry is one consumer cursor row (tq watermarks show). Shared by every Store backend's ListWatermarks read.