package models import ( "errors" "maps" "time" "github.com/kandev/kandev/internal/agentruntime" "github.com/kandev/kandev/internal/sysprompt" v1 "github.com/kandev/kandev/pkg/api/v1" ) // ErrExecutorRunningNotFound is returned when no executor running record exists for a session. var ErrExecutorRunningNotFound = errors.New("executor running not found") // ErrExecutorNotFound is returned by the executor repository when no // executor row exists for the given ID. Callers should use errors.Is to // distinguish "row doesn't exist" (404 semantically) from transport-level // failures (storage outage, timeout, ctx cancel) that happen to also // surface from the same lookup. var ErrExecutorNotFound = errors.New("executor not found") // ErrExecutionRotated is returned by CAS-style updates on executors_running when the // row's agent_execution_id has rotated since the caller observed it. Indicates the // caller's write target a now-defunct execution and should be discarded — typically // a stale resume-token event from an execution that was replaced (model switch, // context reset, fresh re-launch). Callers should not retry; the new execution // will produce its own events. var ErrExecutionRotated = errors.New("execution rotated; CAS write rejected") // Status values for executors_running.status. The lifecycle manager defaults // rows to "starting" on creation (see runtime/lifecycle/persistence.go); the // orchestrator flips a row to "prepared" when a prepare-only launch finishes // with the agent process intentionally not started. const ( ExecutorRunningStatusStarting = "starting" ExecutorRunningStatusPrepared = "prepared" ) // ListMessagesOptions defines pagination options for listing messages type ListMessagesOptions struct { Limit int Before string After string Sort string } // SearchMessagesOptions defines options for searching a session's messages. type SearchMessagesOptions struct { Query string Limit int } // Task metadata keys used for deferred agent start (e.g., task.moved → handleTaskMovedNoSession). const ( MetaKeyAgentProfileID = "agent_profile_id" MetaKeyExecutorProfileID = "executor_profile_id" // MetaKeyWorkspacePath is the optional host folder for repo-less tasks // (set by CreateTask, read by the orchestrator when building a session). // Centralised here so the set/read sites can't drift apart. MetaKeyWorkspacePath = "workspace_path" ) // TaskSession.Metadata key that records how the session came into existence. // Read by maybySwitchSessionForProfile to decide whether transitioning to a // step with no agent_profile override should revert to the task default: // workflow-spawned sessions (created_by=workflow_switch) -> yes, revert. // user-created sessions (no created_by tag) -> no, preserve. const ( SessionMetaKeyCreatedBy = "created_by" SessionCreatedByWorkflowSwitch = "workflow_switch" ) // SessionMetaKeySessionMode records the agent's last-known session permission // mode (auto / default / accept-edits, etc.) so it survives a backend restart or // SSR reload, alongside the in-memory re-apply on context reset. See issue #1183. const SessionMetaKeySessionMode = "session_mode" // Task origin values for the Origin field. const ( TaskOriginManual = "manual" TaskOriginAgentCreated = "agent_created" TaskOriginRoutine = "routine" TaskOriginOnboarding = "onboarding" TaskOriginAutomationRun = "automation_run" ) // Task represents a task in the database type Task struct { ID string `json:"id"` WorkspaceID string `json:"workspace_id"` WorkflowID string `json:"workflow_id"` WorkflowStepID string `json:"workflow_step_id"` Title string `json:"title"` Description string `json:"description"` State v1.TaskState `json:"state"` Priority string `json:"priority"` Position int `json:"position"` // Order within workflow step Metadata map[string]interface{} `json:"metadata,omitempty"` Repositories []*TaskRepository `json:"repositories,omitempty"` IsEphemeral bool `json:"is_ephemeral"` // Ephemeral tasks are not shown in kanban, used for quick chat ParentID string `json:"parent_id,omitempty"` // FK to parent task for subtasks ArchivedAt *time.Time `json:"archived_at,omitempty"` // ArchivedByCascadeID is set when the task was archived as part of an // office task-handoffs cascade (phase 6). UnarchiveTaskByCascade uses // it to scope restoration to exactly the descendants this cascade // owned, leaving manually-archived tasks alone. Empty for // manually-archived tasks and any task archived before phase 6. ArchivedByCascadeID string `json:"archived_by_cascade_id,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` // Office extensions. // // ADR 0005 Wave F: the task's assignee is no longer a column on the // tasks table. The canonical store is a `runner` row in // workflow_step_participants (resolve via // workflow.Repository.ResolveCurrentRunner). The struct field below // is a read-time projection populated by repo SELECT queries through // a correlated subquery and consumed unchanged by callers that // previously read tasks.assignee_agent_profile_id. Repo writes to // this field are routed to SetTaskRunner / ClearTaskRunner inside // the task repository. AssigneeAgentProfileID string `json:"assignee_agent_profile_id,omitempty"` Origin string `json:"origin,omitempty"` // manual, agent_created, routine ProjectID string `json:"project_id,omitempty"` // FK to office project Labels string `json:"labels,omitempty"` // JSON array string, default "[]" Identifier string `json:"identifier,omitempty"` // e.g. "KAN-42" // IsFromOffice is a read-time projection set by the task repo's SELECT // (see isFromOfficeProjection in repository/sqlite/task.go). True when // the task is owned by office: either it has a non-empty ProjectID, or // its workflow matches the workspace's office_workflow_id. Kanban-board // tasks always come back false. UI callers gate office-only surfaces on // this (e.g. the "Open in office view" topbar link). IsFromOffice bool `json:"is_from_office,omitempty"` } // TaskTreeFilters provides filter options for the task tree query. type TaskTreeFilters struct { ProjectID string AssigneeID string WorkflowID string Origin string } // WorkflowStyle values are persisted in workflows.style. See ADR-0004. // They're a UX hint for the frontend; backend code MUST NOT branch on them. const ( WorkflowStyleKanban = "kanban" WorkflowStyleOffice = "office" WorkflowStyleCustom = "custom" ) // Workflow represents a task workflow type Workflow struct { ID string `json:"id"` WorkspaceID string `json:"workspace_id"` Name string `json:"name"` Description string `json:"description"` AgentProfileID string `json:"agent_profile_id,omitempty"` WorkflowTemplateID *string `json:"workflow_template_id,omitempty"` SortOrder int `json:"sort_order"` // Hidden workflows are excluded from management and picker UIs by default. // Used by system-only flows like Improve Kandev. Hidden bool `json:"hidden,omitempty"` // Style is a Phase 2 (ADR-0004) UX hint read by the frontend ONLY. // Allowed values: "kanban" | "office" | "custom". Empty / unknown // values fall back to "kanban" via the schema default. Backend code // MUST NOT branch on this field — it is a presentation hint, not a // behavioural invariant. Style string `json:"style,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // Workspace represents a workspace type Workspace struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` OwnerID string `json:"owner_id"` DefaultExecutorID *string `json:"default_executor_id,omitempty"` DefaultEnvironmentID *string `json:"default_environment_id,omitempty"` DefaultAgentProfileID *string `json:"default_agent_profile_id,omitempty"` DefaultConfigAgentProfileID *string `json:"default_config_agent_profile_id,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` // Office extensions TaskPrefix string `json:"task_prefix,omitempty"` // e.g. "KAN" TaskSequence int `json:"task_sequence,omitempty"` // auto-incrementing counter OfficeWorkflowID string `json:"office_workflow_id,omitempty"` // FK to system office workflow } // TaskRepository represents a repository associated with a task type TaskRepository struct { ID string `json:"id"` TaskID string `json:"task_id"` RepositoryID string `json:"repository_id"` BaseBranch string `json:"base_branch"` CheckoutBranch string `json:"checkout_branch,omitempty"` Position int `json:"position"` Metadata map[string]interface{} `json:"metadata,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // MessageAuthorType represents who authored a message type MessageAuthorType string const ( // MessageAuthorUser indicates a message from a human user MessageAuthorUser MessageAuthorType = "user" // MessageAuthorAgent indicates a message from an AI agent MessageAuthorAgent MessageAuthorType = "agent" ) // MessageType represents the type of message content type MessageType string const ( // MessageTypeMessage is the default type for user/agent regular messages MessageTypeMessage MessageType = "message" // MessageTypeContent is for agent response content MessageTypeContent MessageType = "content" // MessageTypeToolCall is when agent uses a tool MessageTypeToolCall MessageType = "tool_call" // MessageTypeToolEdit is for file edit operations with diff visualization MessageTypeToolEdit MessageType = "tool_edit" // MessageTypeToolRead is for file read operations MessageTypeToolRead MessageType = "tool_read" // MessageTypeToolExecute is for command execution operations MessageTypeToolExecute MessageType = "tool_execute" // MessageTypeProgress is for progress updates MessageTypeProgress MessageType = "progress" // MessageTypeLog is for agent log messages (info, debug, warning, etc.) MessageTypeLog MessageType = "log" // MessageTypeError is for error messages MessageTypeError MessageType = "error" // MessageTypeStatus is for status changes: started, completed, failed MessageTypeStatus MessageType = "status" // MessageTypePermissionRequest is for agent permission requests MessageTypePermissionRequest MessageType = "permission_request" // MessageTypeClarificationRequest is for agent clarification questions MessageTypeClarificationRequest MessageType = "clarification_request" // MessageTypeScriptExecution is for setup/cleanup script execution messages MessageTypeScriptExecution MessageType = "script_execution" // MessageTypeThinking is for agent thinking/reasoning content MessageTypeThinking MessageType = "thinking" // MessageTypeAgentPlan is for agent native plan content (e.g. ExitPlanMode) MessageTypeAgentPlan MessageType = "agent_plan" // MessageTypeTodo is for agent todo/task list updates MessageTypeTodo MessageType = "todo" ) // PermissionStatus is the resolution status of a permission request message, // stored under metadata.status. Pending is the absence sentinel: a freshly // created permission_request message does not set metadata.status, and any // reader treats that as PermissionStatusPending. type PermissionStatus string const ( // PermissionStatusPending is the empty-string sentinel for the unresolved // state. CreatePermissionRequestMessage does not write metadata.status at // all; readers compare against this constant instead of the bare "". PermissionStatusPending PermissionStatus = "" // PermissionStatusApproved means the user (or automation) accepted the request. PermissionStatusApproved PermissionStatus = "approved" // PermissionStatusRejected means the user did not accept the request. // Collapses two distinct ACP outcomes: explicit Deny (selected option with // kind reject_once/reject_always) AND dialog dismissal (ACP cancelled // outcome on the wire, triggered today only by the fallback path when the // prompt offers no reject_* option). Split into separate Rejected / // Dismissed statuses if the audit trail ever needs to distinguish them. PermissionStatusRejected PermissionStatus = "rejected" // PermissionStatusExpired means the request became unanswerable: the agent // withdrew the prompt before a response arrived (permission_cancelled // event), or agentctl rejected the response because the pending entry was // already gone. No ACP outcome ever reaches the wire in this state. PermissionStatusExpired PermissionStatus = "expired" ) // Message represents a message in a task session type Message struct { ID string `json:"id"` TaskSessionID string `json:"session_id"` TaskID string `json:"task_id,omitempty"` TurnID string `json:"turn_id"` // FK to task_session_turns AuthorType MessageAuthorType `json:"author_type"` AuthorID string `json:"author_id,omitempty"` // User ID or Agent Execution ID Content string `json:"content"` Type MessageType `json:"type,omitempty"` // Defaults to "message" Metadata map[string]interface{} `json:"metadata,omitempty"` RequestsInput bool `json:"requests_input"` // True if agent is requesting user input CreatedAt time.Time `json:"created_at"` } // ToAPI converts internal Message to API type. // Only true system-injected content (wrapped in tags) is stripped // from the visible content sent to the UI. func (m *Message) ToAPI() *v1.Message { messageType := string(m.Type) if messageType == "" { messageType = string(MessageTypeMessage) } hasHidden := sysprompt.HasSystemContent(m.Content) meta := m.Metadata if hasHidden { if meta == nil { meta = make(map[string]interface{}) } else { // Copy to avoid mutating original meta = copyMetadata(meta) } meta["has_hidden_prompts"] = true } result := &v1.Message{ ID: m.ID, TaskSessionID: m.TaskSessionID, TaskID: m.TaskID, TurnID: m.TurnID, AuthorType: string(m.AuthorType), AuthorID: m.AuthorID, Content: sysprompt.StripSystemContent(m.Content), Type: messageType, Metadata: meta, RequestsInput: m.RequestsInput, CreatedAt: m.CreatedAt, } if hasHidden { result.RawContent = m.Content } return result } // copyMetadata returns a shallow copy of a metadata map. func copyMetadata(m map[string]any) map[string]any { cp := make(map[string]any, len(m)) maps.Copy(cp, m) return cp } // Turn represents a single prompt/response cycle within a task session. // A turn starts when a user sends a prompt and ends when the agent completes, // cancels, or errors. type Turn struct { ID string `json:"id"` TaskSessionID string `json:"session_id"` TaskID string `json:"task_id"` StartedAt time.Time `json:"started_at"` CompletedAt *time.Time `json:"completed_at,omitempty"` Metadata map[string]interface{} `json:"metadata,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // ReviewStatus represents the review state of a TaskSession. The zero value // (ReviewStatusNone, the empty string) means "no review needed" and serializes // out via omitempty, preserving the JSON shape of the legacy *string field. // The persisted column (task_sessions.review_status TEXT DEFAULT ”) is // compatible with these values without any migration. type ReviewStatus string const ( // ReviewStatusNone is the zero value meaning no review is needed. ReviewStatusNone ReviewStatus = "" // ReviewStatusPending indicates the session is waiting on a human review. ReviewStatusPending ReviewStatus = "pending" // ReviewStatusApproved indicates the reviewer accepted the session output. ReviewStatusApproved ReviewStatus = "approved" ) // String returns the underlying string value for the review status. func (r ReviewStatus) String() string { return string(r) } // TaskSessionState represents the state of an agent session type TaskSessionState string const ( // TaskSessionStateCreated - session created but agent not started TaskSessionStateCreated TaskSessionState = "CREATED" // TaskSessionStateStarting - agent is starting up TaskSessionStateStarting TaskSessionState = "STARTING" // TaskSessionStateRunning - agent is actively running TaskSessionStateRunning TaskSessionState = "RUNNING" // TaskSessionStateIdle - office sessions only: agent process and executor torn down // between turns; conversation (acp_session_id) preserved for the next run. TaskSessionStateIdle TaskSessionState = "IDLE" // TaskSessionStateWaitingForInput - agent waiting for user input TaskSessionStateWaitingForInput TaskSessionState = "WAITING_FOR_INPUT" // TaskSessionStateCompleted - agent finished successfully TaskSessionStateCompleted TaskSessionState = "COMPLETED" // TaskSessionStateFailed - agent failed with error TaskSessionStateFailed TaskSessionState = "FAILED" // TaskSessionStateCancelled - agent was manually stopped TaskSessionStateCancelled TaskSessionState = "CANCELLED" ) // TaskSessionWorktree represents the association between a task session and a worktree type TaskSessionWorktree struct { ID string `json:"id"` SessionID string `json:"session_id"` WorktreeID string `json:"worktree_id"` RepositoryID string `json:"repository_id"` Position int `json:"position"` CreatedAt time.Time `json:"created_at"` // Worktree details stored on this association WorktreePath string `json:"worktree_path,omitempty"` WorktreeBranch string `json:"worktree_branch,omitempty"` } // SessionBranchInfo is a lightweight projection of a session with its worktree branch. // Used by the PR watch reconciler to find sessions that may need PR watches. type SessionBranchInfo struct { SessionID string TaskID string Branch string } // TaskSession represents a persistent agent execution session for a task. // This replaces the in-memory TaskExecution tracking and survives backend restarts. type TaskSession struct { ID string `json:"id"` TaskID string `json:"task_id"` AgentExecutionID string `json:"agent_execution_id"` // Docker container/agent execution ContainerID string `json:"container_id"` // Docker container ID for cleanup AgentProfileID string `json:"agent_profile_id"` // ID of the agent profile used ExecutorID string `json:"executor_id"` ExecutorProfileID string `json:"executor_profile_id"` EnvironmentID string `json:"environment_id"` RepositoryID string `json:"repository_id"` // Primary repository (for backward compatibility) BaseBranch string `json:"base_branch"` // Primary base branch (for backward compatibility) BaseCommitSHA string `json:"base_commit_sha"` // Git commit SHA at session start (for cumulative diff) WorkspacePath string `json:"workspace_path"` // Optional host folder for repo-less tasks (when user picked a starting folder) Worktrees []*TaskSessionWorktree `json:"worktrees,omitempty"` // Associated worktrees AgentProfileSnapshot map[string]interface{} `json:"agent_profile_snapshot,omitempty"` ExecutorSnapshot map[string]interface{} `json:"executor_snapshot,omitempty"` EnvironmentSnapshot map[string]interface{} `json:"environment_snapshot,omitempty"` RepositorySnapshot map[string]interface{} `json:"repository_snapshot,omitempty"` State TaskSessionState `json:"state"` ErrorMessage string `json:"error_message,omitempty"` Metadata map[string]interface{} `json:"metadata,omitempty"` StartedAt time.Time `json:"started_at"` CompletedAt *time.Time `json:"completed_at,omitempty"` UpdatedAt time.Time `json:"updated_at"` // Environment reference TaskEnvironmentID string `json:"task_environment_id,omitempty"` // FK to task_environments for shared env // Workflow-related fields IsPrimary bool `json:"is_primary"` // Whether this is the primary session for the task IsPassthrough bool `json:"is_passthrough"` // Whether this session uses passthrough (PTY) mode ReviewStatus ReviewStatus `json:"review_status,omitempty"` // zero value = no review needed } // ToAPI converts internal TaskSession to API type // TODO: Add v1.TaskSession type to pkg/api/v1/ func (s *TaskSession) ToAPI() map[string]interface{} { result := map[string]interface{}{ "id": s.ID, "task_id": s.TaskID, "agent_execution_id": s.AgentExecutionID, "container_id": s.ContainerID, "agent_profile_id": s.AgentProfileID, "executor_id": s.ExecutorID, "executor_profile_id": s.ExecutorProfileID, "environment_id": s.EnvironmentID, "repository_id": s.RepositoryID, "base_branch": s.BaseBranch, "base_commit_sha": s.BaseCommitSHA, "worktrees": s.Worktrees, "state": string(s.State), "started_at": s.StartedAt, "updated_at": s.UpdatedAt, } // For backward compatibility, populate worktree_path and worktree_branch from first worktree if len(s.Worktrees) > 0 { result["worktree_path"] = s.Worktrees[0].WorktreePath result["worktree_branch"] = s.Worktrees[0].WorktreeBranch } if s.ErrorMessage != "" { result["error_message"] = s.ErrorMessage } if s.CompletedAt != nil { result["completed_at"] = s.CompletedAt } if s.Metadata != nil { result["metadata"] = s.Metadata } if s.AgentProfileSnapshot != nil { result["agent_profile_snapshot"] = s.AgentProfileSnapshot } if s.ExecutorSnapshot != nil { result["executor_snapshot"] = s.ExecutorSnapshot } if s.EnvironmentSnapshot != nil { result["environment_snapshot"] = s.EnvironmentSnapshot } if s.RepositorySnapshot != nil { result["repository_snapshot"] = s.RepositorySnapshot } result["is_passthrough"] = s.IsPassthrough if s.TaskEnvironmentID != "" { result["task_environment_id"] = s.TaskEnvironmentID } // Office sessions carry an agent_profile_id; the frontend uses it // to distinguish per-(task, agent) office sessions from per-launch // kanban/quick-chat sessions, which drives "live" detection in the // chat. Omit when blank so kanban responses look unchanged. if s.AgentProfileID != "" { result["agent_profile_id"] = s.AgentProfileID } return result } // Repository represents a workspace repository type Repository struct { ID string `json:"id"` WorkspaceID string `json:"workspace_id"` Name string `json:"name"` SourceType string `json:"source_type"` // LocalPath is the path to a local checkout; for provider-backed repos, this is // populated after the repo is cloned/synced on the agent host. LocalPath string `json:"local_path"` // Provider fields describe the upstream source (e.g. github/gitlab) for future syncing. Provider string `json:"provider"` ProviderRepoID string `json:"provider_repo_id"` ProviderOwner string `json:"provider_owner"` ProviderName string `json:"provider_name"` DefaultBranch string `json:"default_branch"` WorktreeBranchPrefix string `json:"worktree_branch_prefix"` PullBeforeWorktree bool `json:"pull_before_worktree"` SetupScript string `json:"setup_script"` CleanupScript string `json:"cleanup_script"` DevScript string `json:"dev_script"` CopyFiles string `json:"copy_files"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt *time.Time `json:"deleted_at,omitempty"` } // RepositoryScript represents a custom script for a repository type RepositoryScript struct { ID string `json:"id"` RepositoryID string `json:"repository_id"` Name string `json:"name"` Command string `json:"command"` Position int `json:"position"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // ExecutorType represents the executor runtime type. type ExecutorType string const ( ExecutorTypeLocal ExecutorType = "local" ExecutorTypeWorktree ExecutorType = "worktree" ExecutorTypeLocalDocker ExecutorType = "local_docker" ExecutorTypeRemoteDocker ExecutorType = "remote_docker" ExecutorTypeSprites ExecutorType = "sprites" ExecutorTypeSSH ExecutorType = "ssh" ExecutorTypeMockRemote ExecutorType = "mock_remote" ) // IsRemoteExecutorType reports whether the given executor type represents // a remote execution environment (including containerized environments like Docker). // These environments run shells inside the container/VM, not on the host. func IsRemoteExecutorType(t ExecutorType) bool { switch t { case ExecutorTypeSprites, ExecutorTypeRemoteDocker, ExecutorTypeLocalDocker, ExecutorTypeSSH, ExecutorTypeMockRemote: return true default: return false } } // Runtime maps an ExecutorType to the runtime backend that hosts the // agent subprocess. Mirrors executor.ExecutorTypeToBackend so callers // outside the executor package can ask the property without importing // it. The two are kept in lock-step intentionally — keep them in sync // when adding a new ExecutorType. func (t ExecutorType) Runtime() agentruntime.Runtime { switch t { case ExecutorTypeLocal, ExecutorTypeWorktree, ExecutorTypeMockRemote: return agentruntime.RuntimeStandalone case ExecutorTypeLocalDocker: return agentruntime.RuntimeDocker case ExecutorTypeRemoteDocker: return agentruntime.RuntimeRemoteDocker case ExecutorTypeSprites: return agentruntime.RuntimeSprites case ExecutorTypeSSH: return agentruntime.RuntimeSSH default: return agentruntime.RuntimeStandalone } } // IsContainerizedExecutorType reports whether the given executor type runs // in a container/sandbox where shells must be executed inside the container // via agentctl, not on the host. Single source of truth is // Runtime().IsContainerized() so a new ExecutorType only has to declare its // runtime once. func IsContainerizedExecutorType(t ExecutorType) bool { return t.Runtime().IsContainerized() } // IsAlwaysResumableRuntime reports whether the given runtime represents // an executor that can always be resumed even without an explicit resume token. func IsAlwaysResumableRuntime(runtime agentruntime.Runtime) bool { return runtime == agentruntime.RuntimeSprites || runtime == agentruntime.RuntimeSSH } const ( ExecutorIDLocal = "exec-local" ExecutorIDWorktree = "exec-worktree" ExecutorIDLocalDocker = "exec-local-docker" ExecutorIDSprites = "exec-sprites" ) // ExecutorStatus represents executor availability. type ExecutorStatus string const ( ExecutorStatusActive ExecutorStatus = "active" ExecutorStatusDisabled ExecutorStatus = "disabled" ) // Executor represents an execution target. type Executor struct { ID string `json:"id"` Name string `json:"name"` Type ExecutorType `json:"type"` Status ExecutorStatus `json:"status"` IsSystem bool `json:"is_system"` Resumable bool `json:"resumable"` Config map[string]string `json:"config,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt *time.Time `json:"deleted_at,omitempty"` } // ExecutorRunning tracks an active executor instance for a session. type ExecutorRunning struct { ID string `json:"id"` SessionID string `json:"session_id"` TaskID string `json:"task_id"` ExecutorID string `json:"executor_id"` Runtime agentruntime.Runtime `json:"runtime,omitempty"` Status string `json:"status"` Resumable bool `json:"resumable"` ResumeToken string `json:"resume_token,omitempty"` LastMessageUUID string `json:"last_message_uuid,omitempty"` AgentExecutionID string `json:"agent_execution_id,omitempty"` ContainerID string `json:"container_id,omitempty"` AgentctlURL string `json:"agentctl_url,omitempty"` AgentctlPort int `json:"agentctl_port,omitempty"` PID int `json:"pid,omitempty"` WorktreeID string `json:"worktree_id,omitempty"` WorktreePath string `json:"worktree_path,omitempty"` WorktreeBranch string `json:"worktree_branch,omitempty"` ErrorMessage string `json:"error_message,omitempty"` Metadata map[string]interface{} `json:"metadata,omitempty"` LastSeenAt *time.Time `json:"last_seen_at,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // ProfileEnvVar represents an environment variable for an executor profile. // Either Value (plain text) or SecretID (reference to a secret) should be set, not both. type ProfileEnvVar struct { Key string `json:"key"` Value string `json:"value,omitempty"` SecretID string `json:"secret_id,omitempty"` } // ExecutorProfile represents a named configuration preset for an executor. type ExecutorProfile struct { ID string `json:"id"` ExecutorID string `json:"executor_id"` Name string `json:"name"` McpPolicy string `json:"mcp_policy,omitempty"` Config map[string]string `json:"config,omitempty"` PrepareScript string `json:"prepare_script,omitempty"` CleanupScript string `json:"cleanup_script,omitempty"` EnvVars []ProfileEnvVar `json:"env_vars,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // EnvironmentKind represents the runtime type for environments. type EnvironmentKind string const ( EnvironmentKindLocalPC EnvironmentKind = "local_pc" EnvironmentKindDockerImage EnvironmentKind = "docker_image" ) const ( EnvironmentIDLocal = "env-local" ) // Environment represents a runtime environment configuration. type Environment struct { ID string `json:"id"` Name string `json:"name"` Kind EnvironmentKind `json:"kind"` IsSystem bool `json:"is_system"` WorktreeRoot string `json:"worktree_root,omitempty"` ImageTag string `json:"image_tag,omitempty"` Dockerfile string `json:"dockerfile,omitempty"` BuildConfig map[string]string `json:"build_config,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt *time.Time `json:"deleted_at,omitempty"` } // TaskEnvironmentStatus represents the lifecycle state of a task execution environment. type TaskEnvironmentStatus string const ( TaskEnvironmentStatusCreating TaskEnvironmentStatus = "creating" TaskEnvironmentStatusReady TaskEnvironmentStatus = "ready" TaskEnvironmentStatusStopped TaskEnvironmentStatus = "stopped" TaskEnvironmentStatusFailed TaskEnvironmentStatus = "failed" ) // TaskEnvironment represents a per-task execution environment instance. // It owns the workspace (worktree/container/sandbox) and the agentctl control server. // Multiple sessions can share the same TaskEnvironment. type TaskEnvironment struct { ID string `json:"id"` TaskID string `json:"task_id"` RepositoryID string `json:"repository_id"` // Deprecated: use Repos. Kept for dual-write/backwards compat. ExecutorType string `json:"executor_type"` ExecutorID string `json:"executor_id"` ExecutorProfileID string `json:"executor_profile_id"` // AgentExecutionID was removed: executors_running owns the execution<->session // mapping now. Read it via repo.GetExecutorRunningBySessionID(sessionID) when // needed (the orchestrator does this in service_turns.go for WorkspaceInfo). ControlPort int `json:"control_port"` // agentctl control port Status TaskEnvironmentStatus `json:"status"` // Type-specific fields. The single worktree fields below are legacy: with // multi-repo tasks, the per-repo worktrees live on Repos. WorkspacePath // continues to point at the agent workspace root (the task root when // TaskDirName is set, otherwise the single repo's worktree path). WorktreeID string `json:"worktree_id,omitempty"` // Deprecated: use Repos[i].WorktreeID WorktreePath string `json:"worktree_path,omitempty"` // Deprecated: use Repos[i].WorktreePath WorktreeBranch string `json:"worktree_branch,omitempty"` // Deprecated: use Repos[i].WorktreeBranch WorkspacePath string `json:"workspace_path,omitempty"` ContainerID string `json:"container_id,omitempty"` SandboxID string `json:"sandbox_id,omitempty"` // TaskDirName is the semantic directory name for the task (e.g. "fix-bug_ab12"). // Set when the task uses the multi-repo task-directory layout // (~/.kandev/tasks/{TaskDirName}/{RepoName}/). TaskDirName string `json:"task_dir_name,omitempty"` // Repos contains one entry per repository associated with this environment. // Populated by repository getters. Empty for environments created before the // multi-repo backfill ran with no legacy repository_id. Repos []*TaskEnvironmentRepo `json:"repos,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // RepoFor returns the per-repo environment row for repositoryID, or nil if not present. func (te *TaskEnvironment) RepoFor(repositoryID string) *TaskEnvironmentRepo { for _, r := range te.Repos { if r.RepositoryID == repositoryID { return r } } return nil } // TaskEnvironmentRepo represents the per-repository state of a task environment. // One row per repository associated with the task: each carries its own worktree // reference and any per-repo preparation error. type TaskEnvironmentRepo struct { ID string `json:"id"` TaskEnvironmentID string `json:"task_environment_id"` RepositoryID string `json:"repository_id"` WorktreeID string `json:"worktree_id,omitempty"` WorktreePath string `json:"worktree_path,omitempty"` WorktreeBranch string `json:"worktree_branch,omitempty"` Position int `json:"position"` ErrorMessage string `json:"error_message,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // ToAPI converts internal TaskEnvironment to API map. func (te *TaskEnvironment) ToAPI() map[string]interface{} { result := map[string]interface{}{ "id": te.ID, "task_id": te.TaskID, "repository_id": te.RepositoryID, "executor_type": te.ExecutorType, "executor_id": te.ExecutorID, "executor_profile_id": te.ExecutorProfileID, "status": string(te.Status), "workspace_path": te.WorkspacePath, "created_at": te.CreatedAt, "updated_at": te.UpdatedAt, } // agent_execution_id is no longer carried on TaskEnvironment — see executors_running. if te.ControlPort != 0 { result["control_port"] = te.ControlPort } if te.WorktreeID != "" { result["worktree_id"] = te.WorktreeID } if te.WorktreePath != "" { result["worktree_path"] = te.WorktreePath } if te.WorktreeBranch != "" { result["worktree_branch"] = te.WorktreeBranch } if te.ContainerID != "" { result["container_id"] = te.ContainerID } if te.SandboxID != "" { result["sandbox_id"] = te.SandboxID } if te.TaskDirName != "" { result["task_dir_name"] = te.TaskDirName } if len(te.Repos) > 0 { repos := make([]map[string]interface{}, 0, len(te.Repos)) for _, repo := range te.Repos { repos = append(repos, repo.ToAPI()) } result["repos"] = repos } return result } // ToAPI converts internal TaskEnvironmentRepo to API map. func (r *TaskEnvironmentRepo) ToAPI() map[string]interface{} { out := map[string]interface{}{ "id": r.ID, "task_environment_id": r.TaskEnvironmentID, "repository_id": r.RepositoryID, "position": r.Position, "created_at": r.CreatedAt, "updated_at": r.UpdatedAt, } if r.WorktreeID != "" { out["worktree_id"] = r.WorktreeID } if r.WorktreePath != "" { out["worktree_path"] = r.WorktreePath } if r.WorktreeBranch != "" { out["worktree_branch"] = r.WorktreeBranch } if r.ErrorMessage != "" { out["error_message"] = r.ErrorMessage } return out } // TaskPlan represents a plan associated with a task type TaskPlan struct { ID string `json:"id"` TaskID string `json:"task_id"` Title string `json:"title"` Content string `json:"content"` CreatedBy string `json:"created_by"` // "agent" or "user" CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // TaskPlanRevision is one immutable snapshot in the revision history of a task plan. // Revisions are the source of truth for history; TaskPlan stores the latest revision's content as HEAD. type TaskPlanRevision struct { ID string `json:"id"` TaskID string `json:"task_id"` RevisionNumber int `json:"revision_number"` Title string `json:"title"` Content string `json:"content"` AuthorKind string `json:"author_kind"` // "agent" | "user" AuthorName string `json:"author_name"` // display snapshot (agent profile name or user identifier) RevertOfRevisionID *string `json:"revert_of_revision_id,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` // bumps on coalesce merge } // TaskDocument represents a named document (plan, spec, notes, etc.) associated with a task. // The key uniquely identifies the document within a task (e.g., "plan", "spec", "notes"). type TaskDocument struct { ID string `json:"id" db:"id"` TaskID string `json:"task_id" db:"task_id"` Key string `json:"key" db:"key"` Type string `json:"type" db:"type"` Title string `json:"title" db:"title"` Content string `json:"content" db:"content"` AuthorKind string `json:"author_kind" db:"author_kind"` AuthorName string `json:"author_name" db:"author_name"` Filename string `json:"filename,omitempty" db:"filename"` MimeType string `json:"mime_type,omitempty" db:"mime_type"` SizeBytes int64 `json:"size_bytes,omitempty" db:"size_bytes"` DiskPath string `json:"-" db:"disk_path"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` } // TaskDocumentRevision is one immutable snapshot in the revision history of a task document. // Revisions are the source of truth for history; TaskDocument stores the latest revision's content as HEAD. type TaskDocumentRevision struct { ID string `json:"id" db:"id"` TaskID string `json:"task_id" db:"task_id"` DocumentKey string `json:"document_key" db:"document_key"` RevisionNumber int `json:"revision_number" db:"revision_number"` Title string `json:"title" db:"title"` Content string `json:"content" db:"content"` AuthorKind string `json:"author_kind" db:"author_kind"` AuthorName string `json:"author_name" db:"author_name"` RevertOfRevisionID *string `json:"revert_of_revision_id,omitempty" db:"revert_of_revision_id"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` } // SessionFileReview tracks per-file review state within a session type SessionFileReview struct { ID string `json:"id"` SessionID string `json:"session_id"` FilePath string `json:"file_path"` Reviewed bool `json:"reviewed"` DiffHash string `json:"diff_hash"` ReviewedAt *time.Time `json:"reviewed_at,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // ToAPI converts internal Task to API type func (t *Task) ToAPI() *v1.Task { // Convert TaskRepository models to API types var repositories []v1.TaskRepository for _, repo := range t.Repositories { repositories = append(repositories, v1.TaskRepository{ ID: repo.ID, TaskID: repo.TaskID, RepositoryID: repo.RepositoryID, BaseBranch: repo.BaseBranch, Position: repo.Position, Metadata: repo.Metadata, CreatedAt: repo.CreatedAt, UpdatedAt: repo.UpdatedAt, }) } result := &v1.Task{ ID: t.ID, WorkspaceID: t.WorkspaceID, WorkflowID: t.WorkflowID, Title: t.Title, Description: t.Description, State: t.State, Priority: t.Priority, Repositories: repositories, CreatedAt: t.CreatedAt, UpdatedAt: t.UpdatedAt, Metadata: t.Metadata, IsEphemeral: t.IsEphemeral, ParentID: t.ParentID, } if t.Identifier != "" { result.Identifier = t.Identifier } return result }