Files
kandev/apps/backend/internal/task/repository/interface.go
T
2026-07-10 12:56:05 +01:00

337 lines
23 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"
v1 "github.com/kandev/kandev/pkg/api/v1"
)
var ErrWorkspaceNameMismatch = repoerrors.ErrWorkspaceNameMismatch
var ErrWorkspaceNotFound = repoerrors.ErrWorkspaceNotFound
var ErrTaskNotFound = repoerrors.ErrTaskNotFound
var ErrTaskPlanNotFound = repoerrors.ErrTaskPlanNotFound
// 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)
ListTasksForAutoArchive(ctx context.Context) ([]*models.Task, 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)
UpdateTaskState(ctx context.Context, id string, state v1.TaskState) error
// UpdateTaskStateIfCurrentIn atomically transitions state only when the
// task's current state is in allowed. 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)
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)
}
// 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)
}
// 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)
FindPendingClarificationMessagesBySessionID(ctx context.Context, sessionID string) ([]*models.Message, 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)
SearchMessages(ctx context.Context, sessionID string, opts models.SearchMessagesOptions) ([]*models.Message, error)
DeleteMessage(ctx context.Context, id string) error
}
// TurnRepository handles conversation turn persistence.
type TurnRepository interface {
CreateTurn(ctx context.Context, turn *models.Turn) 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
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
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) (int64, 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
DismissLastAgentError(ctx context.Context, sessionID string, expected models.LastAgentError, dismissedAt time.Time) (bool, error)
GetLastAgentMessage(ctx context.Context, sessionID string) (string, error)
}
// SessionWorktreeRepository handles the task session↔worktree association.
type SessionWorktreeRepository interface {
CreateTaskSessionWorktree(ctx context.Context, sessionWorktree *models.TaskSessionWorktree) error
UpdateTaskSessionWorktreeBranch(ctx context.Context, sessionID, branch string) error
UpdateTaskSessionWorktreeBranchByRepository(ctx context.Context, sessionID, repositoryID, branch string) error
ListTaskSessionWorktrees(ctx context.Context, sessionID string) ([]*models.TaskSessionWorktree, error)
ListWorktreesBySessionIDs(ctx context.Context, sessionIDs []string) (map[string][]*models.TaskSessionWorktree, error)
DeleteTaskSessionWorktree(ctx context.Context, id string) error
DeleteTaskSessionWorktreesBySession(ctx context.Context, sessionID string) 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)
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) 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)
GetRepositoryByProviderInfo(ctx context.Context, workspaceID, provider, owner, name string) (*models.Repository, 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
}