interfaces

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 28, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrJobRunEventExists = goerr.New("job run event sequence already exists")

ErrJobRunEventExists is returned when JobRunEventRepository.Append is called with a Sequence that already exists for the same (key, runID). This signals a sequencer bug (e.g. two emitters not sharing a counter) rather than a transient collision; the caller should fail loudly.

View Source
var ErrJobRunLogExists = goerr.New("job run log already exists")

ErrJobRunLogExists is returned when JobRunLogRepository.Create is called with a (key, runID) that already exists. Hard error rather than silent overwrite so a duplicate RunID generation surfaces immediately.

View Source
var ErrJobRunLogNotFound = goerr.New("job run log not found")

ErrJobRunLogNotFound is returned when JobRunLogRepository.Get does not find a log for the given (key, runID).

View Source
var ErrJobRunNotFound = goerr.New("job run not found")

ErrJobRunNotFound is returned when a JobRunRepository operation expects an existing record for the key but none exists. Callers that treat "no prior run" as a normal idle case must check for this with errors.Is(err, ErrJobRunNotFound) rather than parsing strings.

View Source
var ErrTurnOwnerMismatch = goerr.New("turn lock owner mismatch")

ErrTurnOwnerMismatch is returned by Heartbeat / ReleaseTurnLock when the caller's ownerID does not match the current turn owner. The caller should treat it as a signal that its session was reclaimed (heartbeat staleness) or interrupted, and stop the in-flight turn.

Functions

func BuildListCaseConfig

func BuildListCaseConfig(opts ...ListCaseOption) *listCaseConfig

BuildListCaseConfig builds a listCaseConfig from options

Types

type AcquireResult

type AcquireResult struct {
	// Acquired is true when the caller now holds the turn lock.
	Acquired bool
	// Reclaimed is true when Acquired came from displacing a stale owner
	// (TurnHeartbeatAt older than staleAfter). For trace logging only.
	Reclaimed bool
	// IdempotentRetry is true when the caller's triggerTS is non-empty and
	// matches the existing turn's TurnTriggerTS — typically a duplicate
	// Slack event. Acquired is false in this case; the caller should drop
	// the trigger. Synthetic triggers (ws-switch etc.) pass triggerTS="" so
	// they never match and always proceed (or get Busy).
	IdempotentRetry bool
	// Session is the Session as observed after the operation. When Acquired
	// is true, this reflects the running turn (with TurnOwnerID set to the
	// caller). When Acquired is false (busy), this reflects the live owner's
	// state so the caller can format a busy notification.
	Session *model.Session
}

AcquireResult is the outcome of SessionRepository.AcquireTurnLock.

Exactly one of Acquired / IdempotentRetry should be true (or neither, when the lock is held by a different live owner — busy). Reclaimed implies Acquired (the lock was taken from a stale prior owner).

type ActionArchiveScope

type ActionArchiveScope int

ActionArchiveScope selects which slice of an action list to return.

const (
	// ActionArchiveScopeActiveOnly returns only non-archived actions.
	// This is the zero value and the default behaviour.
	ActionArchiveScopeActiveOnly ActionArchiveScope = iota
	// ActionArchiveScopeArchivedOnly returns only archived actions.
	ActionArchiveScopeArchivedOnly
	// ActionArchiveScopeAll returns both active and archived actions.
	ActionArchiveScopeAll
)

func (ActionArchiveScope) Allows

func (s ActionArchiveScope) Allows(isArchived bool) bool

Allows reports whether an action with the given archived state passes this scope's filter.

type ActionEventRepository

type ActionEventRepository interface {
	// Put inserts a new event. The ID must be unique within the action.
	Put(ctx context.Context, workspaceID string, actionID int64, event *model.ActionEvent) error

	// List returns events for the action, newest first. limit must be > 0.
	// cursor is the last-seen event ID for pagination; "" means start from the
	// newest. The returned cursor is "" when there are no more events.
	List(ctx context.Context, workspaceID string, actionID int64, limit int, cursor string) ([]*model.ActionEvent, string, error)
}

ActionEventRepository persists structural change events for an Action.

type ActionListOptions

type ActionListOptions struct {
	// ArchiveScope selects active / archived / both. Defaults to active only.
	ArchiveScope ActionArchiveScope

	// ExcludePrivateCaseActions, when true, drops every action whose parent
	// Case is private — unconditionally, regardless of channel membership.
	// This is stricter than the membership-based access control applied when
	// an auth token is present: it is the policy for entry points (such as
	// the MCP endpoint) where private Cases and their Actions must never be
	// exposed, not even to members. Defaults to false so existing callers
	// keep the membership-based behaviour.
	ExcludePrivateCaseActions bool
}

ActionListOptions controls how List / GetByCase / GetByCases filter actions.

type ActionMessageRepository

type ActionMessageRepository interface {
	// Put saves a Slack message under a specific action (upsert)
	Put(ctx context.Context, workspaceID string, actionID int64, msg *slack.Message) error

	// List retrieves messages for a specific action with pagination
	// Returns messages in descending order (newest first)
	List(ctx context.Context, workspaceID string, actionID int64, limit int, cursor string) ([]*slack.Message, string, error)
}

