9b82595cfd
Users sometimes need a clean slate — broken packages, stale caches, a container that drifted into a bad state. Reset Environment is the explicit escape hatch: tear down the recorded container, sandbox, and/or worktree for a task and delete the TaskEnvironment row so the next session starts fresh. - service.ResetTaskEnvironment(taskID, ResetOptions) is the entry point. ResetOptions.PushBranch optionally git-pushes the workspace branch before teardown so committed work isn't dropped on the floor; a failed push aborts the reset and leaves the environment intact. - A new EnvironmentDestroyer interface in the task service hides the per-resource teardown behind a single seam, implemented in cmd/kandev by delegating to lifecycle.Manager (containers, sprites) and worktree.Manager (worktrees, branch preserved). - lifecycle.Manager.DestroyContainer / DestroySandbox dispatch to the Docker and Sprites executor backends without leaking model types into the lifecycle package. - Reset is blocked while any session on the task still has an ExecutorRunning row (returns ErrSessionRunning → HTTP 409). Per-resource teardown is best-effort but the DB row is only removed once everything is confirmed gone or already missing, so a partial failure is retryable by clicking Reset again. - Endpoint: POST /api/v1/tasks/:id/environment/reset.
201 lines
7.6 KiB
Go
201 lines
7.6 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/kandev/kandev/internal/common/logger"
|
|
"github.com/kandev/kandev/internal/events/bus"
|
|
"github.com/kandev/kandev/internal/task/repository"
|
|
wfmodels "github.com/kandev/kandev/internal/workflow/models"
|
|
"github.com/kandev/kandev/internal/worktree"
|
|
)
|
|
|
|
// WorktreeCleanup provides worktree cleanup on task deletion.
|
|
type WorktreeCleanup interface {
|
|
// OnTaskDeleted is called when a task is deleted to clean up its worktree.
|
|
OnTaskDeleted(ctx context.Context, taskID string) error
|
|
}
|
|
|
|
// WorktreeProvider extends WorktreeCleanup with query capabilities.
|
|
// Implementations that support this can be type-asserted from WorktreeCleanup.
|
|
type WorktreeProvider interface {
|
|
WorktreeCleanup
|
|
// GetAllByTaskID returns all worktrees associated with a task.
|
|
GetAllByTaskID(ctx context.Context, taskID string) ([]*worktree.Worktree, error)
|
|
}
|
|
|
|
// WorktreeBatchCleaner extends WorktreeProvider with batch cleanup.
|
|
type WorktreeBatchCleaner interface {
|
|
WorktreeProvider
|
|
// CleanupWorktrees removes multiple worktrees in a single operation.
|
|
CleanupWorktrees(ctx context.Context, worktrees []*worktree.Worktree) error
|
|
}
|
|
|
|
// TaskExecutionStopper stops active task execution (agent session + instance).
|
|
type TaskExecutionStopper interface {
|
|
StopTask(ctx context.Context, taskID, reason string, force bool) error
|
|
StopSession(ctx context.Context, sessionID, reason string, force bool) error
|
|
StopExecution(ctx context.Context, executionID, reason string, force bool) error
|
|
}
|
|
|
|
// GitArchiveCapture captures git state (commits, cumulative diff) when a task is archived.
|
|
// This allows preserving the final git state of a session for historical purposes.
|
|
type GitArchiveCapture interface {
|
|
// CaptureArchiveSnapshot captures the git state for a session before archiving.
|
|
// Returns nil if capture is not possible (e.g., agent not running).
|
|
CaptureArchiveSnapshot(ctx context.Context, sessionID string) error
|
|
}
|
|
|
|
// WorkflowStepCreator creates workflow steps from a template for a workflow.
|
|
type WorkflowStepCreator interface {
|
|
CreateStepsFromTemplate(ctx context.Context, workflowID, templateID string) error
|
|
}
|
|
|
|
// WorkflowStepGetter retrieves workflow step information.
|
|
type WorkflowStepGetter interface {
|
|
GetStep(ctx context.Context, stepID string) (*wfmodels.WorkflowStep, error)
|
|
// GetNextStepByPosition returns the next step after the given position for a workflow.
|
|
// Returns nil if there is no next step (i.e., current step is the last one).
|
|
GetNextStepByPosition(ctx context.Context, workflowID string, currentPosition int) (*wfmodels.WorkflowStep, error)
|
|
}
|
|
|
|
// StartStepResolver resolves the starting step for a workflow.
|
|
type StartStepResolver interface {
|
|
ResolveStartStep(ctx context.Context, workflowID string) (string, error)
|
|
ResolveFirstStep(ctx context.Context, workflowID string) (string, error)
|
|
}
|
|
|
|
var (
|
|
ErrActiveTaskSessions = errors.New("active agent sessions exist")
|
|
ErrInvalidRepositorySettings = errors.New("invalid repository settings")
|
|
ErrInvalidExecutorConfig = errors.New("invalid executor config")
|
|
)
|
|
|
|
func validateExecutorConfig(config map[string]string) error {
|
|
if config == nil {
|
|
return nil
|
|
}
|
|
policy := strings.TrimSpace(config["mcp_policy"])
|
|
if policy == "" {
|
|
return nil
|
|
}
|
|
var decoded any
|
|
if err := json.Unmarshal([]byte(policy), &decoded); err != nil {
|
|
return fmt.Errorf("%w: mcp_policy must be valid JSON", ErrInvalidExecutorConfig)
|
|
}
|
|
if _, ok := decoded.(map[string]any); !ok {
|
|
return fmt.Errorf("%w: mcp_policy must be a JSON object", ErrInvalidExecutorConfig)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Repos holds the repository sub-interfaces used by the task service.
|
|
type Repos struct {
|
|
Workspaces repository.WorkspaceRepository
|
|
Tasks repository.TaskRepository
|
|
TaskRepos repository.TaskRepoRepository
|
|
Workflows repository.WorkflowRepository
|
|
Messages repository.MessageRepository
|
|
Turns repository.TurnRepository
|
|
Sessions repository.SessionRepository
|
|
GitSnapshots repository.GitSnapshotRepository
|
|
RepoEntities repository.RepositoryEntityRepository
|
|
Executors repository.ExecutorRepository
|
|
Environments repository.EnvironmentRepository
|
|
TaskEnvironments repository.TaskEnvironmentRepository
|
|
Reviews repository.ReviewRepository
|
|
}
|
|
|
|
// Service provides task business logic
|
|
type Service struct {
|
|
workspaces repository.WorkspaceRepository
|
|
tasks repository.TaskRepository
|
|
taskRepos repository.TaskRepoRepository
|
|
workflows repository.WorkflowRepository
|
|
messages repository.MessageRepository
|
|
turns repository.TurnRepository
|
|
sessions repository.SessionRepository
|
|
gitSnapshots repository.GitSnapshotRepository
|
|
repoEntities repository.RepositoryEntityRepository
|
|
executors repository.ExecutorRepository
|
|
environments repository.EnvironmentRepository
|
|
taskEnvironments repository.TaskEnvironmentRepository
|
|
reviews repository.ReviewRepository
|
|
eventBus bus.EventBus
|
|
logger *logger.Logger
|
|
discoveryConfig RepositoryDiscoveryConfig
|
|
worktreeCleanup WorktreeCleanup
|
|
executionStopper TaskExecutionStopper
|
|
gitArchiveCapture GitArchiveCapture
|
|
workflowStepCreator WorkflowStepCreator
|
|
workflowStepGetter WorkflowStepGetter
|
|
startStepResolver StartStepResolver
|
|
quickChatDir string // Directory for quick-chat workspaces (e.g., ~/.kandev/quick-chat)
|
|
branchFetcher *branchFetcher
|
|
envDestroyer EnvironmentDestroyer
|
|
sessionRunningChecker SessionRunningChecker
|
|
}
|
|
|
|
// NewService creates a new task service
|
|
func NewService(repos Repos, eventBus bus.EventBus, log *logger.Logger, discoveryConfig RepositoryDiscoveryConfig) *Service {
|
|
return &Service{
|
|
workspaces: repos.Workspaces,
|
|
tasks: repos.Tasks,
|
|
taskRepos: repos.TaskRepos,
|
|
workflows: repos.Workflows,
|
|
messages: repos.Messages,
|
|
turns: repos.Turns,
|
|
sessions: repos.Sessions,
|
|
gitSnapshots: repos.GitSnapshots,
|
|
repoEntities: repos.RepoEntities,
|
|
executors: repos.Executors,
|
|
environments: repos.Environments,
|
|
taskEnvironments: repos.TaskEnvironments,
|
|
reviews: repos.Reviews,
|
|
eventBus: eventBus,
|
|
logger: log,
|
|
discoveryConfig: discoveryConfig,
|
|
branchFetcher: newBranchFetcher(log.Zap()),
|
|
}
|
|
}
|
|
|
|
// SetWorktreeCleanup sets the worktree cleanup handler for task deletion.
|
|
func (s *Service) SetWorktreeCleanup(cleanup WorktreeCleanup) {
|
|
s.worktreeCleanup = cleanup
|
|
}
|
|
|
|
// SetExecutionStopper wires the task execution stopper (orchestrator).
|
|
func (s *Service) SetExecutionStopper(stopper TaskExecutionStopper) {
|
|
s.executionStopper = stopper
|
|
}
|
|
|
|
// SetGitArchiveCapture wires the git archive capture handler.
|
|
func (s *Service) SetGitArchiveCapture(capture GitArchiveCapture) {
|
|
s.gitArchiveCapture = capture
|
|
}
|
|
|
|
// SetWorkflowStepCreator wires the workflow step creator for workflow creation.
|
|
func (s *Service) SetWorkflowStepCreator(creator WorkflowStepCreator) {
|
|
s.workflowStepCreator = creator
|
|
}
|
|
|
|
// SetWorkflowStepGetter wires the workflow step getter for MoveTask.
|
|
func (s *Service) SetWorkflowStepGetter(getter WorkflowStepGetter) {
|
|
s.workflowStepGetter = getter
|
|
}
|
|
|
|
// SetStartStepResolver wires the start step resolver for CreateTask.
|
|
func (s *Service) SetStartStepResolver(resolver StartStepResolver) {
|
|
s.startStepResolver = resolver
|
|
}
|
|
|
|
// SetQuickChatDir sets the directory for quick-chat workspaces.
|
|
// When set, ephemeral task cleanup will delete the session directory under this path.
|
|
func (s *Service) SetQuickChatDir(dir string) {
|
|
s.quickChatDir = dir
|
|
}
|