store

package
v0.3.2 Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Overview

Free-text search over the structured entities -- tasks, sessions, projects, plans. Memories and notes are not here: they are indexed in the fts table and searched through FTSSearch (fused with semantic hits by internal/retrieve). These four have no FTS mirror, so they match with LIKE over the one or two columns a human would search by (a task's title, a session's name, a project's slug/name). That is a deliberate floor, not a stopgap: they are short, low-cardinality labels where substring matching is what the observer expects, and mirroring them into fts would mean maintaining index rows for high-churn state the files layer does not own.

Every query takes the search text as a bound parameter escaped through escapeLikePrefix, so a literal % or _ matches itself rather than acting as a wildcard.

Package store owns the SQLite database: connection setup, schema migrations, FTS5, and embedding storage. It takes a database path (not the config) to stay a leaf dependency. Files remain the source of truth for durable knowledge; the *_index tables and the fts virtual table are rebuildable mirrors.

Task CRUD and the declarations the rest of the tasks_*.go files share. The dependency-aware queue splits across: tasks_claim.go (leases), tasks_deps.go (the edge DAG), tasks_query.go (the ready/blocked/list reads), tasks_plans.go (plan rollups) and tasks_scan.go (row scanning).

Lease-based task claiming: the compare-and-set claim and the four ways a claim ends (holder release, owner force-release, session teardown, lease expiry). Expiry is enforced lazily inside ClaimTask -- there is no background sweeper.

The task dependency DAG: edge insertion with dangling-reference and cycle rejection, plus the depends-on reads (single and batched) the queue builds on.

Plan rollups: a plan's status is derived from its step tasks, never stored.

The task read surface: the ready queue, the blocked list with each task's blockers, and the plain per-project/per-plan listings. "Ready" means open with no blocker still open or in_progress -- one rule, repeated as a NOT EXISTS in each query here and mirrored by ClaimTask's branch (a) and ActivePlans.

Row scanning for the tasks table. scanTasksWithDeps is the batched path every multi-row task query drains through, so listing N tasks costs 1 query for the rows plus a handful for their edges -- not N+1.

Index

Constants

View Source
const (
	SnippetStartMark = "\x01"
	SnippetEndMark   = "\x02"
)

Snippet marks wrap the matched terms inside a SnippetHit.Snippet. They are control characters rather than "<mark>" because a snippet is raw item text: handing HTML to the caller would make every consumer responsible for telling our markup from the item's own. A control char cannot survive HTML escaping as markup, so a consumer escapes first and substitutes second (see the console's highlightSnippet), and a sentinel a writer embedded in their own body can only ever produce a stray inert <mark>, never an injection.

View Source
const (
	ProposalPending   = "pending"
	ProposalApplied   = "applied"
	ProposalDismissed = "dismissed"
)

Proposal statuses.

View Source
const (
	ProposalMerge       = "merge"
	ProposalArchive     = "archive"
	ProposalDigest      = "digest"
	ProposalConsolidate = "consolidate"
	ProposalReproject   = "reproject"    // move one memory to another project
	ProposalSplit       = "split"        // set up child/shared projects + family for a project split
	ProposalAbandonPlan = "abandon_plan" // retag a never-approved captured plan plan-status:abandoned
)

Proposal kinds (mirrors the gardener_proposals.kind CHECK constraint).

View Source
const SettingBriefingConfig = "briefing_config"

SettingBriefingConfig is the settings key holding the console-saved briefing override: a JSON-encoded config.Briefing. When present it layers over the file/env briefing config (see BriefingConfig), so the owner can tune the SessionStart injection from the console without editing seamless.yaml or restarting the daemon.

View Source
const SettingProjectFamilies = "project_families"

SettingProjectFamilies is the settings key holding project families: a JSON object {family-name: [slug, ...]}. Sibling briefings surface recent findings from a project's family members.

View Source
const SettingRepoProjectMap = "repo_project_map"

SettingRepoProjectMap is the settings key holding the cwd->project-slug map: a JSON object {absolute-path: slug}. The hooks and session_start resolve an agent's working directory to a project slug through it.

Variables

View Source
var ErrFamilyNotFound = errors.New("store: project family not found")

ErrFamilyNotFound is returned by RemoveFamilyMembers when the named family does not exist.

View Source
var ErrSessionNameExists = errors.New("store: session name already exists")

ErrSessionNameExists is returned by CreateSession when the name is already taken (sessions.name is UNIQUE). It mirrors ErrSlugExists so a caller racing to create a named session can tell "someone beat me to this name" (resume it) apart from a real database failure, instead of having to match on error text.

It reads any uniqueness failure on the insert as a name collision. The table's other unique constraint is the id primary key, and callers mint a fresh ULID per call, so a PK collision cannot happen without an id-generation bug -- the same assumption ErrSlugExists makes.

View Source
var ErrSlugExists = errors.New("store: project slug already exists")

ErrSlugExists is returned by CreateProject when the slug is already taken.

View Source
var ErrTaskBlocked = errors.New("task blocked")

ErrTaskBlocked is returned when a claim targets an open task with an unfinished dependency; the message names the blockers. No one holds the task -- it becomes claimable the moment its blockers close.

View Source
var ErrTaskClaimConflict = errors.New("task already claimed")

ErrTaskClaimConflict is returned when a claim loses the compare-and-set race to another live lease (or, from Release/Update paths, when the caller is not the holder). A refused claim whose cause is not a holder reports ErrTaskBlocked or ErrTaskClosed instead -- those need a different fix (finish the blocker; nothing, respectively), and reporting them as "already claimed" used to send agents chasing a holder that did not exist ("held by \"\"").

View Source
var ErrTaskClosed = errors.New("task closed")

ErrTaskClosed is returned when a claim targets a done/dropped task. No one holds it either; there is simply nothing left to claim.

View Source
var ErrTaskCycle = errors.New("dependency would create a cycle")

ErrTaskCycle is returned when adding a dependency edge would create a cycle.

View Source
var ErrTaskNotFound = errors.New("task not found")

ErrTaskNotFound is returned when a task id does not exist (e.g. a dangling depends_on reference).

ProposalKinds lists every valid proposal kind. This is the canonical set: derive the MCP schema's enum from it rather than transcribing, so a new kind cannot reach the store while staying invisible at the boundary.

View Source
var RetrievalWindowKeys = []string{"24h", "7d", "30d", "all"}

RetrievalWindowKeys are the selectable trailing windows for the retrieval-health views, in display order. "24h" is the default; "all" spans every recorded event.

Functions

func ActiveAmbientByCWD

func ActiveAmbientByCWD(ctx context.Context, db *sql.DB, cwd string) ([]core.Session, error)