ActionMessageRepository defines the interface for action-scoped Slack message persistence. These are messages posted into the Slack thread of an Action's notification message.

type ActionRepository

type ActionRepository interface {
	// Create creates a new action with auto-generated ID
	Create(ctx context.Context, workspaceID string, action *model.Action) (*model.Action, error)

	// Get retrieves an action by ID. Archived actions are returned as-is so
	// callers can inspect history; UI/agent layers must enforce visibility.
	Get(ctx context.Context, workspaceID string, id int64) (*model.Action, error)

	// GetByIDs retrieves multiple actions by IDs in a single batch.
	// Returns a map keyed by action ID containing only the actions that
	// were found; missing IDs are silently absent from the result map.
	// Archived actions are included for the same reason Get returns
	// them: the GraphQL Action loader is used from sub-resolvers that
	// already need history visibility.
	GetByIDs(ctx context.Context, workspaceID string, ids []int64) (map[int64]*model.Action, error)

	// List retrieves all actions filtered by opts.ArchiveScope.
	List(ctx context.Context, workspaceID string, opts ActionListOptions) ([]*model.Action, error)

	// Update updates an existing action
	Update(ctx context.Context, workspaceID string, action *model.Action) (*model.Action, error)

	// Delete permanently removes an action document. This is INTERNAL ONLY:
	// callers are limited to the Case-deletion cascade in the usecase layer,
	// because the public Action lifecycle no longer exposes deletion. Use
	// ArchiveAction at the usecase layer for user-facing removal.
	Delete(ctx context.Context, workspaceID string, id int64) error

	// GetByCase retrieves all actions associated with a specific case,
	// filtered by opts.ArchiveScope.
	GetByCase(ctx context.Context, workspaceID string, caseID int64, opts ActionListOptions) ([]*model.Action, error)

	// GetByCases retrieves actions for multiple cases (for batch operations).
	// Returns a map of case ID to list of actions, filtered by
	// opts.ArchiveScope.
	GetByCases(ctx context.Context, workspaceID string, caseIDs []int64, opts ActionListOptions) (map[int64][]*model.Action, error)

	// GetBySlackMessageTS retrieves an action by its Slack message timestamp.
	// Returns ErrNotFound if no action matches. Archived actions ARE returned
	// because Slack threads must be resolvable regardless of archive state.
	GetBySlackMessageTS(ctx context.Context, workspaceID string, ts string) (*model.Action, error)
}

ActionRepository defines the interface for Action data access

type ActionStepRepository

type ActionStepRepository interface {
	// Put inserts or replaces a step. The ID must be unique within the action.
	Put(ctx context.Context, workspaceID string, step *model.ActionStep) error

	// Get retrieves a single step by id.
	Get(ctx context.Context, workspaceID string, actionID int64, stepID string) (*model.ActionStep, error)

	// List returns all steps for an action ordered by CreatedAt ascending.
	List(ctx context.Context, workspaceID string, actionID int64) ([]*model.ActionStep, error)

	// Delete removes a single step. Deleting a non-existent step is a no-op.
	Delete(ctx context.Context, workspaceID string, actionID int64, stepID string) error
}

ActionStepRepository persists per-Action ActionStep documents.

type AssistLogRepository

type AssistLogRepository interface {
	// Create creates a new assist log entry
	Create(ctx context.Context, workspaceID string, caseID int64, log *model.AssistLog) (*model.AssistLog, error)

	// List retrieves assist log entries for a specific case with pagination.
	// Returns items, totalCount, and error. Items are ordered by CreatedAt descending.
	List(ctx context.Context, workspaceID string, caseID int64, limit, offset int) ([]*model.AssistLog, int, error)
}

AssistLogRepository defines the interface for AssistLog data persistence

type CaseMessageRepository

type CaseMessageRepository interface {
	// Put saves a Slack message under a specific case (upsert)
	Put(ctx context.Context, workspaceID string, caseID int64, msg *slack.Message) error

	// List retrieves messages for a specific case with pagination
	// Returns messages in descending order (newest first)
	List(ctx context.Context, workspaceID string, caseID int64, limit int, cursor string) ([]*slack.Message, string, error)

	// Prune deletes messages older than the specified time for a specific case
	// Returns the number of messages deleted
	Prune(ctx context.Context, workspaceID string, caseID int64, before time.Time) (int, error)
}

CaseMessageRepository defines the interface for case-scoped Slack message persistence

type CaseProposalRepository

type CaseProposalRepository interface {
	// Save creates or fully overwrites a draft.
	Save(ctx context.Context, draft *model.CaseProposal) error

	// Get retrieves a draft by ID. Returns ErrNotFound when missing.
	// Implementations may return ErrNotFound for expired drafts.
	Get(ctx context.Context, id model.CaseProposalID) (*model.CaseProposal, error)

	// SetMaterialization updates the SelectedWorkspaceID, Materialization, and
	// InferenceInProgress fields atomically. Other fields are left untouched.
	// Pass m=nil with inProgress=true to mark inference as started.
	SetMaterialization(
		ctx context.Context,
		id model.CaseProposalID,
		workspaceID string,
		m *model.WorkspaceMaterialization,
		inProgress bool,
	) error

	// Delete removes the draft.
	Delete(ctx context.Context, id model.CaseProposalID) error
}

