a627957c5b
* refactor(task/repo): split sqlite/base.go (1486 lines) into schema/migrations/queries base.go grew to 1.85x the 800-line lint threshold by mixing the Repository struct, schema initialization, ALTER TABLE migrations, recreate-and-copy table rebuilds, and historical backfills/heals. Split along those seams: - base.go (58 lines) keeps only the Repository struct, NewWithDB, newRepository, Close, DB. - base_schema.go owns the initSchema orchestrator, every init*Schema function, ensure*Indexes, ensureRunnerProjectionTables, and the plan-revision backfill that runs as part of initPlansSchema. - base_migrations.go owns migrate*, recreateTable[Named], runMigrations, the backfillRow type, and every backfill*/heal* function. Verbatim function moves with no semantic changes. - base_queries.go owns sqlLimitClause (still referenced by plan.go, document.go, message.go). To keep golangci-lint (new: true) green after the move: - initSchema's 17 if-return chain becomes a for-each-step loop so cyclomatic complexity drops from 19 to within the 15 limit. - initInfraSchema and initSessionWorktreeSchema hoist their DDL strings into package-level consts (infraSchemaDDL, sessionWorktreeSchemaDDL) so each function body fits the 80-line funlen cap. Step ordering in initSchema is preserved exactly (backfillTaskEnvironments -> backfillTaskEnvironmentRepos -> healTaskEnvironmentWorkspacePaths -> healDuplicateTaskEnvironments -> ensureTaskEnvironmentTaskUniqueIndex -> healSessionTaskEnvironmentIDs) so existing prod DBs continue to migrate correctly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(task/repo): add BatchGetSessionsByTaskIDs to eliminate task-list N+1 Adds a single repository method that returns every task_session for a list of task IDs in one chunked IN (...) query, grouped by task_id. The workspace task-list endpoint previously needed three batched calls (GetPrimarySessionIDsByTaskIDs + GetSessionCountsByTaskIDs + GetPrimarySessionInfoByTaskIDs); with this loader the handler can derive primary session ID, session count, and per-task session info from a single round trip. - buildInPlaceholders and chunkIDs land in base_queries.go. sqliteMaxHostParams=500 keeps each chunk well below SQLite's compile-time SQLITE_MAX_VARIABLE_NUMBER (999 on older builds, 32766 on newer). - session.go gains BatchGetSessionsByTaskIDs, which reuses taskSessionSelectCols / scanTaskSessions / loadWorktreesBatch so the returned sessions are indistinguishable from ListTaskSessions output (worktrees populated, JSON snapshots decoded). Empty input short-circuits with a non-nil empty map. - SessionRepository interface and Service.BatchGetSessionsForTasks expose the new method to the handler layer. The three existing *ByTaskIDs methods are left in place because service_events.go still uses them for single-task event publishing. - mockRepository implementations updated in two test files so the packages still satisfy the interface. TestBatchGetSessionsByTaskIDs covers the empty-input short-circuit, the multi-task (3 x 2) case (primary derivation + no cross-task bleed), and the asked-for-but-missing case (absent from result map). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * perf(task/handlers): use BatchGetSessionsForTasks in task-list endpoint buildTaskDTOsWithSessionInfo previously issued three batched queries per request (GetPrimarySessionIDsForTasks + GetSessionCountsForTasks + GetPrimarySessionInfoForTasks). Each was already chunked by task_id, but together they were three round trips for data that can be derived from one. Collapse to a single BatchGetSessionsForTasks call and derive primary session ID, count, and per-task primary info in memory. The JSON response shape is unchanged: dto.FromTaskWithSessionInfo still receives the same pointer-typed arguments, primarySessionID stays nil when no primary exists, and sessionCount stays nil for tasks with zero sessions (matches the existing pointer-when-present contract that the frontend store hydration depends on). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(task/handlers): restore JOIN-derived executor type/name in task-list response buildTaskDTOsWithSessionInfo regressed when it stopped calling GetPrimarySessionInfoForTasks: extractSessionInfo reads ExecutorSnapshot["executor_type"] and ExecutorSnapshot["executor_name"], but those keys exist only on the synthetic snapshot built by the GetPrimarySessionInfoByTaskIDs LEFT JOIN to executors. The persisted ExecutorSnapshot JSON uses different keys (e.g. "type" — see share/builder.go:96), so BatchGetSessionsForTasks alone always yielded empty executor_type / executor_name fields in the task-list response, breaking the frontend task card's executor badge. Fix (caught in code review): - Handler now calls both BatchGetSessionsForTasks (for primary session ID and session count) and GetPrimarySessionInfoForTasks (for the JOIN-derived executor type/name and other session info). Two queries instead of the original three; correctness preserved. - New TestGetPrimarySessionInfoByTaskIDs_PopulatesExecutorJoinFields seeds an executor + primary session, then asserts the JOIN populates ExecutorSnapshot["executor_type"] / ["executor_name"] on the returned session. Locks in the contract the handler depends on so a future refactor that drops the JOIN fails loudly instead of silently emitting empty fields. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(task/repo): chunkIDs returns empty slice for empty input chunkIDs previously returned [][]string{nil} for an empty/nil input (a single chunk whose value is nil). The only current caller (BatchGetSessionsByTaskIDs) short-circuits empty input before reaching chunkIDs, so the bug stayed dormant — but any new caller that forgot the short-circuit would feed a nil chunk to buildInPlaceholders (which returns "" for nil), produce a `... WHERE task_id IN () ORDER BY ...` query, and hit a SQL syntax error at runtime. Make the helper safe by construction: an empty input returns nil (empty outer slice), so `range chunkIDs(empty, n)` iterates zero times. Add base_queries_test.go covering: - Empty/nil input → no chunks (locks the safety contract). - Single-pass input fits in one chunk. - Over-threshold input splits into correctly-sized, in-order chunks. - buildInPlaceholders normal + empty-input cases. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(task/repo): propagate non-ErrNoRows errors in backfillSingleTask executor lookup `backfillSingleTask` looked up `executors.type` for the legacy executor referenced by a session and defaulted to `"local_pc"` on any error. That silently masked driver failures, schema mismatches, and corruption — every backfilled task_environment would get the wrong executor_type without surfacing the underlying problem. Narrow the default to the legitimate case: only `sql.ErrNoRows` (the executor row was actually deleted, e.g. legacy session whose executor is gone) falls back to `"local_pc"`. Any other scan error now aborts the migration with a wrapped error so operators see the real cause. Adds TestBackfillSingleTask_DefaultsExecutorTypeOnMissingRow which seeds a session referencing a non-existent executor id and asserts the backfill still completes with `executor_type = local_pc` — locks in the legitimate-fallback path. Addresses PR #1058 review feedback from cubic & claude. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(task/repo): chunk ListWorktreesBySessionIDs to honor SQLite var limit ListWorktreesBySessionIDs built its IN-clause from every input session ID in one shot. BatchGetSessionsByTaskIDs chunks task IDs at 500 to stay below SQLITE_MAX_VARIABLE_NUMBER (999 on older builds), but its loadWorktreesBatch step then handed all sessions in that 500-task chunk to ListWorktreesBySessionIDs un-chunked — so at modest workloads (500 tasks × ≥2 sessions each = 1000 placeholders) the query fails with SQLITE_RANGE and bubbles up as a 404 on the task-list endpoint. ListWorktreesBySessionIDs now applies the same chunkIDs / 500 cap that BatchGet uses. Body extracted to appendWorktreesForSessionChunk so the public method stays under the 80-line funlen cap. New TestListWorktreesBySessionIDs_ChunksOverPlaceholderLimit feeds the method 1001 non-existent session IDs and asserts it returns cleanly — without chunking, this fails with SQLITE_RANGE on a default-built SQLite. Also splits TestBatchGetSessionsByTaskIDs (was 98 lines) into focused subtests + a shared seedBatchSessions helper, satisfying the test-size convention. The three sub-scenarios (empty input, multi-task grouping, missing-task absence) now fail in isolation. Addresses PR #1058 review feedback from greptile (P1) and claude (suggestion). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(task/repo): port repositories.copy_files migration to split files Rebased onto main; main added copy_files to runMigrations() and initTaskSchema() in base.go while our branch had split those into base_migrations.go and base_schema.go. Cherry-pick both additions into the correct split files so the schema is consistent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
299 lines
20 KiB
Go
299 lines
20 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
|
|
agentdto "github.com/kandev/kandev/internal/agent/dto"
|
|
"github.com/kandev/kandev/internal/task/models"
|
|
v1 "github.com/kandev/kandev/pkg/api/v1"
|
|
)
|
|
|
|
// 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
|
|
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)
|
|
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, 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)
|
|
UpdateTaskState(ctx context.Context, id string, state v1.TaskState) 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)
|
|
UpdateMessage(ctx context.Context, message *models.Message) error
|
|
ListMessages(ctx context.Context, sessionID 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
|
|
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)
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
}
|
|
|
|
// 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
|
|
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
|
|
}
|