ActiveAmbientByCWD returns the active ambient (cc/*) sessions whose cwd matches, most recent first. session_start consults it to link a freshly created explicit session to the Claude session that owns the cwd (via its claude_session_id), so a graceful SessionEnd can close both at once instead of leaving the explicit one to the idle reaper. An empty cwd matches nothing (no basis to link).

func ActiveAmbientProjects

func ActiveAmbientProjects(ctx context.Context, db *sql.DB, within time.Duration) ([]string, error)

ActiveAmbientProjects returns the distinct project slugs that have at least one active ambient (cc/*) session updated within the window, ordered by each project's most recent activity. The MCP fallback consults it to tell a safe single-project inference (len 1) from the ambiguous concurrent-agent case (len > 1, agents in different repos) where guessing would bleed a write into the wrong project. A non-positive within disables the recency filter. The global scope is reported as the empty string, distinct from any named project.

func ActiveAmbientSessionsForProject

func ActiveAmbientSessionsForProject(ctx context.Context, db *sql.DB, project string, within time.Duration) ([]core.Session, error)

ActiveAmbientSessionsForProject returns every active ambient (cc/*) session in the given project updated within the window, most recent first. resolveSession uses it to refuse targeting a session by inference when more than one same- project ambient could be the one meant -- two agents in the same repo -- so a session_update/end without an explicit id can't complete a sibling's session. A non-positive within disables the recency filter.

func ActiveMemories

func ActiveMemories(ctx context.Context, db *sql.DB, project string) ([]core.Memory, error)

ActiveMemories returns the active (not superseded/archived) memories visible to a project: its own plus all global memories (project == ""). Rows carry index metadata only (no body); newest-updated first. Passing project == "" returns only global memories.

func ActiveMemoriesForProjects

func ActiveMemoriesForProjects(ctx context.Context, db *sql.DB, slugs []string) ([]core.Memory, error)

ActiveMemoriesForProjects returns the active memories belonging to exactly the given project slugs -- no own-project or global union, unlike ActiveMemoriesForScope -- newest-updated first. It backs the briefing's opt-in sibling-memories cross-over, where the caller already holds the own scope and wants only the family members' rows. Blank/duplicate slugs are ignored; no slugs yields nil.

func ActiveMemoriesForScope

func ActiveMemoriesForScope(ctx context.Context, db *sql.DB, project string, extra []string) ([]core.Memory, error)

ActiveMemoriesForScope returns the active memories visible to a project widened by extra project slugs (e.g. a shared parent whose memories a split injects into each child). It is ActiveMemories plus the union of extra: rows where invalid_at IS NULL and project is the project, global (”), or any extra slug -- deduped, newest-updated first. Blank/duplicate extras are ignored; with no extras it is exactly ActiveMemories.

func ActiveSessionIDs

func ActiveSessionIDs(ctx context.Context, db *sql.DB, ids []string) (map[string]bool, error)

ActiveSessionIDs reports which of the given session ids belong to a currently active session. It backs the MCP server's connection-binding sweep: a binding whose session is no longer active -- ended by the session_end tool, the SessionEnd hook, or the idle reaper -- is evicted. Ids absent from the result (unknown or non-active) are simply not set.

func ActiveSessionsByClaudeID

func ActiveSessionsByClaudeID(ctx context.Context, db *sql.DB, claudeSessionID string) ([]core.Session, error)

ActiveSessionsByClaudeID returns every active session -- the ambient cc/* plus any explicit session_start that linked to it -- stamped with claudeSessionID, ambient first. The SessionEnd hook uses it to complete a whole Claude session's sessions the moment we know it ended, rather than waiting out the idle TTL. An empty id matches nothing.

func AddFamilyMembers

func AddFamilyMembers(ctx context.Context, db *sql.DB, family string, slugs []string) ([]string, error)

AddFamilyMembers adds slugs to the named family, creating the family when it is new, and persists the result. Existing members keep their order and duplicates are ignored, so callers may re-add the same slugs idempotently. Returns the family's resulting members.

The read-decode-mutate-write runs inside one transaction (the AddRepoMapping recipe): the pool is capped at a single connection (see Open), so the whole mutation is serialized against concurrent mutators and two callers growing different families at once can no longer clobber each other's family. If the pool ever grows past one connection this needs BEGIN IMMEDIATE, since two deferred transactions could still interleave read-then-write.

func AddRepoMapping

func AddRepoMapping(ctx context.Context, db *sql.DB, repoPath, slug string) error

AddRepoMapping records repoPath -> slug in the repo_project_map and persists it, so the mapping survives restarts and is shared by every agent. It is a no-op when that exact entry already exists, so callers may invoke it on every resolve without churning the setting. The read-decode-write runs inside one transaction: the pool is capped at a single connection (see Open), so the whole mutation is serialized against concurrent mutators and two agents registering different repos at once can no longer clobber each other's entry.

func AllActiveMemories

func AllActiveMemories(ctx context.Context, db *sql.DB) ([]core.Memory, error)

AllActiveMemories returns every active memory across all projects (index rows, no body), newest-updated first. It backs the gardener's reference scan, which reads each file's body to find [[name]] links.

func AllMemoriesIncludingInvalid

func AllMemoriesIncludingInvalid(ctx context.Context, db *sql.DB) ([]core.Memory, error)

AllMemoriesIncludingInvalid returns every memory index row -- active and invalid (superseded or archived) -- newest-updated first. It backs the console Memories browser, which renders supersession chains and archived items.

func AllProposalKeys

func AllProposalKeys(ctx context.Context, db *sql.DB) (map[string]struct{}, error)

AllProposalKeys returns the set of payload "key" values across proposals of EVERY status. The gardener consults it before proposing, so a suggestion the owner already applied or dismissed is never raised again.

func AllReadyTasks

func AllReadyTasks(ctx context.Context, db *sql.DB) ([]core.Task, error)

AllReadyTasks returns the ready tasks across every project (see ReadyTasks for the readiness rule), oldest-created first. It backs the console Tasks page.

func AllRetrievalStats

func AllRetrievalStats(ctx context.Context, db *sql.DB) (map[string]RetrievalStat, error)

AllRetrievalStats loads the whole retrieval_stats table into a map keyed by item id, so a caller (the console) can annotate many memories with one query instead of N GetRetrievalStat calls.

func AllTasksByStatus

func AllTasksByStatus(ctx context.Context, db *sql.DB, status core.TaskStatus) ([]core.Task, error)

AllTasksByStatus returns every task with the given status across all projects, newest-updated first.

func BriefingConfig

func BriefingConfig(ctx context.Context, db *sql.DB, base config.Briefing) (cfg config.Briefing, overridden bool, err error)

BriefingConfig returns the effective briefing config: base (the file/env values) with the console-saved override row, when present, decoded over it. overridden reports whether such a row exists. Absent fields in a stored override keep their base value, so a row written by an older console version stays forward-compatible.

func ClearBriefingConfig

func ClearBriefingConfig(ctx context.Context, db *sql.DB) error

ClearBriefingConfig removes the console briefing override, reverting the effective briefing config to the file/env base.

func CompletedSessionsSince

func CompletedSessionsSince(ctx context.Context, db *sql.DB, since time.Time) ([]core.Session, error)

CompletedSessionsSince returns completed sessions with non-empty findings updated on or after since, across all projects, newest first. It feeds the gardener's monthly digest pass.

func Cosine

func Cosine(a, b []float32) float64

Cosine returns the cosine similarity of two equal-length vectors, in [-1, 1]. Mismatched lengths or a zero-magnitude vector yield 0.

func CreateProject

func CreateProject(ctx context.Context, db *sql.DB, p core.Project) error

CreateProject inserts a project. It returns ErrSlugExists if the slug is taken.

func CreateSession

func CreateSession(ctx context.Context, db *sql.DB, s core.Session) error

CreateSession inserts a session. The caller mints the ULID id; a duplicate name returns ErrSessionNameExists.

func CreateTask

func CreateTask(ctx context.Context, db *sql.DB, t core.Task) error

CreateTask inserts a task and its dependency edges in one transaction. Every depends_on must reference an existing task (dangling references are rejected) and must not create a cycle. The caller mints the ULID id and timestamps.

func CreateTrial

func CreateTrial(ctx context.Context, db *sql.DB, tr core.Trial) error

CreateTrial inserts a trial row. The caller mints the ULID id and timestamp.

func DecodeVector

func DecodeVector(b []byte) []float32

DecodeVector reverses EncodeVector. A byte slice whose length is not a multiple of four yields nil (corrupt row; skip it).

func DeleteSetting

func DeleteSetting(ctx context.Context, db *sql.DB, key string) error

DeleteSetting removes a settings key. Deleting an absent key is a no-op.

func DistinctPlanSlugsForProject

func DistinctPlanSlugsForProject(ctx context.Context, db *sql.DB, project string) ([]string, error)

DistinctPlanSlugsForProject returns the full set of plan slugs a project has ever had, including completed plans, sorted. Unlike ActivePlans (which drops a plan once every step is closed) this keeps completed plans, so it backs a history/lineage view. It rejects an empty project for the same reason as ListSessionsForProject.

Plan slugs are unique only PER PROJECT: two projects may each have a plan "refactor". A cross-project consumer must key by (project, slug), never slug alone.

func EncodeVector

func EncodeVector(vec []float32) []byte

EncodeVector serializes a float32 vector to a little-endian byte slice, the on-disk form of the embeddings.vec BLOB column.

func EnsureProject

func EnsureProject(ctx context.Context, db *sql.DB, slug, name string) (core.Project, error)

EnsureProject returns the project registered under slug, creating a minimal row when none exists yet. It is the idempotent upsert used by the importer and by session resolution so that every project referenced by memories, notes, or sessions also has a first-class projects-table row -- the row project_list reads. A blank slug is the global scope: it is never registered and yields the zero Project with no error. When name is blank the slug is used as the name.

func ExpireStaleSessions

func ExpireStaleSessions(ctx context.Context, db *sql.DB, cutoff time.Time) ([]core.Session, error)

ExpireStaleSessions reaps active sessions whose last activity predates cutoff, flipping each to SessionExpired. It leaves updated_at untouched so the row still records when the session was last alive (the console orders and dates by it), which also keeps the flip idempotent: a reaped session no longer matches the active filter. It returns only the sessions it actually flipped (id + project) so the caller can release their task claims and record telemetry -- a session that was heartbeated or gracefully completed between the candidate read and the flip is skipped, never falsely reaped. Passing updated_at unchanged means a later graceful session_end/resume can still upgrade the row (its guards key off completed/active, not expired).

func ForceReleaseTask

func ForceReleaseTask(ctx context.Context, db *sql.DB, id string, now time.Time) (core.Task, error)

ForceReleaseTask unconditionally releases a claimed (in_progress) task, reopening it (status back to open, claim and lease cleared) regardless of who holds it or whether the lease is still live. It backs the owner override (the console "release lock" button and `seam task release --force`); it is not reachable from the agent MCP surface. Unlike ReleaseTask it does not check the holder. Returns ErrTaskClaimConflict when the task is not in_progress (nothing to release) and ErrTaskNotFound when the id is unknown.

func GetSetting

func GetSetting(ctx context.Context, db *sql.DB, key string) (string, bool, error)

GetSetting returns the value for a settings key. found is false when unset.

func LatestActiveAmbientSessionForProject

func LatestActiveAmbientSessionForProject(ctx context.Context, db *sql.DB, project string, within time.Duration) (core.Session, bool, error)

LatestActiveAmbientSessionForProject returns the most recently updated active ambient (cc/*) session in the given project, updated within the window, or found=false when none. It backs the MCP write-scope fallback: an agent that writes without calling session_start inherits its project's ambient session's provenance. A non-positive within disables the recency filter. Scoping to a single project is what prevents cross-agent bleed -- see ActiveAmbientProjects.

func ListNotes

func ListNotes(ctx context.Context, db *sql.DB) ([]core.Note, error)

ListNotes returns every note, newest-updated first. Rows carry index metadata only (no body, which lives in the file). It backs the console Notes browser.

func ListProjects

func ListProjects(ctx context.Context, db *sql.DB) ([]core.Project, error)

ListProjects returns every project, ordered by slug.

func ListSessions

func ListSessions(ctx context.Context, db *sql.DB, status core.SessionStatus, since time.Time, limit int) ([]core.Session, error)

ListSessions returns sessions newest-updated first, optionally filtered by status and to those updated since a cutoff (a zero `since` means all time), capped at limit (default 100). It backs the console Sessions list.

func ListSessionsForProject

func ListSessionsForProject(ctx context.Context, db *sql.DB, project string, status core.SessionStatus, since time.Time, limit int) ([]core.Session, error)

ListSessionsForProject returns a project's sessions newest-updated first, optionally filtered by status and to those updated since a cutoff (a zero `since` means all time), capped at limit (default 100).

It is STRICT per-slug (project_slug = ?): unlike RecentFindings it does NOT union the global scope, so a project sees only its own sessions. It rejects an empty project: an empty project_slug marks real global sessions, so "" is ambiguous between "all sessions" and "global-only" -- use ListSessions for all sessions (there is no global-only variant; pass an explicit slug for one project).

func ListTasks

func ListTasks(ctx context.Context, db *sql.DB, project string, status core.TaskStatus) ([]core.Task, error)

ListTasks returns a project's tasks, optionally filtered by status, newest first. An empty status returns every status.

func ListTasksForPlan

func ListTasksForPlan(ctx context.Context, db *sql.DB, project string, status core.TaskStatus, plan string) ([]core.Task, error)

ListTasksForPlan returns a plan's step tasks in a project, optionally filtered by status, newest first. Unlike ListTasks it includes (only) plan-scoped tasks.

func MemoriesByIDs

func MemoriesByIDs(ctx context.Context, db *sql.DB, ids []string) (map[string]core.Memory, error)

MemoriesByIDs returns the memories for the given ids in no particular order. Missing ids are simply absent from the result; callers key by ID.

func MemoriesForSession

func MemoriesForSession(ctx context.Context, db *sql.DB, sessionName string) ([]core.Memory, error)

MemoriesForSession returns the memories a session produced -- the index rows whose source_session matches the session NAME -- newest-updated first. Memory.SourceSession stores the session name (e.g. "cc/ab12cd34"), never its ULID, so callers must pass the name.

GUARD: if the argument looks like a bare session ULID (no '/', 26 chars, and parses as a ULID) it is almost certainly a mis-keyed id that would silently match nothing; the call is rejected with a message pointing at SessionByID to resolve the name.

func MemoriesSuperseding

func MemoriesSuperseding(ctx context.Context, db *sql.DB, id string) ([]core.Memory, error)

MemoriesSuperseding returns the memories that the given memory replaced: the index rows whose superseded_by points at id. It is the inverse of a memory's own SupersededBy edge (which the store does not otherwise materialize), newest-updated first, and empty when the memory superseded nothing. It backs the console memory peek's reverse "supersedes" section.

func MemoryByID

func MemoryByID(ctx context.Context, db *sql.DB, id string) (core.Memory, bool, error)

MemoryByID returns the memory with the given id. found is false when absent.

func MemoryByName

func MemoryByName(ctx context.Context, db *sql.DB, project, name string) (core.Memory, bool, error)

MemoryByName returns the active memory with an exact (project, name), most recently updated first. found is false when none matches. It does not fall back to the global scope; a caller that wants that resolves it explicitly.

func MemoryByNameIncludingInvalid

func MemoryByNameIncludingInvalid(ctx context.Context, db *sql.DB, project, name string) (core.Memory, bool, error)

MemoryByNameIncludingInvalid returns the most recently updated memory with an exact (project, name), whether or not it is still valid. It backs memory_read's warning path: a superseded memory (which MemoryByName excludes) is still readable, prefixed with a warning pointing at its replacement.

func NoteByID

func NoteByID(ctx context.Context, db *sql.DB, id string) (core.Note, bool, error)

NoteByID returns the note with the given id. found is false when absent.

func NoteBySlug

func NoteBySlug(ctx context.Context, db *sql.DB, project, slug string) (core.Note, bool, error)

NoteBySlug returns the note with an exact (project, slug). found is false when none matches.

func NotesByIDs

func NotesByIDs(ctx context.Context, db *sql.DB, ids []string) (map[string]core.Note, error)

NotesByIDs returns the notes for the given ids keyed by ID; missing ids are simply absent.

func NotesByTag

func NotesByTag(ctx context.Context, db *sql.DB, project, tag string) ([]core.Note, error)

NotesByTag returns the notes carrying an exact tag, newest-updated first, optionally scoped to a project ("" = every project). Tags are stored as a JSON array, so the match runs through json_each rather than LIKE.

func NotesByTagPrefix

func NotesByTagPrefix(ctx context.Context, db *sql.DB, project, prefix string) ([]core.Note, error)

NotesByTagPrefix returns the notes carrying at least one tag that begins with prefix (e.g. "plan:" for every plan composition), newest-updated first, optionally scoped to a project ("" = every project). It backs surfaces that enumerate a tag family without knowing the concrete values up front. prefix is treated literally: its LIKE metacharacters are escaped.

func Open

func Open(dbPath string) (*sql.DB, error)

Open opens (creating if needed) the SQLite database at dbPath, applies PRAGMAs via the DSN so every pooled connection inherits them, runs migrations, and returns the handle. The caller is responsible for closing the *sql.DB.

func ProjectBySlug

func ProjectBySlug(ctx context.Context, db *sql.DB, slug string) (core.Project, bool, error)

ProjectBySlug returns the project with the given slug. found is false when absent.

func ProjectFamilies

func ProjectFamilies(ctx context.Context, db *sql.DB) (map[string][]string, error)

ProjectFamilies decodes the project_families setting into a map. An unset or blank value yields an empty map (not an error).

func ProjectMemoriesIncludingInvalid

func ProjectMemoriesIncludingInvalid(ctx context.Context, db *sql.DB, project string) ([]core.Memory, error)

ProjectMemoriesIncludingInvalid returns a project's memory index rows -- both active and invalid (superseded/archived) -- newest-updated first, for a lineage view that must render supersession chains.

It is STRICT per-slug (project = ?): it does NOT union the global (empty) scope. This is the deliberate opposite of ActiveMemories, which DOES union global rows into a project's view. Mixing the two conventions is exactly how a project count comes out larger than expected, so callers must pick the strict variant on purpose. It rejects an empty project for the same reason as ListSessionsForProject.

func ProjectsByParent

func ProjectsByParent(ctx context.Context, db *sql.DB, parent string) ([]core.Project, error)

ProjectsByParent returns the projects whose parent_slug equals parent, ordered by slug (using idx_projects_parent). It backs walking a project's children in the parent/child topology a split builds.

func QueryTrials

func QueryTrials(ctx context.Context, db *sql.DB, f TrialFilter) ([]core.Trial, error)

QueryTrials returns trials matching the filter, newest first. Lab/outcome/ project are filtered in SQL; MetricsEquals is applied in Go (labs are small, and this matches the DB-first metrics design without a JSON1 dependency).

func ReactivateSessionByName

func ReactivateSessionByName(ctx context.Context, db *sql.DB, name, project string, now time.Time) (bool, error)

ReactivateSessionByName resumes the named session with a single targeted UPDATE: status flips back to active, project_slug is re-scoped (only when project is non-empty), and updated_at is bumped -- findings and metadata are never touched. The SessionStart hook resumes ambient sessions through it instead of a full-row read-modify-write, so a concurrent transcript harvest (which writes findings via UpdateSession) cannot be clobbered by a racing resume that read the row before the harvest landed. found is false when no session has that name.

func ReadyTasks

func ReadyTasks(ctx context.Context, db *sql.DB, project string) ([]core.Task, error)

ReadyTasks returns the actionable tasks for a project: open tasks with no blocking dependency still open or in_progress (done/dropped deps do not block). in_progress tasks are excluded (already claimed) but still block their dependents. Ordered oldest-created first, ties broken by id (ULID-monotonic), so the queue is stable and agent-predictable.

func ReadyTasksForPlan

func ReadyTasksForPlan(ctx context.Context, db *sql.DB, project, plan string) ([]core.Task, error)

ReadyTasksForPlan returns the ready (claimable) step tasks of one plan in a project: open, unclaimed, with no unfinished blocker. Same readiness rule as ReadyTasks, scoped to plan_slug instead of excluding plan tasks.

func RebuildRetrievalStats

func RebuildRetrievalStats(ctx context.Context, db *sql.DB) error

RebuildRetrievalStats recomputes the entire retrieval_stats table from the event log. It is the canonical maintenance path (the gardener calls it at the top of each pass, and it is safe to call after an import): retrieval.injected events contribute inject_count + last_injected_at (per item id, whether the id is in the item_id column or the payload's item_ids array), and memory.read events contribute read_count + last_read_at. Events are read oldest-first so the last-seen timestamp wins.

func RecentFindings

func RecentFindings(ctx context.Context, db *sql.DB, project string, limit int) ([]core.Session, error)

RecentFindings returns completed sessions with meaningful findings visible to a project (its own plus global sessions), most recent first. It backs the briefing's "recent findings" section, so it excludes both blank findings and the core.FindingNoSummary sentinel (a session that ended with nothing to harvest) -- a content-free line is not worth an agent's context.

func RegisterProjectForCWD

func RegisterProjectForCWD(ctx context.Context, db *sql.DB, cwd string) (string, error)

RegisterProjectForCWD resolves cwd to a project slug like ResolveProjectForCWD, but grows the map at runtime when cwd falls outside every mapped repo: it finds the enclosing git repository, derives a slug from that repo's directory name, records repoRoot -> slug in the repo_project_map, and ensures a projects-table row. This is how the repo->project map evolves as agents work in new repos -- no recompile, no manual map-repo. For an already-mapped cwd it still backfills the registry row, so project_list stays complete. A blank cwd, or a cwd outside any git repo, resolves to the global scope ("") and registers nothing.

func ReleaseClaimsForSession

func ReleaseClaimsForSession(ctx context.Context, db *sql.DB, sessionID string, now time.Time) (int, error)

ReleaseClaimsForSession reopens every in_progress task currently claimed by sessionID (called on session_end so a departing agent's work returns to the queue). It returns the number of tasks released.

func ReleaseTask

func ReleaseTask(ctx context.Context, db *sql.DB, id, sessionID string, now time.Time) (core.Task, error)

ReleaseTask releases a task held by sessionID, reopening it (status back to open, claim and lease cleared) so another agent can pick it up. Only the current holder may release; otherwise it returns ErrTaskClaimConflict (or ErrTaskNotFound when the id is unknown).

func RemoveFamilyMembers

func RemoveFamilyMembers(ctx context.Context, db *sql.DB, family string, slugs []string) ([]string, error)

RemoveFamilyMembers removes slugs from the named family and persists the result. Passing no slugs removes the whole family; a family left with no members after the removal is dropped as well. Returns the family's resulting members, empty when the family was removed. It errors with ErrFamilyNotFound when the named family does not exist.

Like AddFamilyMembers the read-decode-mutate-write runs inside one transaction, serialized by the single-connection pool (see Open), so a concurrent removal from another family survives instead of being clobbered by this write-back. If the pool ever grows past one connection this needs BEGIN IMMEDIATE.

func RepoProjectMap

func RepoProjectMap(ctx context.Context, db *sql.DB) (map[string]string, error)

RepoProjectMap decodes the repo_project_map setting into a map. An unset or blank value yields an empty map (not an error), so an unconfigured install simply resolves every cwd to the global scope.

func ResolveProjectForCWD

func ResolveProjectForCWD(ctx context.Context, db *sql.DB, cwd string) (string, error)

ResolveProjectForCWD maps an absolute working directory to a project slug via the repo_project_map, using a longest-prefix match. It is read-only and failure-soft: an unresolvable cwd (or an unconfigured map) returns "" (the global scope), never an error from the matching itself. Use it on read paths (briefing, prompt recall); use RegisterProjectForCWD on session-start paths that should grow the map.

func ResolveProposal

func ResolveProposal(ctx context.Context, db *sql.DB, id, status string, at time.Time) error

ResolveProposal marks a proposal applied or dismissed and stamps resolved_at. It errors if the proposal is missing or already resolved (not pending).

func RetireProject

func RetireProject(ctx context.Context, db *sql.DB, slug string, at time.Time) error

RetireProject stamps a project's retired_at (marking it emptied by a split) and bumps updated_at. Passing the zero time clears it (un-retire). It is idempotent and leaves the project's rows and files intact -- retirement is a flag, never a delete. An unknown slug affects no rows and returns nil.

func SchemaVersion

func SchemaVersion(db *sql.DB) (int, error)

SchemaVersion returns the highest applied migration version.

func SearchProjects

func SearchProjects(ctx context.Context, db *sql.DB, q string, limit int) ([]core.Project, error)

SearchProjects returns projects whose slug or display name contains q, alphabetically by slug (projects are few and stable, so a name order reads better than a recency one).

func SearchSessions

func SearchSessions(ctx context.Context, db *sql.DB, q string, limit int) ([]core.Session, error)

SearchSessions returns sessions whose name contains q, newest-updated first. An exact id also matches.

func SearchTasks

func SearchTasks(ctx context.Context, db *sql.DB, q string, limit int) ([]core.Task, error)

SearchTasks returns tasks whose title contains q, newest-updated first. An exact id also matches, so pasting a task id from a log finds its task.

func SessionByID

func SessionByID(ctx context.Context, db *sql.DB, id string) (core.Session, bool, error)

SessionByID returns the session with the given id. found is false when absent.

func SessionByName

func SessionByName(ctx context.Context, db *sql.DB, name string) (core.Session, bool, error)

SessionByName returns the session with the given (unique) name. found is false when absent.

func SetBriefingConfig

func SetBriefingConfig(ctx context.Context, db *sql.DB, b config.Briefing) error

SetBriefingConfig persists b as the console briefing override. Callers validate first (config.Briefing.Validate); this only encodes and stores.

func SetProjectFamilies

func SetProjectFamilies(ctx context.Context, db *sql.DB, families map[string][]string) error

SetProjectFamilies persists the full families map as the project_families setting. Empty families (no members) are dropped and members are trimmed and deduped so the stored value stays canonical; passing an empty map clears the setting to "{}". It is the single writer the family CLI and mutators funnel through, so the setting never accumulates blanks or duplicates.

It reads nothing, so unlike the read-modify-write mutators below it needs no transaction: last-write-wins is inherent to its "set the whole map" contract.

func SetProjectParent

func SetProjectParent(ctx context.Context, db *sql.DB, slug, parent string, now time.Time) error

SetProjectParent sets (or clears, with parent == "") a project's parent slug and bumps updated_at. The parent's active memories are injected into the child's briefing (see retrieve.Briefing). It is idempotent -- re-setting the same parent is a harmless no-op write -- so a split apply is retry-safe. An unknown slug affects no rows and returns nil (the caller ensures the row first).

func SetSetting

func SetSetting(ctx context.Context, db *sql.DB, key, value string) error

SetSetting upserts a settings key/value.

func SiblingFindings

func SiblingFindings(ctx context.Context, db *sql.DB, slugs []string, limit int) ([]core.Session, error)

SiblingFindings returns the most recent completed sessions with meaningful findings across the given sibling projects, newest first, capped at limit. It backs the briefing's "Sibling projects" section, so -- like RecentFindings -- it excludes blank findings and the core.FindingNoSummary sentinel. An empty slugs slice yields no results.

func SiblingProjects

func SiblingProjects(ctx context.Context, db *sql.DB, project string) ([]string, error)

SiblingProjects returns the other project slugs sharing a family with project, deduped and excluding project itself. A project may appear in more than one family; all such siblings are unioned. Returns nil for the global scope or a project with no family.

func StaleMemories

func StaleMemories(ctx context.Context, db *sql.DB, cutoff time.Time) ([]core.Memory, error)

StaleMemories returns active memories that have seen no activity since cutoff: neither updated, injected, nor read on or after that instant. It LEFT JOINs retrieval_stats (a memory with no stats row counts as never injected/read, so only its updated_at matters), oldest-updated first. It backs the gardener's staleness pass.

func TableCount

func TableCount(db *sql.DB) (int, error)

TableCount returns the number of user tables (excluding SQLite internals and FTS shadow tables). Useful for the doctor's DB check.

func TaskByID

func TaskByID(ctx context.Context, db *sql.DB, id string) (core.Task, error)

TaskByID returns a task with its dependency ids populated. found is false when absent.

func TasksBlockedBy

func TasksBlockedBy(ctx context.Context, db *sql.DB, id string) ([]core.Task, error)

TasksBlockedBy returns the tasks that depend on id -- the inverse of a task's DependsOn edges, i.e. the tasks this one blocks. Unlike openBlockers it applies no status filter (closed dependents are included too), oldest-created first. It backs the console task peek's reverse "blocks" section.

func TasksClaimedBy

func TasksClaimedBy(ctx context.Context, db *sql.DB, sessionID string) ([]core.Task, error)

TasksClaimedBy returns the tasks a session holds or held -- the rows whose claimed_by matches the session ULID -- oldest-created first (ties by id, the ULID-monotonic order the ready queue uses). Task.ClaimedBy stores the session ULID, never its name.

GUARD (the inverse of MemoriesForSession): if the argument is not ULID-shaped -- it contains '/' or is not 26 chars or fails to parse as a ULID -- it is almost certainly a session name that would silently match nothing; the call is rejected with a message pointing at SessionByName to resolve the id.

func TouchSession

func TouchSession(ctx context.Context, db *sql.DB, id string, now time.Time) error

TouchSession heartbeats an active session by bumping its updated_at, marking it still-alive for the idle reaper. It only touches active sessions (the WHERE guard), so it never resurrects a completed or expired one, and is a no-op on an empty id. Callers fire it on activity (a bound MCP tool call), best-effort.

func TouchSessionByName

func TouchSessionByName(ctx context.Context, db *sql.DB, name string, now time.Time) error

TouchSessionByName is TouchSession keyed by the unique session name, for the ambient hooks which know the cc/{prefix} name (not the ULID). Same active-only guard and no-op-on-empty semantics.

func UpdateProposalPayload

func UpdateProposalPayload(ctx context.Context, db *sql.DB, id string, payload map[string]any) error

UpdateProposalPayload replaces a pending proposal's payload (e.g. retargeting a reproject to a different project before applying). It errors if the proposal is missing or not pending, so a resolved proposal is never silently rewritten.

func UpdateSession

func UpdateSession(ctx context.Context, db *sql.DB, s core.Session) error

UpdateSession updates the mutable fields of a session (status, findings, metadata, project scope, updated_at) by id.

func UpdateTask

func UpdateTask(ctx context.Context, db *sql.DB, id string, patch TaskPatch, actor string, now time.Time) (core.Task, error)

UpdateTask applies patch to the task and returns the updated task (with deps). Moving to a terminal status stamps closed_at; reopening clears it. Added deps are validated for existence and cycles like CreateTask.

actor is the caller's session id (empty for callers with no session). A task with a live claim (see core.Task.ClaimLive) is locked to its holder: any mutation by a different actor is rejected with ErrTaskClaimConflict, so an agent's claimed task cannot be edited or closed out from under it. The holder itself, an expired lease, and the owner override (ForceReleaseTask) are the only ways past the lock.

func UpsertEmbedding

func UpsertEmbedding(ctx context.Context, db *sql.DB, itemID, kind, model string, vec []float32) error

UpsertEmbedding stores (or replaces) the vector for an item. dims is recorded from the vector length so a later model change is detectable.

Types

type BlockedTask

type BlockedTask struct {
	Task     core.Task   `json:"task"`
	Blockers []core.Task `json:"blockers"`
}

BlockedTask is an open task that is not ready, paired with the blockers still keeping it off the ready queue (open/in_progress dependencies).

func AllBlockedTasks

func AllBlockedTasks(ctx context.Context, db *sql.DB) ([]BlockedTask, error)

AllBlockedTasks returns open-but-not-ready tasks across every project, each with its still-open blockers.

func BlockedTasks

func BlockedTasks(ctx context.Context, db *sql.DB, project string) ([]BlockedTask, error)

BlockedTasks returns a project's open-but-not-ready tasks, each with its still-open blockers, so a caller can render the dependency chain legibly.

func BlockedTasksForPlan

func BlockedTasksForPlan(ctx context.Context, db *sql.DB, project, plan string) ([]BlockedTask, error)

BlockedTasksForPlan returns a plan's open-but-not-ready step tasks, each with its still-open blockers.

type ClaimResult

type ClaimResult struct {
	Task        core.Task
	Reclaimed   bool
	PriorHolder string
}

ClaimResult reports the outcome of a successful ClaimTask. Reclaimed is true when a different session's lapsed lease was stolen; PriorHolder then names that session so the caller can record a reclaim event.

func ClaimTask

func ClaimTask(ctx context.Context, db *sql.DB, id, sessionID string, lease time.Duration, now time.Time) (ClaimResult, error)

ClaimTask atomically claims a task for sessionID, moving it to in_progress and stamping a lease that expires at now+lease. It is a compare-and-set (mirrors ResolveProposal): the write lands only when the task is claimable --

(a) open and ready (no open/in_progress blocker), or
(b) in_progress and held by sessionID (a re-claim / heartbeat that refreshes
    the lease), or
(c) in_progress but carrying no live claim -- nobody holds it, so it is up for
    grabs (a steal from a dead holder, or a row that was never claimed).

Otherwise it returns an error naming the actual refusal: ErrTaskClaimConflict when another live lease holds the task, ErrTaskBlocked when the task is open with an unfinished dependency (naming the blockers), ErrTaskClosed when it is done/dropped. Lease expiry is enforced lazily here -- there is no background sweeper. sessionID must be non-empty.

core.Task.ClaimLive is the single authority on whether a task is held, and branches (b)+(c) are exactly its negation for an in_progress row: an empty claimed_by (no holder), a NULL lease_expires_at (no lease stamped), or a lapsed lease (lease_expires_at <= now -- ClaimLive uses After, so a lease expiring exactly at now is already dead). Branch (c) must tolerate a claimless in_progress row because UpdateTask can produce one: `seam task start <id>` and MCP tasks_update status=in_progress patch the status without stamping a claim, which is a legitimate owner action. Such a row is already freely editable by anyone (UpdateTask's holder-lock also defers to ClaimLive), so refusing to claim it would strand it as permanently unclaimable until someone reopened it.

Branch (a) deliberately ignores claimed_by: a live claim exists only on an in_progress task with an unexpired lease, and every path that writes status='open' clears the claim fields, so a claim value on an open row can only be stale residue (rows written before reopen released claims). Claiming such a row overwrites the residue, self-healing it.

type CoverageBucket

type CoverageBucket struct {
	Label   string `json:"label"`
	Total   int    `json:"total"`   // sessions created in the bucket
	Covered int    `json:"covered"` // of those, how many retained knowledge
}

CoverageBucket is one time bucket of the session-coverage trend: a pre-formatted tick label, how many sessions started in it, and how many of those retained knowledge. Total == 0 means no sessions in the bucket, so its coverage is undefined (the trend renders it as a dip to the floor, not a ceiling).

func SessionCoverageBuckets

func SessionCoverageBuckets(ctx context.Context, db *sql.DB, w RetrievalWindow, now time.Time) ([]CoverageBucket, error)

SessionCoverageBuckets computes the windowed session-coverage trend in the viewer's local time -- hourly for the 24h window, daily otherwise (for "all", daily from the earliest session). It applies the same covered-ness test as GetSessionCoverage over the same window, so the coverage hero and this trend describe the same set of sessions. Buckets are contiguous (a quiet stretch reads as a dip), and it shares localBucketAxis/bucketKey with the injection trend so the two charts' x-axes line up. Returns nil when the window holds no sessions.

type KindReach

type KindReach struct {
	Kind     string `json:"kind"`
	Injects  int    `json:"injects"`
	Memories int    `json:"memories"`
}

KindReach is one memory kind's share of a window's injection activity: total injections (volume) and the number of distinct active memories of that kind that were surfaced at least once.

type MemoryStat

type MemoryStat struct {
	ID           string     `json:"id"`
	Name         string     `json:"name"`
	Kind         string     `json:"kind"`
	Project      string     `json:"project"`
	Injects      int        `json:"injects"`
	Reads        int        `json:"reads"`
	Sessions     int        `json:"sessions"` // distinct sessions reached (retrieval-reach view)
	Updated      time.Time  `json:"updated"`
	LastInjected *time.Time `json:"lastInjected,omitempty"`
}

MemoryStat is an active memory annotated with its retrieval counts, for the Retrieval page's top-injected and stale lists.

type MemoryVector

type MemoryVector struct {
	ID          string
	Project     string
	Name        string
	Description string
	Kind        string
	UpdatedAt   time.Time
	Vec         []float32
}

MemoryVector pairs an active memory's index metadata with its stored embedding vector, for the gardener's pairwise dedup scan.

func ActiveMemoryVectors

func ActiveMemoryVectors(ctx context.Context, db *sql.DB, model string) ([]MemoryVector, error)

ActiveMemoryVectors returns the vectors of all active memories embedded under model, oldest-created first (stable ordering for deterministic pairing). It joins the embeddings table, so a memory with no vector for that model is omitted (the gardener simply cannot dedup it semantically).

type Migration

type Migration struct {
	Version int
	SQL     string
}

Migration is a single numbered schema migration.

type NamedCount

type NamedCount struct {
	ID    string `json:"id"`
	Name  string `json:"name"`
	Count int    `json:"count"`
}

NamedCount pairs an item's name with a count, for "top N" lists.

type NavCounts struct {
	Sessions         int // all sessions
	Memories         int // active memories
	Notes            int // all notes
	OpenTasks        int // open or in_progress
	PendingProposals int // pending gardener proposals
	Projects         int // registered projects
	Plans            int // distinct plan:<slug> compositions (captures + composed)
}

NavCounts are the cheap roll-up counts the console shows in its sidebar. It is a handful of COUNT queries, safe to run on every page load.

func GetNavCounts

func GetNavCounts(ctx context.Context, db *sql.DB) (NavCounts, error)

GetNavCounts computes the sidebar counts.

type PlanRollup

type PlanRollup struct {
	Slug      string `json:"slug"`
	Total     int    `json:"total"`
	Done      int    `json:"done"`
	InFlight  int    `json:"inFlight"`
	Claimable int    `json:"claimable"`
}

PlanRollup is the per-plan aggregate the briefing surfaces: Total step tasks, how many are Done (closed), InFlight (in_progress), and Claimable (ready).

func ActivePlans

func ActivePlans(ctx context.Context, db *sql.DB, project string) ([]PlanRollup, error)

ActivePlans returns a rollup for each not-yet-complete plan in a project (plans whose every step is closed are omitted, like a done stage). Claimable counts ready open steps (same readiness rule as ReadyTasksForPlan); the plan's status is derived, never stored. One grouped query covers every plan.

type PlanSearchRow

type PlanSearchRow struct {
	Slug    string
	Project string
	Title   string
	Updated time.Time
}

PlanSearchRow is one plan hit. A plan is a composition, not a table, so a row is identified by (Project, Slug); Title is the best label found -- a matching note's title, or the slug itself when only tasks carry it.

func SearchPlans

func SearchPlans(ctx context.Context, db *sql.DB, q string, limit int) ([]PlanSearchRow, error)

SearchPlans returns plans whose slug or narrative-note title contains q, newest-updated first.

Plans are bi-sourced (a note tagged plan:<slug>, and tasks carrying plan_slug), and either source alone can carry a match: a plan whose steps exist but whose note does not, or vice versa. Both are queried and merged deduped by (project, slug), keeping the newer Updated and preferring a note's title over the slug fallback -- the same merge the Plans screen does, done here so a search hit cannot disagree with the page it links to.

type ProjectBoardRow

type ProjectBoardRow struct {
	Project      string    `json:"project"`      // slug; "" is the global scope
	Memories     int       `json:"memories"`     // active memories, project = slug (strict, matches GetProjectCounts.Memories)
	Sessions     int       `json:"sessions"`     // all sessions project_slug = slug (matches GetProjectCounts.Sessions)
	LiveSessions int       `json:"liveSessions"` // live sessions: active AND updated within core.SessionIdleTTL (matches the Sessions screen)
	OpenTasks    int       `json:"openTasks"`    // status IN ('open','in_progress') (matches GetProjectCounts.OpenTasks)
	Blocked      int       `json:"blocked"`      // open tasks with >=1 open/in_progress blocker (== len(BlockedTasks))
	Notes        int       `json:"notes"`        // notes project = slug (matches GetProjectCounts.Notes)
	LastActive   time.Time `json:"lastActive"`   // MAX(sessions.updated_at) for the project; zero when none
	Unregistered bool      `json:"unregistered"` // slug seen in data tables but absent from the projects table
	ReachRate    int       `json:"reachRate"`    // Surfaced/Active rounded %, from BuildRetrievalReport; 0 when absent
	Surfaced     int       `json:"surfaced"`     // distinct active memories surfaced >=1x in the window
	Active       int       `json:"active"`       // active memories (reach denominator) per BuildRetrievalReport
}

ProjectBoardRow is one project's health roll-up for the project board: the same strict per-slug counts GetProjectCounts computes for a single project, batched across every project in one pass, plus liveness (live sessions), backpressure (blocked tasks), recency (last session activity), and retrieval reach joined from BuildRetrievalReport.

Counts are STRICT per-slug (project = slug / project_slug = slug), never a global union: the global scope ("") is its own row whose counts are the global-scope counts, never folded into each named project. A slug that appears in the data tables but has no projects-table row (files are the source of truth and can drift ahead of the registry) still gets a row, flagged Unregistered so the caller can surface the drift rather than hide it.

func ProjectsWithCounts

func ProjectsWithCounts(ctx context.Context, db *sql.DB, window RetrievalWindow, now time.Time, idleTTL time.Duration) ([]ProjectBoardRow, error)

ProjectsWithCounts returns a ProjectBoardRow for every project -- every slug that appears in memories_index / sessions / tasks / notes_index unioned with the projects table -- with strict per-slug counts, liveness, blocked-task backpressure, and last-active recency computed in SQL, then retrieval reach (ReachRate/Surfaced/Active) joined by slug from BuildRetrievalReport over the given window. LiveSessions counts sessions that are active AND updated within idleTTL of now (<= 0 falls back to core.SessionIdleTTL), matching the Sessions screen's live bucket, so an active-but-idle session awaiting the reaper never inflates the board.

The counts mirror GetProjectCounts exactly (same strict per-slug predicates), so a single row equals the single-project peek. The global scope ("") is its own row (never folded into named projects); it is never flagged Unregistered because the global scope is legitimately never registered. Rows are ordered by slug (the global "" scope first).

type ProjectCounts

type ProjectCounts struct {
	Memories  int `json:"memories"`  // active memories in the project
	Sessions  int `json:"sessions"`  // sessions scoped to the project
	OpenTasks int `json:"openTasks"` // open or in_progress tasks
	Notes     int `json:"notes"`     // notes in the project
}

ProjectCounts are the per-project totals the console project peek shows. The channels do not overlap in the way coverage does; each is a plain count.

func GetProjectCounts

func GetProjectCounts(ctx context.Context, db *sql.DB, slug string) (ProjectCounts, error)

GetProjectCounts computes the per-project roll-up for one slug in a single round trip (scalar subqueries). It backs the console project peek.

type ProjectReach

type ProjectReach struct {
	Project   string `json:"project"`
	Surfaced  int    `json:"surfaced"`  // distinct active memories of this project surfaced
	Active    int    `json:"active"`    // total active memories in this project (reach denominator)
	ReachRate int    `json:"reachRate"` // Surfaced / Active, rounded %
	Injects   int    `json:"injects"`   // injection volume attributable to this project's memories
}

ProjectReach is one project's reach within the window: how many of its active memories were surfaced at least once, out of its total active memories, plus the injection volume attributable to them. It lets the global reach number be read per knowledge base -- a project whose memories never surface shows 0% even while the global rate looks healthy. Project "" is the global scope.

type Proposal

type Proposal struct {
	ID         string         `json:"id"`
	Kind       string         `json:"kind"`
	Payload    map[string]any `json:"payload"`
	Status     string         `json:"status"`
	CreatedAt  time.Time      `json:"createdAt"`
	ResolvedAt *time.Time     `json:"resolvedAt,omitempty"`
}

Proposal is one gardener suggestion awaiting owner review. Payload carries the kind-specific detail; every payload includes a stable "key" string the gardener uses to avoid re-proposing the same thing on a later pass.

func CreateProposal

func CreateProposal(ctx context.Context, db *sql.DB, kind string, payload map[string]any) (Proposal, error)

CreateProposal inserts a pending gardener proposal and returns it. The caller supplies the kind and payload; the id and created_at are stamped here.

func PendingProposals

func PendingProposals(ctx context.Context, db *sql.DB, kind string) ([]Proposal, error)

PendingProposals returns pending proposals, newest first. kind filters by proposal kind when non-empty.

func ProposalByID

func ProposalByID(ctx context.Context, db *sql.DB, id string) (Proposal, bool, error)

ProposalByID returns one proposal. found is false when absent.

type RetrievalReport

type RetrievalReport struct {
	Window           RetrievalWindow `json:"window"`
	Injected         int             `json:"injected"`         // item-level injections in the window (volume) == sum(Trend)
	MemoriesSurfaced int             `json:"memoriesSurfaced"` // distinct active memories surfaced >=1x
	ActiveMemories   int             `json:"activeMemories"`   // total active memories (reach denominator)
	ReachRate        int             `json:"reachRate"`        // MemoriesSurfaced / ActiveMemories, rounded %
	SessionsReached  int             `json:"sessionsReached"`  // distinct sessions that received >=1 injection
	ByKind           []KindReach     `json:"byKind"`
	ByProject        []ProjectReach  `json:"byProject"` // reach sliced per project (denominator = that project's active memories)
	Top              []MemoryStat    `json:"top"`       // most-injected active memories, with sessions reached
	Trend            []TrendBucket   `json:"trend"`
	Hourly           bool            `json:"hourly"` // trend granularity: hourly (24h window) vs daily
}

RetrievalReport is the retrieval-REACH rollup for a window, computed live from the injection event stream. It measures how far the knowledge base actually reaches agents -- how many distinct memories get surfaced, across how many sessions -- rather than a read-back rate. (Read-after-inject is not tracked here: agents almost never memory_read what a briefing already surfaced, so the only honest consumption signal for this system is reach, not re-reads.)

func BuildRetrievalReport

func BuildRetrievalReport(ctx context.Context, db *sql.DB, w RetrievalWindow, topN int) (RetrievalReport, error)

BuildRetrievalReport computes the windowed retrieval-reach rollup in a single pass over the injection event stream: total injection volume, how many distinct active memories were surfaced (reach), across how many sessions, plus the per-kind and most-injected breakdowns and the injection trend. topN caps the most-injected list (<=0 => 12). Archived/unknown injected ids still count toward the volume total and the trend, but drop out of the by-kind and most-injected breakdowns (scoped to active memories).

type RetrievalStat

type RetrievalStat struct {
	ItemID         string
	InjectCount    int
	ReadCount      int
	LastInjectedAt *time.Time
	LastReadAt     *time.Time
}

RetrievalStat mirrors one retrieval_stats row: how often an item has been surfaced (injected) to an agent and read back, and when last. It is a materialized projection of the append-only event log, rebuilt by RebuildRetrievalStats -- the events table is the source of truth.

func GetRetrievalStat

func GetRetrievalStat(ctx context.Context, db *sql.DB, itemID string) (RetrievalStat, bool, error)

GetRetrievalStat returns the stats row for an item. found is false when the item has never been injected or read.

type RetrievalWindow

type RetrievalWindow struct {
	Key   string    `json:"key"`
	Label string    `json:"label"`
	Since time.Time `json:"since"`
}

RetrievalWindow is a resolved trailing time window for the retrieval-health views: a stable key (URL + selector), a human label, and the inclusive lower bound. A zero Since means "all time" (unbounded).

func ResolveRetrievalWindow

func ResolveRetrievalWindow(key string, now time.Time) RetrievalWindow

ResolveRetrievalWindow maps a selector key to a window anchored at now. Unknown or empty keys fall back to "24h", the default: an unscoped console page answers about today rather than about all history.

type SearchHit

type SearchHit struct {
	ItemID string
	Kind   string
	Score  float64
}

SearchHit is one result of a cosine similarity search.

func CosineSearch

func CosineSearch(ctx context.Context, db *sql.DB, query []float32, model string, kinds, projects []string, limit int) ([]SearchHit, error)

CosineSearch brute-force-scans stored vectors for the given model and returns the top-limit most similar items, highest score first. An empty kinds filter searches all kinds. At the corpus scale this system targets (thousands of items) a full scan is milliseconds, which is why there is no vector index.

projects restricts hits to items whose project is in the list (recall passes the bound project plus "" for global); an empty filter searches all projects. Filtering inside the candidate query keeps the whole candidate depth in scope, so a corpus dominated by out-of-scope vectors cannot starve in-scope results out of the top-limit window. Embedding rows carry no project, so the scope is resolved by joining the index tables; an embedding orphaned from both indexes matches no scope.

Superseded and archived memories are excluded on the same grounds: a memory keeps its vector after it is invalidated, and a retired revision sits close to the replacement that superseded it, so filtering validity after the LIMIT let dead revisions eat the candidate depth. An embedding with no memories_index row (a note, or an orphan) has nothing to invalidate it and survives this predicate -- the scope filter is what drops orphans.

func FTSSearch

func FTSSearch(ctx context.Context, db *sql.DB, query string, kinds, projects []string, limit int) ([]SearchHit, error)

FTSSearch runs a full-text query over the unified fts table and returns hits ordered best-first. Scores are the negated bm25 rank (higher = better) so they share the "bigger is better" convention with CosineSearch. An empty kinds filter searches all kinds. A query with no usable terms yields no hits (not an error), so recall degrades quietly on punctuation-only input.

projects restricts hits to items whose project is in the list (recall passes the bound project plus "" for global); an empty filter searches all projects. Filtering inside the candidate query keeps the whole candidate depth in scope, so a corpus dominated by out-of-scope matches cannot starve in-scope results out of the top-limit window.

Superseded and archived memories are excluded for the same reason: the fts table is self-contained and keeps a full row for a memory that is no longer valid, so validity is resolved by joining memories_index. Filtering it after the LIMIT (as callers used to) let retired revisions of a name -- which match their replacement's queries almost as well, by construction -- eat the candidate depth and starve the live memory that replaced them. An fts row with no index row (notes, or an orphan) has nothing to invalidate it and is kept.

type SessionCoverage

type SessionCoverage struct {
	Total    int `json:"total"`    // all sessions (the denominator)
	Covered  int `json:"covered"`  // sessions with >=1 durable artifact
	Findings int `json:"findings"` // sessions whose findings are non-empty
	Memories int `json:"memories"` // sessions that wrote >=1 memory
	Notes    int `json:"notes"`    // sessions that created >=1 note
	Trials   int `json:"trials"`   // sessions that recorded >=1 trial
}

SessionCoverage measures how much Claude Code session knowledge Seamless is retaining: a session is "covered" when it left a durable artifact behind -- non-empty findings, or at least one written memory, note, or recorded trial. It is a rough proxy for "how much of what happened in a session did we keep". The per-channel counts (Findings/Memories/Notes/Trials) overlap -- a session can be covered several ways -- so only Total and Covered partition the set.

func GetSessionCoverage

func GetSessionCoverage(ctx context.Context, db *sql.DB, since time.Time) (SessionCoverage, error)

GetSessionCoverage computes the coverage roll-up in a single pass over the sessions created within the window (a zero `since` means all time), testing each against the event log for durable artifacts. It reads the event log directly rather than retrieval_stats, so it needs no rebuild.

func GetSessionCoverageForProject

func GetSessionCoverageForProject(ctx context.Context, db *sql.DB, project string, since time.Time) (SessionCoverage, error)

GetSessionCoverageForProject computes the session-coverage roll-up (see GetSessionCoverage) restricted to one project's sessions (project_slug = ?).

It rejects an empty project: an empty project_slug marks real global sessions, so "" is ambiguous between "all sessions" and "global-only" -- the caller who wants the all-sessions number must use GetSessionCoverage instead.

type SnippetHit

type SnippetHit struct {
	SearchHit
	Snippet string
}

SnippetHit is an FTS hit carrying the matched text in context. Snippet is the raw item text with the matched terms wrapped in SnippetStartMark/SnippetEndMark.

func FTSSearchSnippets

func FTSSearchSnippets(ctx context.Context, db *sql.DB, query string, kinds, projects []string, limit int) ([]SnippetHit, error)

FTSSearchSnippets is FTSSearch plus a generated snippet per hit: the same query, filters, and ordering, with the matched terms marked in context. The two share ftsSearch so the validity predicate, the scope filter, and the ordering cannot drift between the snippet and no-snippet paths -- callers rely on both returning identical hits for identical inputs.

type TaskPatch

type TaskPatch struct {
	Status       *core.TaskStatus
	Title        *string
	Body         *string
	ProjectSlug  *string
	AddDependsOn []string
}

TaskPatch is the set of mutable fields UpdateTask may change; nil fields are left untouched. AddDependsOn edges are added (existing edges are kept). ProjectSlug reassigns the task to another project (used when a split moves a project's open work to a child); the holder-lock still applies.

type TrendBucket

type TrendBucket struct {
	Label string `json:"label"`
	Count int    `json:"count"`
}

TrendBucket is one time bucket of the injection trend: a pre-formatted tick label and the item-level injection count that fell in it. The report's total Injected equals the sum of bucket counts, so the hero number and the chart always describe the same quantity over the same window.

type TrialFilter

type TrialFilter struct {
	Lab           string
	Outcome       string
	Project       string
	MetricsEquals map[string]any
	Limit         int
}

TrialFilter parameterizes QueryTrials. Empty string fields are not filtered; MetricsEquals matches trials whose metrics contain each given key with an equal value (compared after JSON normalization). A non-positive Limit defaults to 20.

type UsageSummary

type UsageSummary struct {
	Memories struct {
		Active int            `json:"active"`
		ByKind map[string]int `json:"byKind"`
	} `json:"memories"`
	Notes     int            `json:"notes"`
	Sessions  map[string]int `json:"sessions"` // status -> count
	Tasks     map[string]int `json:"tasks"`    // status -> count
	Retrieval struct {
		Injections  int          `json:"injections"`
		Reads       int          `json:"reads"`
		TopInjected []NamedCount `json:"topInjected"`
	} `json:"retrieval"`
	GardenerPending map[string]int `json:"gardenerPending"` // kind -> count
	EventsByKind    map[string]int `json:"eventsByKind"`
}

UsageSummary is a point-in-time roll-up of activity across the store, backing the usage_summary MCP tool and (later) the console's overview. It is derived entirely from the DB-of-record tables and the event log.

func GetUsageSummary

func GetUsageSummary(ctx context.Context, db *sql.DB) (UsageSummary, error)

GetUsageSummary computes the usage roll-up. It reads current retrieval_stats; callers wanting fresh numbers should RebuildRetrievalStats first.

Jump to

Keyboard shortcuts

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