CaseProposalRepository persists workspace-agnostic Case drafts that are created when a user mentions the bot in a non-Case Slack channel. A draft holds collected source material plus the current AI materialization for the selected workspace; switching workspace overwrites the materialization.

type CaseRepository

type CaseRepository interface {
	// Create creates a new case with auto-generated ID
	Create(ctx context.Context, workspaceID string, c *model.Case) (*model.Case, error)

	// Get retrieves a case by ID
	Get(ctx context.Context, workspaceID string, id int64) (*model.Case, error)

	// GetByIDs retrieves multiple cases by IDs in a single batch.
	// Returns a map keyed by case ID containing only the cases that
	// were found; missing IDs are silently absent from the result map
	// (callers must distinguish "missing" from "found"). This is the
	// batch fetch hook used by the GraphQL DataLoader to collapse
	// per-row Reporter / Assignees lookups into one repository call
	// per request.
	GetByIDs(ctx context.Context, workspaceID string, ids []int64) (map[int64]*model.Case, error)

	// List retrieves cases with optional filtering.
	// Cases in DRAFT status are excluded by default; use ListDrafts to read
	// drafts. Passing WithStatus(CaseStatusDraft) honours the filter, but
	// callers should generally rely on ListDrafts for the draft-author view.
	List(ctx context.Context, workspaceID string, opts ...ListCaseOption) ([]*model.Case, error)

	// ListDrafts retrieves all cases in DRAFT status across the workspace.
	// Drafts are surfaced workspace-wide so any team member can pick up an
	// in-progress entry; the usecase layer applies private-draft access
	// control (private drafts are visible only to their reporter).
	ListDrafts(ctx context.Context, workspaceID string) ([]*model.Case, error)

	// Update updates an existing case
	Update(ctx context.Context, workspaceID string, c *model.Case) (*model.Case, error)

	// Transact reads the case, hands it to fn for mutation, and writes it back
	// inside a single transaction, returning the written case. Unlike a plain
	// Get followed by Update — which races with a concurrent edit in the window
	// between them — the read fn observes IS the state the write is applied to,
	// so simultaneous "assign me" actions cannot clobber one another and a
	// caller may compute a before/after diff that is guaranteed to describe
	// exactly what was persisted. A missing case fails with ErrNotFound. The
	// full case document is rewritten, so model invariants are re-validated.
	//
	// fn owns every field it changes, including UpdatedAt — the repository
	// never reads the clock. An fn that changes nothing simply leaves the
	// document byte-identical (the write still happens; do not rely on it being
	// skipped). An error returned by fn aborts the transaction without writing
	// and is propagated to the caller with its chain intact, so errors.Is /
	// errors.As on the result still work.
	//
	// fn MUST be idempotent and free of external side effects. The Firestore
	// backend retries the closure on contention, so a Slack post or a counter
	// increment inside fn can run more than once; a captured result variable
	// must be reset at the top of fn so a retry cannot accumulate onto the
	// previous attempt's value. Act on the outcome after Transact returns, not
	// inside it. fn must also not call back into the repository — the in-memory
	// backend holds its write lock for the duration of the call.
	Transact(ctx context.Context, workspaceID string, id int64, fn func(*model.Case) error) (*model.Case, error)

	// Delete deletes a case by ID
	Delete(ctx context.Context, workspaceID string, id int64) error

	// GetBySlackChannelID retrieves a case by its Slack channel ID.
	// Returns nil, nil if no case is found with the given channel ID.
	GetBySlackChannelID(ctx context.Context, workspaceID string, channelID string) (*model.Case, error)

	// GetBySlackThread retrieves a thread-mode case by its monitored channel
	// and thread timestamp. Returns nil, nil if no matching case exists. Used
	// for thread-mode case lookup (idempotent creation and reply ingest).
	GetBySlackThread(ctx context.Context, workspaceID string, channelID string, threadTS string) (*model.Case, error)

	// GetByRequestKey retrieves a case by its request key.
	// Returns nil, nil if no case is found with the given key.
	GetByRequestKey(ctx context.Context, workspaceID string, key string) (*model.Case, error)

	// CountFieldValues counts the total number of cases with the specified field
	// and how many of those have a value matching one of validValues.
	// invalidCount = total - valid detects the existence of invalid values
	// without transferring document data (uses aggregation queries).
	CountFieldValues(ctx context.Context, workspaceID string, fieldID string, fieldType types.FieldType, validValues []string) (total int64, valid int64, err error)

	// FindCaseWithInvalidFieldValue returns one case where the specified field
	// has a value not in validValues. Returns nil if all values are valid.
	// Intended to be called after CountFieldValues confirms invalid values exist.
	FindCaseWithInvalidFieldValue(ctx context.Context, workspaceID string, fieldID string, fieldType types.FieldType, validValues []string) (*model.Case, error)
}

