Files
kandev/apps/backend/internal/task/repository/interface.go
T
2026-08-18 22:30:38 +01:00

574 lines
38 KiB
Go

package repository
import (
"context"
"time"
agentdto "github.com/kandev/kandev/internal/agent/dto"
"github.com/kandev/kandev/internal/task/models"
"github.com/kandev/kandev/internal/task/repository/repoerrors"
"github.com/kandev/kandev/internal/task/statussummary"
wfmodels "github.com/kandev/kandev/internal/workflow/models"
v1 "github.com/kandev/kandev/pkg/api/v1"
)
var ErrWorkspaceNameMismatch = repoerrors.ErrWorkspaceNameMismatch
var ErrWorkspaceNotFound = repoerrors.ErrWorkspaceNotFound
var ErrTaskNotFound = repoerrors.ErrTaskNotFound
var ErrTaskPlanNotFound = repoerrors.ErrTaskPlanNotFound
var ErrRepositoryNotFound = repoerrors.ErrRepositoryNotFound
var ErrTaskEnvironmentNotFound = repoerrors.ErrTaskEnvironmentNotFound
var ErrWIPLimitExceeded = wfmodels.ErrWIPLimitExceeded
var ErrExternalIDConflict = repoerrors.ErrExternalIDConflict
// WorkspaceRepository handles workspace CRUD.
type WorkspaceRepository interface {
CreateWorkspace(ctx context.Context, workspace *models.Workspace) error
GetWorkspace(ctx context.Context, id string) (*models.Workspace, error)
UpdateWorkspace(ctx context.Context, workspace *models.Workspace) error
DeleteWorkspace(ctx context.Context, id string) error
DeleteWorkspaceCascade(ctx context.Context, id string) ([]*models.Task, []*models.Workflow, error)
DeleteWorkspaceCascadeWithName(ctx context.Context, id, name string) ([]*models.Task, []*models.Workflow, error)
ListWorkspaces(ctx context.Context) ([]*models.Workspace, error)
}
// TaskRepository handles task CRUD and workflow placement.
// Note: models.TaskRepository is a struct in internal/task/models; no Go conflict exists.
type TaskRepository interface {
CreateTask(ctx context.Context, task *models.Task) error
GetTask(ctx context.Context, id string) (*models.Task, error)
GetTasksByIDs(ctx context.Context, ids []string) ([]*models.Task, error)
UpdateTask(ctx context.Context, task *models.Task) error
DeleteTask(ctx context.Context, id string) error
ListTasks(ctx context.Context, workflowID string) ([]*models.Task, error)
ListTasksByWorkspace(ctx context.Context, workspaceID, workflowID, repositoryID, query string, page, pageSize int, sort string, includeArchived, includeEphemeral, onlyEphemeral, excludeConfig bool) ([]*models.Task, int, error)
ListTasksByWorkflowStep(ctx context.Context, workflowStepID string) ([]*models.Task, error)
ArchiveTask(ctx context.Context, id string) error
// ArchiveTaskIfActive is the CAS variant used by office task-handoffs
// cascade archives. Returns whether the row was updated.
ArchiveTaskIfActive(ctx context.Context, id, cascadeID string) (bool, error)
// UnarchiveTaskByCascade clears archived_at only when the task was
// archived by the named cascade. Returns whether the row was updated.
UnarchiveTaskByCascade(ctx context.Context, id, cascadeID string) (bool, error)
// UnarchiveTask clears archived_at only when the task carries no
// cascade stamp (archived_by_cascade_id empty/NULL) — the CAS keeps a
// delayed manual unarchive from erasing a newer cascade archive.
// Cascade-stamped rows are restored via UnarchiveTaskByCascade.
// Returns whether the row was updated.
UnarchiveTask(ctx context.Context, id string) (bool, error)
ListTasksForAutoArchive(ctx context.Context) ([]*models.Task, error)
// ListArchivedTasksWithActiveSessions returns the IDs of archived tasks
// (archived_at IS NOT NULL) that still have at least one task_sessions
// row in an active DB state (CREATED/STARTING/RUNNING/WAITING_FOR_INPUT).
// Candidate list for the periodic reconciliation sweep that recovers
// sessions left stranded when finalizeCancelledSessions's bounded
// in-line retry was exhausted by sustained SQLite writer contention.
ListArchivedTasksWithActiveSessions(ctx context.Context) ([]string, error)
ListExpiredQuickChatTasks(ctx context.Context, cutoff time.Time) ([]*models.Task, error)
DeleteExpiredQuickChatTask(ctx context.Context, id string, cutoff time.Time) (bool, error)
// CountOpenWatcherCreatedTasks returns the number of open watcher-created
// tasks for a single watch, identified by the integration's task-metadata
// key (e.g. "sentry_issue_watch_id") and the watch id. Open = non-archived
// AND state NOT IN (COMPLETED, FAILED, CANCELLED). Used by the
// orchestrator's watcher throttle gate to enforce a per-watch cap. Keyed
// by metadata key (not integration name) so this layer stays agnostic of
// which integrations exist.
CountOpenWatcherCreatedTasks(ctx context.Context, metadataKey, watchID string) (int, error)
// SetTaskMetadataKeyIfPresent rewrites one metadata key only while that
// key is still present, reporting whether the write landed. The CAS
// counterpart to an atomic remove: an editor must never re-create a key a
// concurrent claim just consumed.
SetTaskMetadataKeyIfPresent(ctx context.Context, taskID, key string, value interface{}) (bool, error)
UpdateTaskState(ctx context.Context, id string, state v1.TaskState) error
// UpdateTaskStateIfSessionState atomically transitions task state only while
// the named session remains in expectedSessionState and the task is not
// archived. Returns the pre-update state and whether a row changed.
UpdateTaskStateIfSessionState(
ctx context.Context,
taskID, sessionID string,
expectedSessionState models.TaskSessionState,
state v1.TaskState,
) (v1.TaskState, bool, error)
// UpdateTaskStateIfCurrentIn atomically transitions state only when the
// task's current state is in allowed AND the task is not archived
// (archived_at IS NULL). The archived check is enforced inside the same
// UPDATE's WHERE clause, not just by a caller's earlier (non-transactional)
// read, so a late write can never race an ArchiveTask commit that lands
// between that read and this call. Returns the pre-update state and
// whether a row was modified.
UpdateTaskStateIfCurrentIn(ctx context.Context, id string, state v1.TaskState, allowed []v1.TaskState) (v1.TaskState, bool, error)
// UpdateTaskStateIfNotArchived is UpdateTaskStateIfCurrentIn without the
// prior-state constraint — for writers (e.g. IN_PROGRESS reconciliation)
// that legitimately fire from many prior states and only need the
// archived_at IS NULL guarantee. Same TOCTOU-closing semantics: the
// archived check is atomic with the write. Returns the pre-update state
// and whether a row was modified.
UpdateTaskStateIfNotArchived(ctx context.Context, id string, state v1.TaskState) (v1.TaskState, bool, error)
CountTasksByWorkflow(ctx context.Context, workflowID string) (int, error)
CountTasksByWorkflowStep(ctx context.Context, stepID string) (int, error)
AddTaskToWorkflow(ctx context.Context, taskID, workflowID, workflowStepID string, position int) error
RemoveTaskFromWorkflow(ctx context.Context, taskID, workflowID string) error
ListTasksByProject(ctx context.Context, projectID string) ([]*models.Task, error)
ListTasksByAssignee(ctx context.Context, agentInstanceID string) ([]*models.Task, error)
ListTaskTree(ctx context.Context, workspaceID string, filters models.TaskTreeFilters) ([]*models.Task, error)
// ListChildren returns non-archived, non-ephemeral child tasks of parentID.
ListChildren(ctx context.Context, parentID string) ([]*models.Task, error)
// ListChildrenIncludingArchived returns ALL child tasks of parentID,
// including archived ones. Used by the office task-handoffs unarchive
// cascade (phase 6) to walk a previously-archived descendant tree.
ListChildrenIncludingArchived(ctx context.Context, parentID string) ([]*models.Task, error)
// ReparentDirectChildren updates every row whose parent_id matches
// oldParentID, replacing it with newParentID. Used by no-cascade
// delete so direct children of a deleted task become roots
// (newParentID="") instead of dangling pointers. Affects archived
// and active rows alike.
ReparentDirectChildren(ctx context.Context, oldParentID, newParentID string) error
// ListSiblings returns non-archived, non-ephemeral sibling tasks for taskID.
// A task is a sibling of taskID when it shares a non-empty parent_id and
// the same workspace_id, and is not taskID itself. Root tasks (empty
// parent_id) intentionally have NO siblings — without a non-empty common
// parent, every other root in the workspace would falsely match.
ListSiblings(ctx context.Context, taskID string) ([]*models.Task, error)
IncrementTaskSequence(ctx context.Context, workspaceID string) (int, error)
GetWorkspaceTaskPrefix(ctx context.Context, workspaceID string) (prefix, officeWorkflowID string, err error)
// GetTaskByExternalID returns the task holding (workspaceID, externalID),
// including archived and unsettled tasks, or ErrTaskNotFound if none does.
GetTaskByExternalID(ctx context.Context, workspaceID, externalID string) (*models.Task, error)
// SettleTaskExternalID stamps external_id_settled_at on the task if it
// still holds externalID and has not already been settled. The predicate
// includes external_id (not just id) because release clears both columns,
// so guarding on id alone would wrongly stamp a released row. Returns
// whether a row was updated.
SettleTaskExternalID(ctx context.Context, taskID, externalID string, settledAt time.Time) (bool, error)
// ReleaseTaskExternalID clears external_id and external_id_settled_at on
// the task holding (workspaceID, externalID), without deleting the task,
// and bumps updated_at. Returns the task as it exists immediately after
// the update, or nil if no task held the identity.
ReleaseTaskExternalID(ctx context.Context, workspaceID, externalID string) (*models.Task, error)
}
// TaskStatusSummaryRepository stores the bounded task-level projection used by
// list and switcher surfaces. Implementations must compare revisions and the
// semantic payload atomically so retries and concurrent source observations do
// not regress a task or create a revision for a no-op.
type TaskStatusSummaryRepository interface {
LoadTaskStatusSummaries(ctx context.Context, taskIDs []string) (map[string]*statussummary.TaskStatusSummary, error)
CompareAndUpdateTaskStatusSummary(ctx context.Context, stored *statussummary.StoredTaskStatusSummary) (bool, error)
DeleteTaskStatusSummary(ctx context.Context, taskID string) error
}
// TaskActivityRepository reconstructs the bounded task activity timestamp
// from authoritative task, prompt, and turn rows.
type TaskActivityRepository interface {
LoadTaskLastActivity(ctx context.Context, taskIDs []string) (map[string]time.Time, error)
}
// TaskRepoRepository handles the task↔repository junction table (models.TaskRepository rows).
// Named TaskRepoRepository to reduce reader confusion with the TaskRepository sub-interface above.
type TaskRepoRepository interface {
CreateTaskRepository(ctx context.Context, taskRepo *models.TaskRepository) error
GetTaskRepository(ctx context.Context, id string) (*models.TaskRepository, error)
ListTaskRepositories(ctx context.Context, taskID string) ([]*models.TaskRepository, error)
ListTaskRepositoriesByTaskIDs(ctx context.Context, taskIDs []string) (map[string][]*models.TaskRepository, error)
UpdateTaskRepository(ctx context.Context, taskRepo *models.TaskRepository) error
DeleteTaskRepository(ctx context.Context, id string) error
DeleteTaskRepositoriesByTask(ctx context.Context, taskID string) error
GetPrimaryTaskRepository(ctx context.Context, taskID string) (*models.TaskRepository, error)
}
// TaskWorkspaceFolderRepository handles canonical non-Git folder attachments.
// It is intentionally separate from TaskRepoRepository to preserve existing
// repository payload and Git-consumer contracts.
type TaskWorkspaceFolderRepository interface {
ListTaskWorkspaceFolders(ctx context.Context, taskID string) ([]*models.TaskWorkspaceFolder, error)
ListTaskWorkspaceFoldersByTaskIDs(ctx context.Context, taskIDs []string) (map[string][]*models.TaskWorkspaceFolder, error)
CreateWorkspaceSourceBatch(ctx context.Context, batch *models.WorkspaceSourceBatch) error
CompensateWorkspaceSourceBatch(ctx context.Context, batch *models.WorkspaceSourceBatch) error
}
// WorkflowRepository handles workflow CRUD.
type WorkflowRepository interface {
CreateWorkflow(ctx context.Context, workflow *models.Workflow) error
GetWorkflow(ctx context.Context, id string) (*models.Workflow, error)
UpdateWorkflow(ctx context.Context, workflow *models.Workflow) error
DeleteWorkflow(ctx context.Context, id string) error
ListWorkflows(ctx context.Context, workspaceID string, includeHidden bool) ([]*models.Workflow, error)
ReorderWorkflows(ctx context.Context, workspaceID string, workflowIDs []string) error
}
// MessageRepository handles message persistence and lookups.
type MessageRepository interface {
CreateMessage(ctx context.Context, message *models.Message) error
GetMessage(ctx context.Context, id string) (*models.Message, error)
GetMessageByToolCallID(ctx context.Context, sessionID, toolCallID string) (*models.Message, error)
GetMessageByPendingID(ctx context.Context, sessionID, pendingID string) (*models.Message, error)
FindMessageByPendingID(ctx context.Context, pendingID string) (*models.Message, error)
FindMessagesByPendingID(ctx context.Context, pendingID string) ([]*models.Message, error)
FindMessageByPendingIDAndQuestion(ctx context.Context, sessionID, pendingID, questionID string) (*models.Message, error)
FindActiveClarificationMessagesBySessionID(ctx context.Context, sessionID string) ([]*models.Message, error)
GetPendingActionsBySessionIDs(ctx context.Context, sessionIDs []string) (map[string]models.TaskPendingAction, error)
CompleteActiveClarificationBundle(ctx context.Context, pendingID, status string, responses map[string]interface{}) ([]*models.Message, bool, error)
FinalizeClarificationResponseDelivery(ctx context.Context, pendingID, terminalStatus string, claimedMessages []*models.Message) ([]*models.Message, bool, error)
RestoreActiveClarificationBundle(ctx context.Context, pendingID, terminalStatus string, claimedMessages []*models.Message) ([]*models.Message, bool, error)
UpdateMessage(ctx context.Context, message *models.Message) error
ListMessages(ctx context.Context, sessionID string) ([]*models.Message, error)
ListMessagesByTurnID(ctx context.Context, turnID string) ([]*models.Message, error)
ListMessagesPaginated(ctx context.Context, sessionID string, opts models.ListMessagesOptions) ([]*models.Message, bool, error)
ListMessagesForPlugin(ctx context.Context, filter models.PluginMessageFilter) ([]*models.Message, error)
SearchMessages(ctx context.Context, sessionID string, opts models.SearchMessagesOptions) ([]*models.Message, error)
DeleteMessage(ctx context.Context, id string) error
}
// AttachmentRepository stores file-backed prompt attachment descriptors.
// Implementations must keep ownership and aggregate-claim checks in the same
// transaction as state transitions so a retry cannot partially claim a batch.
type AttachmentRepository interface {
CreateMessageAttachment(ctx context.Context, attachment *models.TaskMessageAttachment) error
GetMessageAttachment(ctx context.Context, id string) (*models.TaskMessageAttachment, error)
ListMessageAttachments(ctx context.Context, ids []string) ([]*models.TaskMessageAttachment, error)
ClaimMessageAttachments(ctx context.Context, ids []string, ownerID, workspaceID, taskID, sessionID string) error
DeleteClaimedMessageAttachments(ctx context.Context, ids []string, ownerID, taskID, sessionID string) ([]*models.TaskMessageAttachment, error)
DeleteMessageAttachmentsByTask(ctx context.Context, taskID string) ([]*models.TaskMessageAttachment, error)
DeleteMessageAttachment(ctx context.Context, id, ownerID string) error
MarkExpiredMessageAttachments(ctx context.Context, now time.Time) ([]*models.TaskMessageAttachment, error)
}
// TurnRepository handles conversation turn persistence.
type TurnRepository interface {
CreateTurn(ctx context.Context, turn *models.Turn) error
DeleteTurnIfUnreferenced(ctx context.Context, sessionID, turnID string) (bool, error)
// ReconcileUnpublishedPromptTurns repairs or accepts durable prompt
// reservations and response-delivery claims before startup admits new work.
// Accepted reservations retain a durable start-event outbox marker for the
// service to replay before it clears recovery metadata.
// Every production turn store must provide this recovery boundary; callers
// fail rather than skip it.
ReconcileUnpublishedPromptTurns(ctx context.Context) (int, error)
// ListTurnsPendingStartEvent returns accepted reservations whose durable
// start-event outbox marker still needs replay before startup admits work.
ListTurnsPendingStartEvent(ctx context.Context) ([]*models.Turn, error)
// CreateTurnWithStepStamp creates turn atomically with the
// workflow-step-at-start stamp: it reads the task's current step and
// inserts the turn row in the same transaction, taking the same lock
// readTaskStepInTx takes for step moves, so the stamp reflects a state
// serialized against concurrent movers of the same task rather than a
// plain unlocked read taken before the insert. A task-step read failure
// (missing task, transient error) degrades to an unstamped turn rather
// than failing turn creation. Returns whether the stamp was applied.
CreateTurnWithStepStamp(ctx context.Context, turn *models.Turn) (stamped bool, err error)
GetTurn(ctx context.Context, id string) (*models.Turn, error)
GetActiveTurnBySessionID(ctx context.Context, sessionID string) (*models.Turn, error)
UpdateTurn(ctx context.Context, turn *models.Turn) error
// PatchTurnMetadata merges fields into an active or completed turn while
// preserving unrelated metadata under the session's turn-write authority.
PatchTurnMetadata(
ctx context.Context,
sessionID, turnID string,
updates map[string]interface{},
) (bool, time.Time, error)
// UpdateActiveTurnMetadata merges updates and removes named keys only while
// the turn is active and belongs to sessionID. Implementations serialize
// this authority change with other current-turn decisions for the session.
UpdateActiveTurnMetadata(
ctx context.Context,
sessionID, turnID string,
updates map[string]interface{},
removeKeys []string,
) (bool, map[string]interface{}, time.Time, error)
// ClearTurnPromptDispatchMetadata removes reservation-only metadata from
// an active or completed turn after its start event has been accepted by
// the event bus. It preserves unrelated concurrent metadata.
ClearTurnPromptDispatchMetadata(
ctx context.Context,
sessionID, turnID string,
) (bool, map[string]interface{}, time.Time, error)
CompleteTurn(ctx context.Context, id string) error
AbandonTurn(ctx context.Context, id string) error
CompletePendingToolCallsForTurn(ctx context.Context, turnID string) (int64, error)
ListTurnsBySession(ctx context.Context, sessionID string) ([]*models.Turn, error)
}
// SessionRepository handles task session lifecycle and workflow-session relationships.
type SessionRepository interface {
CreateTaskSession(ctx context.Context, session *models.TaskSession) error
GetTaskSession(ctx context.Context, id string) (*models.TaskSession, error)
GetTaskSessionByTaskID(ctx context.Context, taskID string) (*models.TaskSession, error)
GetActiveTaskSessionByTaskID(ctx context.Context, taskID string) (*models.TaskSession, error)
UpdateTaskSession(ctx context.Context, session *models.TaskSession) error
UpdateTaskSessionState(ctx context.Context, id string, state models.TaskSessionState, errorMessage string) error
// ClaimPromptableTaskSessionIfActive marks a promptable session RUNNING only
// while its owning task remains active. The state transition is the prompt
// claim: archive either wins first or follows normal cancellation semantics.
ClaimPromptableTaskSessionIfActive(ctx context.Context, id string) (models.PromptableTaskSessionClaim, error)
ResetTaskSessionBasesForRepository(ctx context.Context, taskID, repositoryID, baseBranch string) (int64, error)
ListTaskSessions(ctx context.Context, taskID string) ([]*models.TaskSession, error)
ListActiveTaskSessions(ctx context.Context) ([]*models.TaskSession, error)
ListActiveTaskSessionsByTaskID(ctx context.Context, taskID string) ([]*models.TaskSession, error)
CancelActiveTaskSessionsByTaskID(ctx context.Context, taskID, reason string) ([]*models.TaskSession, error)
HasActiveTaskSessionsByAgentProfile(ctx context.Context, agentProfileID string) (bool, error)
GetActiveTaskInfoByAgentProfile(ctx context.Context, agentProfileID string) ([]agentdto.ActiveTaskInfo, error)
HasActiveTaskSessionsByExecutor(ctx context.Context, executorID string) (bool, error)
HasActiveTaskSessionsByEnvironment(ctx context.Context, environmentID string) (bool, error)
HasActiveTaskSessionsByRepository(ctx context.Context, repositoryID string) (bool, error)
CountActiveTaskSessionsByRepository(ctx context.Context, repositoryID string) (int, error)
DeleteEphemeralTasksByAgentProfile(ctx context.Context, agentProfileID string) (int64, error)
DeleteTaskSession(ctx context.Context, id string) error
GetPrimarySessionByTaskID(ctx context.Context, taskID string) (*models.TaskSession, error)
GetPrimarySessionIDsByTaskIDs(ctx context.Context, taskIDs []string) (map[string]string, error)
GetSessionCountsByTaskIDs(ctx context.Context, taskIDs []string) (map[string]int, error)
GetPrimarySessionInfoByTaskIDs(ctx context.Context, taskIDs []string) (map[string]*models.TaskSession, error)
// BatchGetSessionsByTaskIDs returns every session for the given task IDs
// grouped by task ID, ordered by started_at DESC within each task. One
// query (chunked to stay within SQLite's host-parameter limit) replaces
// per-task GetSession loops on the task-list path.
BatchGetSessionsByTaskIDs(ctx context.Context, taskIDs []string) (map[string][]*models.TaskSession, error)
SetSessionPrimary(ctx context.Context, sessionID string) error
UpdateSessionReviewStatus(ctx context.Context, sessionID string, status string) error
UpdateSessionMetadata(ctx context.Context, sessionID string, metadata map[string]interface{}) error
SetSessionMetadataKey(ctx context.Context, sessionID, key string, value interface{}) error
SetSessionACPSessionID(ctx context.Context, sessionID, acpSessionID string) (bool, error)
DismissLastAgentError(ctx context.Context, sessionID string, expected models.LastAgentError, dismissedAt time.Time) (bool, error)
GetLastAgentMessage(ctx context.Context, sessionID string) (string, error)
UpdateTaskSessionLastReadMessageID(ctx context.Context, id, messageID string) error
}
// SessionWorktreeRepository exposes session-scoped worktree projections over
// the task environment's repository rows. Sessions reference worktrees only
// through task_sessions.task_environment_id.
type SessionWorktreeRepository interface {
UpdateTaskSessionWorktreeBranch(ctx context.Context, sessionID, branch string) error
UpdateTaskSessionWorktreeBranchByRepository(ctx context.Context, sessionID, repositoryID, branch string) error
ListTaskSessionWorktrees(ctx context.Context, sessionID string) ([]*models.TaskEnvironmentRepo, error)
ListWorktreesBySessionIDs(ctx context.Context, sessionIDs []string) (map[string][]*models.TaskEnvironmentRepo, error)
}
// TaskResourceCleanupRepository persists restart-safe task lifecycle cleanup.
type TaskResourceCleanupRepository interface {
CreateTaskResourceCleanupJob(ctx context.Context, job *models.TaskResourceCleanupJob) error
HasActiveTaskResourceCleanupJob(ctx context.Context, taskID string) (bool, error)
UpdateTaskResourceCleanupSnapshot(ctx context.Context, operationID, snapshot string) error
GetTaskResourceCleanupJob(ctx context.Context, id string) (*models.TaskResourceCleanupJob, error)
GetTaskResourceCleanupJobByOperationID(ctx context.Context, operationID string) (*models.TaskResourceCleanupJob, error)
ListPreparedTaskResourceCleanupJobs(ctx context.Context) ([]*models.TaskResourceCleanupJob, error)
ListDueTaskResourceCleanupJobs(ctx context.Context, now time.Time, limit int) ([]*models.TaskResourceCleanupJob, error)
StartPreparedTaskResourceCleanupJob(ctx context.Context, id string) (bool, error)
MarkTaskResourceCleanupJobRunning(ctx context.Context, id string) (bool, error)
CompleteClaimedTaskResourceCleanupJob(ctx context.Context, id string, attempt int, state models.TaskResourceCleanupState, lastError string, nextAttemptAt *time.Time) (bool, error)
CompleteTaskResourceCleanupJob(ctx context.Context, id string, state models.TaskResourceCleanupState, lastError string, nextAttemptAt *time.Time) error
CancelArchiveTaskResourceCleanupJobs(ctx context.Context, taskID string) error
ResetRunningTaskResourceCleanupJobs(ctx context.Context) error
}
// GitSnapshotRepository handles git snapshots and session commit records.
type GitSnapshotRepository interface {
CreateGitSnapshot(ctx context.Context, snapshot *models.GitSnapshot) error
GetLatestGitSnapshot(ctx context.Context, sessionID string) (*models.GitSnapshot, error)
GetLatestGitSnapshotsBySessionIDs(ctx context.Context, sessionIDs []string) (map[string]*models.GitSnapshot, error)
GetFirstGitSnapshot(ctx context.Context, sessionID string) (*models.GitSnapshot, error)
GetGitSnapshotsBySession(ctx context.Context, sessionID string, limit int) ([]*models.GitSnapshot, error)
CreateSessionCommit(ctx context.Context, commit *models.SessionCommit) (bool, error)
GetSessionCommits(ctx context.Context, sessionID string) ([]*models.SessionCommit, error)
GetLatestSessionCommit(ctx context.Context, sessionID string) (*models.SessionCommit, error)
DeleteSessionCommit(ctx context.Context, id string) error
}
// RepositoryEntityRepository handles git repository entity CRUD and repository scripts.
// Named RepositoryEntityRepository to avoid conflation with the Repository interface itself;
// mirrors the sqlite/repository_entity.go implementation file.
type RepositoryEntityRepository interface {
CreateRepository(ctx context.Context, repository *models.Repository) error
GetRepository(ctx context.Context, id string) (*models.Repository, error)
UpdateRepository(ctx context.Context, repository *models.Repository) error
DeleteRepository(ctx context.Context, id string) error
ListRepositories(ctx context.Context, workspaceID string) ([]*models.Repository, error)
CreateRepositoryScript(ctx context.Context, script *models.RepositoryScript) error
GetRepositoryScript(ctx context.Context, id string) (*models.RepositoryScript, error)
UpdateRepositoryScript(ctx context.Context, script *models.RepositoryScript) error
DeleteRepositoryScript(ctx context.Context, id string) error
ListRepositoryScripts(ctx context.Context, repositoryID string) ([]*models.RepositoryScript, error)
ListScriptsByRepositoryIDs(ctx context.Context, repoIDs []string) (map[string][]*models.RepositoryScript, error)
GetRepositoryByProviderIdentity(ctx context.Context, identity models.ProviderRepositoryIdentity) (*models.Repository, error)
// GetRepositoryByLocalPath finds a live repository by workspace and canonical
// local_path. Returns nil, nil if not found. Used by
// Service.FindOrCreateRepositoryByLocalPath to check for an existing row by
// canonical path immediately before insert (serialized via repoResolveMu),
// instead of relying solely on a batch snapshot that can go stale across
// concurrent callers within this process. This closes the common
// single-process race; it is not a substitute for a database-level
// uniqueness constraint against writers outside this process.
GetRepositoryByLocalPath(ctx context.Context, workspaceID, localPath string) (*models.Repository, error)
}
// RepositorySetRepository stores named, reusable groups of workspace
// repositories. Membership order is authoritative: writes assign contiguous
// positions from the supplied order, and reads return items in that order with
// soft-deleted and out-of-workspace repositories excluded.
type RepositorySetRepository interface {
CreateRepositorySet(ctx context.Context, set *models.RepositorySet) error
GetRepositorySet(ctx context.Context, id string) (*models.RepositorySet, error)
// GetRepositorySetByName compares the name case-insensitively and returns
// nil, nil when it is unused, leaving the conflict decision to the caller.
GetRepositorySetByName(ctx context.Context, workspaceID, name string) (*models.RepositorySet, error)
ListRepositorySets(ctx context.Context, workspaceID string) ([]*models.RepositorySet, error)
// ListRepositorySetIDsByRepository reports which sets hold a repository, so a
// caller can publish their new shape after a deletion prunes membership.
ListRepositorySetIDsByRepository(ctx context.Context, repositoryID string) ([]string, error)
// UpdateRepositorySet writes the set's fields and, when repositoryIDs is
// non-nil, replaces its whole membership in the same transaction so the two
// cannot land apart. A nil repositoryIDs leaves membership untouched.
UpdateRepositorySet(ctx context.Context, set *models.RepositorySet, repositoryIDs *[]string) error
DeleteRepositorySet(ctx context.Context, id string) (bool, error)
}
// RepositorySecretBindingRepository stores normalized repository environment
// references. It is optional on RepositoryEntityRepository to keep legacy
// adapters source-compatible while the SQLite implementation rolls out.
type RepositorySecretBindingRepository interface {
ListRepositorySecretBindings(ctx context.Context, repositoryID string) ([]*models.RepositorySecretBinding, error)
ListRepositorySecretBindingsByRepositoryIDs(ctx context.Context, repositoryIDs []string) (map[string][]*models.RepositorySecretBinding, error)
ReplaceRepositorySecretBindings(ctx context.Context, repositoryID string, bindings []models.RepositorySecretBinding) error
}
// RepositorySecretBindingMutator adds atomic repository-plus-binding writes.
type RepositorySecretBindingMutator interface {
RepositorySecretBindingRepository
CreateRepositoryWithSecretBindings(ctx context.Context, repository *models.Repository, bindings []models.RepositorySecretBinding) error
UpdateRepositoryWithSecretBindings(ctx context.Context, repository *models.Repository, bindings []models.RepositorySecretBinding) error
}
// RepositoryCleanupRepository performs guarded deletion of repositories
// created during workspace-source attachment rollback.
type RepositoryCleanupRepository interface {
// DeleteRepositoryIfUnreferenced soft-deletes a repository only when no
// task_repositories row currently adopts it. The predicate is part of the
// mutation so rollback cleanup cannot delete a repository another task won.
DeleteRepositoryIfUnreferenced(ctx context.Context, id string) (bool, error)
}
// ExecutorRepository handles executor CRUD, executor profiles, and running state.
type ExecutorRepository interface {
CreateExecutor(ctx context.Context, executor *models.Executor) error
GetExecutor(ctx context.Context, id string) (*models.Executor, error)
UpdateExecutor(ctx context.Context, executor *models.Executor) error
DeleteExecutor(ctx context.Context, id string) error
ListExecutors(ctx context.Context) ([]*models.Executor, error)
CreateExecutorProfile(ctx context.Context, profile *models.ExecutorProfile) error
GetExecutorProfile(ctx context.Context, id string) (*models.ExecutorProfile, error)
UpdateExecutorProfile(ctx context.Context, profile *models.ExecutorProfile) error
DeleteExecutorProfile(ctx context.Context, id string) error
ListExecutorProfiles(ctx context.Context, executorID string) ([]*models.ExecutorProfile, error)
ListAllExecutorProfiles(ctx context.Context) ([]*models.ExecutorProfile, error)
ListExecutorsRunning(ctx context.Context) ([]*models.ExecutorRunning, error)
ListExecutorsRunningByTaskID(ctx context.Context, taskID string) ([]*models.ExecutorRunning, error)
UpsertExecutorRunning(ctx context.Context, running *models.ExecutorRunning) error
GetExecutorRunningBySessionID(ctx context.Context, sessionID string) (*models.ExecutorRunning, error)
DeleteExecutorRunningBySessionID(ctx context.Context, sessionID string) error
// HasExecutorRunningRow returns true if a row exists for the session.
// Used to decide "session has been launched at least once" without loading the full row.
HasExecutorRunningRow(ctx context.Context, sessionID string) (bool, error)
// UpdateResumeToken performs a CAS-style narrow update of resume_token + last_message_uuid
// scoped to the row's current agent_execution_id. If the row's agent_execution_id no longer
// matches expectedExecID (i.e. a new execution has taken over), returns models.ErrExecutionRotated
// and writes nothing. Use when persisting state from a specific execution that may have been
// replaced concurrently — typically resume tokens emitted by ACP session events.
UpdateResumeToken(ctx context.Context, sessionID, expectedExecID, resumeToken, lastMessageUUID string) error
// UpdateExecutorRunningStatus performs a narrow status update on the row.
// Used when the agent process is intentionally not being started (prepare-only
// launch) so the row doesn't sit on the misleading default "starting" forever.
// Returns models.ErrExecutorRunningNotFound if no row exists for the session.
UpdateExecutorRunningStatus(ctx context.Context, sessionID, status string) error
// RepairExecutorRunningDead repairs a row in place to reflect a dead backing
// process (status=stopped, local_pid cleared, last_seen re-stamped) while
// preserving resume_token/worktree/endpoint. Used by cleanup paths to honor
// the resume-safety invariant instead of deleting a resumable row.
// Returns models.ErrExecutorRunningNotFound if no row exists for the session.
RepairExecutorRunningDead(ctx context.Context, sessionID string) error
}
// EnvironmentRepository handles environment CRUD.
type EnvironmentRepository interface {
CreateEnvironment(ctx context.Context, environment *models.Environment) error
GetEnvironment(ctx context.Context, id string) (*models.Environment, error)
UpdateEnvironment(ctx context.Context, environment *models.Environment) error
DeleteEnvironment(ctx context.Context, id string) error
ListEnvironments(ctx context.Context) ([]*models.Environment, error)
}
// TaskEnvironmentRepository handles per-task execution environment instances
// and their per-repository child rows.
type TaskEnvironmentRepository interface {
CreateTaskEnvironment(ctx context.Context, env *models.TaskEnvironment) error
GetTaskEnvironment(ctx context.Context, id string) (*models.TaskEnvironment, error)
GetTaskEnvironmentByTaskID(ctx context.Context, taskID string) (*models.TaskEnvironment, error)
UpdateTaskEnvironment(ctx context.Context, env *models.TaskEnvironment) error
DeleteTaskEnvironment(ctx context.Context, id string) error
DeleteTaskEnvironmentsByTask(ctx context.Context, taskID string) error
CreateTaskEnvironmentRepo(ctx context.Context, repo *models.TaskEnvironmentRepo) error
ListTaskEnvironmentRepos(ctx context.Context, envID string) ([]*models.TaskEnvironmentRepo, error)
UpdateTaskEnvironmentRepo(ctx context.Context, repo *models.TaskEnvironmentRepo) error
DeleteTaskEnvironmentRepo(ctx context.Context, id string) error
DeleteTaskEnvironmentReposByEnv(ctx context.Context, envID string) error
}
// ReviewRepository handles session file review records.
type ReviewRepository interface {
UpsertSessionFileReview(ctx context.Context, review *models.SessionFileReview) error
GetSessionFileReviews(ctx context.Context, sessionID string) ([]*models.SessionFileReview, error)
DeleteSessionFileReviews(ctx context.Context, sessionID string) error
}
// DocumentRepository handles task document CRUD and revision history.
// Documents generalize plans: each document is identified by a unique key within a task.
type DocumentRepository interface {
CreateDocument(ctx context.Context, doc *models.TaskDocument) error
GetDocument(ctx context.Context, taskID, key string) (*models.TaskDocument, error)
UpdateDocument(ctx context.Context, doc *models.TaskDocument) error
DeleteDocument(ctx context.Context, taskID, key string) error
ListDocuments(ctx context.Context, taskID string) ([]*models.TaskDocument, error)
// Revision history
InsertDocumentRevision(ctx context.Context, rev *models.TaskDocumentRevision) error
GetLatestDocumentRevision(ctx context.Context, taskID, key string) (*models.TaskDocumentRevision, error)
ListDocumentRevisions(ctx context.Context, taskID, key string, limit int) ([]*models.TaskDocumentRevision, error)
GetDocumentRevision(ctx context.Context, id string) (*models.TaskDocumentRevision, error)
NextDocumentRevisionNumber(ctx context.Context, taskID, key string) (int, error)
// WriteDocumentRevision atomically upserts the HEAD document and writes/merges a revision
// in a single transaction. Pass a non-nil coalesceLatestID to merge into an existing revision;
// otherwise a new revision is appended with revision_number computed inside the tx.
WriteDocumentRevision(ctx context.Context, head *models.TaskDocument, rev *models.TaskDocumentRevision, coalesceLatestID *string) error
}
// PlanRepository handles task plan CRUD and its revision history.
type PlanRepository interface {
CreateTaskPlan(ctx context.Context, plan *models.TaskPlan) error
GetTaskPlan(ctx context.Context, taskID string) (*models.TaskPlan, error)
UpdateTaskPlan(ctx context.Context, plan *models.TaskPlan) error
MarkTaskPlanImplementationStarted(ctx context.Context, taskID, sessionID, actor string) (*models.TaskPlan, error)
DeleteTaskPlan(ctx context.Context, taskID string) error
// Revision history
InsertTaskPlanRevision(ctx context.Context, rev *models.TaskPlanRevision) error
UpdateTaskPlanRevision(ctx context.Context, rev *models.TaskPlanRevision) error
GetTaskPlanRevision(ctx context.Context, id string) (*models.TaskPlanRevision, error)
GetLatestTaskPlanRevision(ctx context.Context, taskID string) (*models.TaskPlanRevision, error)
ListTaskPlanRevisions(ctx context.Context, taskID string, limit int) ([]*models.TaskPlanRevision, error)
NextTaskPlanRevisionNumber(ctx context.Context, taskID string) (int, error)
// WritePlanRevision atomically upserts the HEAD plan and writes/merges a revision in a
// single transaction. Pass a non-nil coalesceLatestID to merge into an existing revision;
// otherwise a new revision is appended with revision_number computed inside the tx.
WritePlanRevision(ctx context.Context, head *models.TaskPlan, rev *models.TaskPlanRevision, coalesceLatestID *string) error
}
// SubagentContextRepository persists the durable, queryable record of a
// subagent (Task tool) invocation. See
// docs/specs/subagent-context-persistence/spec.md.
type SubagentContextRepository interface {
// UpsertSubagentContext inserts or merges one subagent invocation row,
// keyed on (task_session_id, tool_call_id). A single atomic statement —
// no read-then-write.
UpsertSubagentContext(ctx context.Context, sc *models.SubagentContext) error
ListSubagentContextsBySession(ctx context.Context, sessionID string) ([]*models.SubagentContext, error)
ListSubagentContextsByTurn(ctx context.Context, turnID string) ([]*models.SubagentContext, error)
}