CaseRepository defines the interface for Case data access

type EmbedClient

type EmbedClient interface {
	GenerateEmbedding(ctx context.Context, dimension int, input []string) ([][]float64, error)
}

EmbedClient produces fixed-dimension embedding vectors for the given input strings. The signature mirrors gollem.LLMClient.GenerateEmbedding so any gollem client (in practice, the Gemini client wired via the dedicated embedding configuration) satisfies it directly.

type HomeMessageRepository

type HomeMessageRepository interface {
	// Add appends one generated message (Validate then persist).
	Add(ctx context.Context, msg *model.HomeMessage) error
	// ListRecent returns the user's most recent messages, newest first, up to
	// limit. Returns an empty slice when the user has none.
	ListRecent(ctx context.Context, userID string, limit int) ([]*model.HomeMessage, error)
}

HomeMessageRepository persists LLM-generated home messages append-only, per user. There is no Update or Delete: freshness is judged by the caller from CreatedAt, and history is retained deliberately (anti-repetition input).

type ImportRepository

type ImportRepository interface {
	// Create persists a new ImportSession. Implementations MUST call
	// Validate on the session before write.
	Create(ctx context.Context, workspaceID string, s *model.ImportSession) (*model.ImportSession, error)

	// Update overwrites an existing ImportSession in place. Used by the
	// execute path to advance status, fill ExecutedAt, and stamp per-Case
	// results into the snapshot. Implementations MUST call Validate
	// before write.
	Update(ctx context.Context, workspaceID string, s *model.ImportSession) (*model.ImportSession, error)

	// Get retrieves an ImportSession by ID. Returns ErrNotFound (the
	// repository's standard sentinel) when missing.
	Get(ctx context.Context, workspaceID string, id model.ImportSessionID) (*model.ImportSession, error)
}

ImportRepository persists ImportSession documents that drive the YAML → Case/Action wizard. The session is created (status=pending) by createCaseImport and advanced exactly once to applied / failed by executeCaseImport; no Delete method exists because the spec keeps sessions indefinitely and exposes them only by URL.

type JobRunEventRepository

type JobRunEventRepository interface {
	// Append writes one event keyed by ev.EventID. Both EventID and
	// Sequence must be set by the caller; Sequence must be strictly
	// increasing across calls in the same Run. Backends use Create
	// (not Set) so a duplicate EventID surfaces as ErrJobRunEventExists.
	Append(ctx context.Context, ev *model.JobRunEvent) error

	// List returns events for (key, runID) in ascending Sequence order
	// (not doc-ID order — doc IDs are UUIDv7 and may diverge under
	// clock skew).
	List(ctx context.Context, key model.JobRunKey, runID string) ([]*model.JobRunEvent, error)
}

JobRunEventRepository persists the per-Run timeline of events (LLM_REQUEST / LLM_RESPONSE / TOOL_CALL / RUN_ERROR). Stored at:

workspaces/{WS}/cases/{Case}/jobRuns/{Job}/logs/{Run}/events/{EventID}

EventID is a UUIDv7 (timestamp-prefixed) chosen for Firestore-console readability and global uniqueness. The authoritative monotonic order is the Sequence field, not the doc ID — List MUST OrderBy("Sequence").

Within a single Run, exactly one runSequencer instance owns Sequence allocation — both the per-call appends from the trace.Handler AND any RUN_ERROR emits from JobRunner go through the same sequencer.

type JobRunLogRepository

type JobRunLogRepository interface {
	// Create writes the RUNNING-stage log. Errors with
	// ErrJobRunLogExists if a doc for the same (key, runID) already
	// exists; backends MUST use Firestore Create (or equivalent) so the
	// duplicate is rejected by the storage layer.
	Create(ctx context.Context, log *model.JobRunLog) error

	// Finish transitions an existing log to its terminal stage
	// (SUCCESS or FAILED). The caller supplies the full *JobRunLog
	// with Stage / EndedAt / Error populated; the implementation just
	// persists it.
	Finish(ctx context.Context, log *model.JobRunLog) error

	// Suspend transitions an existing log to the non-terminal
	// AWAITING_INPUT stage. The caller supplies the full *JobRunLog with
	// Stage=AWAITING_INPUT and PendingInteraction populated; EndedAt stays
	// zero. Errors with ErrJobRunLogNotFound if the log does not exist.
	Suspend(ctx context.Context, log *model.JobRunLog) error

	// Resume transitions a suspended log back to RUNNING. The caller
	// supplies the full *JobRunLog with Stage=RUNNING and
	// PendingInteraction cleared (nil). Errors with ErrJobRunLogNotFound if
	// the log does not exist.
	Resume(ctx context.Context, log *model.JobRunLog) error

	// Get returns the log identified by (key, runID), or
	// (nil, ErrJobRunLogNotFound) when no such log exists.
	Get(ctx context.Context, key model.JobRunKey, runID string) (*model.JobRunLog, error)

	// List returns logs under (key) in descending StartedAt order, up
	// to limit. limit <= 0 means no limit. Implemented as a single
	// subcollection scan per call (no cross-Job aggregation here).
	List(ctx context.Context, key model.JobRunKey, limit int) ([]*model.JobRunLog, error)
}

JobRunLogRepository persists one *invocation* of a Job (= one Run) against a Case. Stored at:

workspaces/{WorkspaceID}/cases/{CaseID}/jobRuns/{JobID}/logs/{RunID}

The Stage transitions RUNNING -> SUCCESS|FAILED. Callers Create the log in RUNNING state once prompts are ready, then Finish it when the agent loop terminates. A Run that crashes mid-flight leaves the RUNNING log in place; that is intentional so the events captured up to the crash remain attributable.

type JobRunRepository

type JobRunRepository interface {
	// Get returns the JobRun for the given key, or (nil, ErrJobRunNotFound)
	// when no prior run exists. Callers use this for scheduled due-checks
	// and for surface observability; both treat absence as "never run".
	Get(ctx context.Context, key model.JobRunKey) (*model.JobRun, error)

	// ListByCase returns every JobRun stored under the given (workspace,
	// case) tuple. Implemented as a single Firestore subcollection scan
	// per call (no cross-case work), which matches the underlying
	// storage layout. The scanner calls this once per OPEN case during a
	// tick — typical workspaces have a small number of jobs per case
	// (~handful), so a single subcollection query returns the entire
	// per-case index that the due-check needs.
	ListByCase(ctx context.Context, workspaceID string, caseID int64) ([]*model.JobRun, error)

	// TryAcquireLease attempts to take the lock for the given key, valid
	// until now+leaseDuration. Returns true if the caller now owns the
	// lease, false if a live lease is held by someone else. The first
	// acquirer on a previously-absent key also creates the record.
	//
	// Lease ownership is implicit in the LeaseUntil timestamp — there is
	// no separate owner ID because at most one process should be
	// scheduling the same (workspace, case, job) at any given moment, and
	// a stuck holder simply has its lease reclaimed once LeaseUntil
	// elapses.
	TryAcquireLease(ctx context.Context, key model.JobRunKey, now time.Time, leaseDuration time.Duration) (acquired bool, err error)

	// ReleaseLease clears LeaseUntil (sets it to the zero value) so the
	// next acquirer can take the lock immediately. Idempotent: calling
	// it without a prior acquisition is a no-op.
	ReleaseLease(ctx context.Context, key model.JobRunKey) error

	// RecordRun persists the terminal outcome of a Job run. It also
	// clears any lease that may still be active (treat RecordRun as
	// implying release) AND clears any suspension marker (a terminal run
	// is, by definition, no longer awaiting input). lastRunAt is the
	// caller's clock at the moment the run completed — repositories do not
	// stamp it themselves. runID identifies the specific JobRunLog produced
	// by this run and is mirrored into JobRun.LastRunID for cross-reference.
	RecordRun(ctx context.Context, key model.JobRunKey, status model.JobRunStatus, lastRunAt time.Time, runID, traceID, errMsg string) error

	// Suspend marks the (workspace, case, job) as awaiting user input for
	// the given runID and releases any active lease in the same atomic
	// step. While SuspendedRunID is set, the scheduler/dispatcher MUST NOT
	// start a new run for this tuple (see model.JobRun.IsSuspended). The
	// lease is released because a human wait can outlast any lease; the
	// suspension marker is the durable "do not double-start" signal.
	// suspendedAt is the caller's clock, used later by the unanswered-run
	// sweep to expire stale suspensions.
	Suspend(ctx context.Context, key model.JobRunKey, runID string, suspendedAt time.Time) error
}

JobRunRepository persists per-(workspace, case, job) execution metadata and provides atomic lease primitives for serialising concurrent runs.

The same document doubles as the run-history record and the lock holder: LeaseUntil represents "in flight" and the rest of the fields represent the most recently completed run. Storage backends must serialise lease transitions (Firestore RunTransaction, in-memory mutex) so two competing acquirers see a consistent view.

type KnowledgeListOptions

type KnowledgeListOptions struct {
	// TagIDs applies an AND filter: only entries referencing every listed tag id
	// are returned. An empty slice returns all entries. Filtering is done in
	// memory (no Firestore composite index) — see the repository implementations.
	TagIDs []model.TagID
}

KnowledgeListOptions controls how List filters knowledge entries.

type KnowledgeRepository

type KnowledgeRepository interface {
	// Create persists a new knowledge entry. The caller assigns the KnowledgeID
	// (via model.NewKnowledgeID) before calling; the repository does not generate
	// IDs.
	Create(ctx context.Context, workspaceID string, knowledge *model.Knowledge) (*model.Knowledge, error)

	// Get retrieves a knowledge entry by ID within a workspace.
	Get(ctx context.Context, workspaceID string, id model.KnowledgeID) (*model.Knowledge, error)

	// List retrieves the knowledge entries of a workspace, filtered by
	// opts.TagIDs (AND). Results are sorted by CreatedAt ascending.
	List(ctx context.Context, workspaceID string, opts KnowledgeListOptions) ([]*model.Knowledge, error)

	// Update persists changes to an existing knowledge entry. The caller's
	// pointer is the source of truth for every field.
	Update(ctx context.Context, workspaceID string, knowledge *model.Knowledge) (*model.Knowledge, error)

	// Delete removes a knowledge entry by ID within a workspace.
	Delete(ctx context.Context, workspaceID string, id model.KnowledgeID) error
}

KnowledgeRepository defines the interface for Knowledge data access. Every method is workspace-scoped; knowledge is shared across the whole workspace and is not tied to a case.

type ListCaseOption

type ListCaseOption func(*listCaseConfig)

ListCaseOption is a functional option for filtering cases in List

func WithStatus

func WithStatus(status types.CaseStatus) ListCaseOption

WithStatus filters cases by status

type MemoArchiveScope

type MemoArchiveScope int

MemoArchiveScope selects which slice of a memo list to return.

const (
	// MemoArchiveScopeActiveOnly returns only non-archived memos. This is the
	// zero value and the default: an unspecified List excludes archived memos
	// so callers never surface soft-deleted memories by accident.
	MemoArchiveScopeActiveOnly MemoArchiveScope = iota
	// MemoArchiveScopeArchivedOnly returns only archived memos.
	MemoArchiveScopeArchivedOnly
	// MemoArchiveScopeAll returns both active and archived memos.
	MemoArchiveScopeAll
)

func (MemoArchiveScope) Allows

func (s MemoArchiveScope) Allows(isArchived bool) bool

Allows reports whether a memo with the given archived state passes this scope's filter.

type MemoListOptions

type MemoListOptions struct {
	// ArchiveScope selects active / archived / both. Defaults to active only.
	ArchiveScope MemoArchiveScope
}

MemoListOptions controls how List filters memos.

type MemoRepository

type MemoRepository interface {
	// Create persists a new memo. The caller assigns the MemoID (via
	// model.NewMemoID) before calling; the repository does not generate IDs.
	Create(ctx context.Context, workspaceID string, memo *model.Memo) (*model.Memo, error)

	// Get retrieves a memo by ID within a Case. Archived memos are returned
	// as-is so callers holding the ID can inspect history; List is the
	// archive-filtered entry point.
	Get(ctx context.Context, workspaceID string, caseID int64, id model.MemoID) (*model.Memo, error)

	// GetByIDs retrieves multiple memos by ID within a Case in a single batch.
	// Returns a map keyed by MemoID containing only the memos that were found;
	// missing IDs are silently absent. Used by the GraphQL dataloader.
	GetByIDs(ctx context.Context, workspaceID string, caseID int64, ids []model.MemoID) (map[model.MemoID]*model.Memo, error)

	// List retrieves the memos of a Case, filtered by opts.ArchiveScope.
	List(ctx context.Context, workspaceID string, caseID int64, opts MemoListOptions) ([]*model.Memo, error)

	// Update persists changes to an existing memo (including archive/unarchive,
	// expressed by setting/clearing ArchivedAt on the caller's pointer).
	Update(ctx context.Context, workspaceID string, memo *model.Memo) (*model.Memo, error)
}

MemoRepository defines the interface for Memo data access. Every method is Case-scoped (requires caseID) so a memo can never be read or written outside its parent Case; there is no memoID-only lookup.

type NotificationSlotRepository

type NotificationSlotRepository interface {
	// GetActive returns the slot for channelID if ExpiresAt > now, otherwise
	// (nil, nil). An expired slot is treated as absent so the caller posts a
	// fresh channel message and replaces it via Save.
	GetActive(ctx context.Context, channelID string, now time.Time) (*model.NotificationSlot, error)

	// Save upserts the slot keyed by ChannelID.
	Save(ctx context.Context, slot *model.NotificationSlot) error

	// Delete removes the slot, e.g. when chat.update fails and the slot must
	// be reset so the next event starts a new channel message.
	Delete(ctx context.Context, channelID string) error
}

NotificationSlotRepository persists per-channel notification slots used to aggregate Slack channel-side notifications within a rolling time window. See pkg/usecase/notification_slot.go for the consumer.

type PolicyClient

type PolicyClient interface {
	Query(ctx context.Context, query string, input, out any) error
}

PolicyClient evaluates a Rego query against an input document and decodes the policy's result into out. It mirrors the opaq.Client.Query shape so the adapter is a thin wrapper, while keeping the usecase / controller layers free of any direct dependency on the Rego engine.

query is a fully-qualified Rego reference such as "data.auth.mcp". input is marshalled to the policy's `input` document; out receives the policy's result (typically a struct with an `allow` boolean). An evaluation that produces no result is reported as an error rather than a zero-valued out.

type ReactionClaimRepository

type ReactionClaimRepository interface {
	// Claim atomically records the (workspaceID, sourceChannelID,
	// sourceMessageTS) triple. It returns claimed=true for the first caller and
	// claimed=false when a claim already exists. Backed by an index-free
	// create-if-absent (a document keyed by a deterministic hash of the source
	// message), so it is safe across concurrent instances.
	Claim(ctx context.Context, workspaceID, sourceChannelID, sourceMessageTS string) (claimed bool, err error)

	// Release removes a claim so a future reaction on the same message can retry.
	// Called only when case creation failed after a successful Claim (e.g. the
	// seed root post failed, or the create turn fell back before asking a
	// question). Best-effort; a missing claim is not an error.
	Release(ctx context.Context, workspaceID, sourceChannelID, sourceMessageTS string) error
}

ReactionClaimRepository records, once per source message, that a reaction on that message has begun producing a cross-channel case. It is the idempotency gate for reaction-triggered case creation when the reacted message lives outside the workspace's monitored channel — there is no stable case-thread key to dedup on yet, so multiple users reacting (or a re-delivered event) would otherwise each spawn a case.

Same-channel reactions do NOT use this: the reacted message's thread root is a stable key, so the existing turn lock plus Case().GetBySlackThread already dedup that path.

type Repository

type Repository interface {
	Case() CaseRepository
	Action() ActionRepository
	Memo() MemoRepository
	Knowledge() KnowledgeRepository
	Tag() TagRepository
	Slack() SlackRepository
	SlackUser() SlackUserRepository
	Source() SourceRepository
	CaseMessage() CaseMessageRepository
	ActionMessage() ActionMessageRepository
	ActionEvent() ActionEventRepository
	ActionStep() ActionStepRepository
	AssistLog() AssistLogRepository
	CaseProposal() CaseProposalRepository
	Session() SessionRepository
	NotificationSlot() NotificationSlotRepository
	JobRun() JobRunRepository
	JobRunLog() JobRunLogRepository
	JobRunEvent() JobRunEventRepository
	Import() ImportRepository
	ReactionClaim() ReactionClaimRepository
	UserPreference() UserPreferenceRepository
	HomeMessage() HomeMessageRepository

	// Auth methods
	PutToken(ctx context.Context, token *auth.Token) error
	GetToken(ctx context.Context, tokenID auth.TokenID) (*auth.Token, error)
	DeleteToken(ctx context.Context, tokenID auth.TokenID) error

	// Close closes the repository and releases any resources
	Close() error
}

Repository defines the interface for data persistence

type SessionRepository

type SessionRepository interface {
	// GetByThread returns the Session for (channelID, threadTS), or
	// (nil, nil) when no Session exists yet.
	GetByThread(ctx context.Context, channelID, threadTS string) (*model.Session, error)

	// Put writes the Session. It does not touch the turn-lock fields
	// beyond what the caller has set; locks are normally maintained via
	// AcquireTurnLock / Heartbeat / ReleaseTurnLock instead.
	Put(ctx context.Context, s *model.Session) error

	// Claim atomically creates the Session from newSessionFn() when none
	// exists for (channelID, threadTS), and returns the stored Session
	// either way. An existing Session is returned untouched — Claim never
	// overwrites, so the first caller to reach a thread decides what owns
	// it (see model.SessionKind) and every later caller observes that
	// decision.
	//
	// It exists because "read, then create later" is not the same thing:
	// AcquireTurnLock also creates-if-missing, but a host only reaches it
	// after its own setup work, leaving a window in which a concurrent
	// event sees no Session and routes the thread somewhere else. Claim is
	// the durable marker a host takes BEFORE that work.
	Claim(ctx context.Context, channelID, threadTS string, newSessionFn func() *model.Session) (*model.Session, error)

	// AcquireTurnLock atomically transitions the Session from idle (or
	// stale-running) into running owned by ownerID and returns the result.
	//
	// If no Session exists yet, newSessionFn() is invoked to construct an
	// initial Session (with zero turn-lock fields) which is then claimed
	// in the same transaction.
	//
	// staleAfter controls when an existing running lock can be reclaimed
	// based on TurnHeartbeatAt age. Pass the configured heartbeat staleness
	// window (typically 30s).
	AcquireTurnLock(
		ctx context.Context,
		channelID, threadTS, triggerTS, ownerID string,
		staleAfter time.Duration,
		newSessionFn func() *model.Session,
	) (AcquireResult, error)

	// Heartbeat refreshes TurnHeartbeatAt to now if and only if the
	// Session's TurnOwnerID equals ownerID. Returns the latest Session
	// snapshot on success, or ErrTurnOwnerMismatch when the lock has
	// been taken by another owner (stale reclaim, interrupt, or release).
	Heartbeat(ctx context.Context, channelID, threadTS, ownerID string) (*model.Session, error)

	// ReleaseTurnLock atomically transitions the Session from running back
	// to idle, but only if TurnOwnerID matches ownerID. Mismatched releases
	// are silently ignored (no error) so the Phase A defer pattern is safe
	// to call even after a stale reclaim.
	ReleaseTurnLock(ctx context.Context, channelID, threadTS, ownerID string) error
}

SessionRepository persists Session metadata and provides atomic turn-lock primitives shared across modes (draft / casebound / future triage).

The lookup key is (ChannelID, ThreadTS). All lock operations are per-Session and serialised at the storage layer (Firestore RunTransaction or in-memory mutex). See spec §5.3 for full semantics.

type SlackRepository

type SlackRepository interface {
	// PutMessage saves a Slack message (upsert)
	PutMessage(ctx context.Context, msg *slack.Message) error

	// ListMessages retrieves messages from a specific channel within a time range
	// Returns messages in descending order (newest first) with pagination support
	ListMessages(ctx context.Context, channelID string, start, end time.Time, limit int, cursor string) ([]*slack.Message, string, error)

	// PruneMessages deletes messages older than the specified time
	// If channelID is empty, deletes from all channels
	// Returns the number of messages deleted
	PruneMessages(ctx context.Context, channelID string, before time.Time) (int, error)
}

SlackRepository defines the interface for Slack message persistence

type SlackUserRepository

type SlackUserRepository interface {
	// GetAll retrieves all Slack users from the database
	GetAll(ctx context.Context) ([]*model.SlackUser, error)

	// GetByID retrieves a single Slack user by ID
	GetByID(ctx context.Context, id model.SlackUserID) (*model.SlackUser, error)

	// GetByIDs retrieves multiple Slack users by IDs (for DataLoader batching)
	// Returns a map of ID -> SlackUser. Missing users are not included in the map.
	GetByIDs(ctx context.Context, ids []model.SlackUserID) (map[model.SlackUserID]*model.SlackUser, error)

	// SaveMany saves multiple Slack users (upsert operation)
	// Handles Firestore batch write limits (500 per batch) internally
	SaveMany(ctx context.Context, users []*model.SlackUser) error

	// DeleteAll deletes all Slack users from the database
	// Handles Firestore batch delete limits (500 per batch) internally
	DeleteAll(ctx context.Context) error

	// GetMetadata retrieves refresh metadata
	GetMetadata(ctx context.Context) (*model.SlackUserMetadata, error)

	// SaveMetadata saves refresh metadata
	SaveMetadata(ctx context.Context, metadata *model.SlackUserMetadata) error
}

SlackUserRepository provides database operations for Slack users.

N+1 Prevention Policy: - NO individual Save(user) method - always use SaveMany for batch writes - GetByID is minimal - prefer GetByIDs for batch retrieval via DataLoader - Worker always uses bulk operations: DeleteAll → SaveMany (Replace strategy) - All operations avoid loops with individual DB calls

type SourceRepository

type SourceRepository interface {
	// Create creates a new source
	Create(ctx context.Context, workspaceID string, source *model.Source) (*model.Source, error)

	// Get retrieves a source by ID
	Get(ctx context.Context, workspaceID string, id model.SourceID) (*model.Source, error)

	// List retrieves all sources
	List(ctx context.Context, workspaceID string) ([]*model.Source, error)

	// Update updates an existing source
	Update(ctx context.Context, workspaceID string, source *model.Source) (*model.Source, error)

	// Delete deletes a source by ID
	Delete(ctx context.Context, workspaceID string, id model.SourceID) error
}

SourceRepository defines the interface for Source data persistence

type TagRepository

type TagRepository interface {
	// Create persists a new tag. The caller assigns the TagID (via
	// model.NewTagID) and stamps CreatedAt / UpdatedAt before calling; the
	// repository does not generate IDs or timestamps.
	Create(ctx context.Context, workspaceID string, tag *model.Tag) (*model.Tag, error)

	// Get retrieves a tag by ID within a workspace.
	Get(ctx context.Context, workspaceID string, id model.TagID) (*model.Tag, error)

	// List retrieves every tag of a workspace, sorted by CreatedAt ascending.
	List(ctx context.Context, workspaceID string) ([]*model.Tag, error)

	// Update persists changes to an existing tag (only Name is mutable). The
	// caller's pointer is the source of truth for every field.
	Update(ctx context.Context, workspaceID string, tag *model.Tag) (*model.Tag, error)

	// Delete removes a tag by ID within a workspace. The caller (usecase) is
	// responsible for refusing deletion of a tag still referenced by any
	// Knowledge; the repository performs the raw delete.
	Delete(ctx context.Context, workspaceID string, id model.TagID) error
}

TagRepository defines the interface for Tag data access. Every method is workspace-scoped; tags are first-class, workspace-wide classification labels referenced by Knowledge entries via TagID.

type UserPreferenceRepository

type UserPreferenceRepository interface {
	// Get returns the user's preference. Returns the backend's ErrNotFound
	// (memory.ErrNotFound / firestore.ErrNotFound) when the user has none yet.
	Get(ctx context.Context, userID string) (*model.UserPreference, error)
	// Set writes the preference wholesale (Validate then persist).
	Set(ctx context.Context, pref *model.UserPreference) error
}

UserPreferenceRepository persists per-user settings. It is a single document per user (keyed by Slack User ID), so it needs only Get/Set — no List.

Jump to

Keyboard shortcuts

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