1136 lines
39 KiB
Go
1136 lines
39 KiB
Go
// Package sqlite provides SQLite-based repository implementations.
|
|
package sqlite
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
commonsqlite "github.com/kandev/kandev/internal/common/sqlite"
|
|
"github.com/kandev/kandev/internal/task/models"
|
|
)
|
|
|
|
// Turn operations
|
|
|
|
// CreateTurn creates a new turn
|
|
func (r *Repository) CreateTurn(ctx context.Context, turn *models.Turn) error {
|
|
if turn.ID == "" {
|
|
turn.ID = uuid.New().String()
|
|
}
|
|
now := time.Now().UTC()
|
|
if turn.StartedAt.IsZero() {
|
|
turn.StartedAt = now
|
|
}
|
|
if turn.CreatedAt.IsZero() {
|
|
turn.CreatedAt = now
|
|
}
|
|
turn.UpdatedAt = now
|
|
|
|
metadataJSON := "{}"
|
|
if turn.Metadata != nil {
|
|
metadataBytes, err := json.Marshal(turn.Metadata)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to serialize turn metadata: %w", err)
|
|
}
|
|
metadataJSON = string(metadataBytes)
|
|
}
|
|
|
|
_, err := r.db.ExecContext(ctx, `
|
|
INSERT INTO task_session_turns (id, task_session_id, task_id, started_at, completed_at, metadata, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
`, turn.ID, turn.TaskSessionID, turn.TaskID, turn.StartedAt, turn.CompletedAt, metadataJSON, turn.CreatedAt, turn.UpdatedAt)
|
|
|
|
return err
|
|
}
|
|
|
|
// GetTurn retrieves a turn by ID
|
|
func (r *Repository) GetTurn(ctx context.Context, id string) (*models.Turn, error) {
|
|
turn := &models.Turn{}
|
|
var metadataJSON string
|
|
var completedAt sql.NullTime
|
|
err := r.db.QueryRowContext(ctx, `
|
|
SELECT id, task_session_id, task_id, started_at, completed_at, metadata, created_at, updated_at
|
|
FROM task_session_turns WHERE id = ?
|
|
`, id).Scan(&turn.ID, &turn.TaskSessionID, &turn.TaskID, &turn.StartedAt, &completedAt, &metadataJSON, &turn.CreatedAt, &turn.UpdatedAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if completedAt.Valid {
|
|
turn.CompletedAt = &completedAt.Time
|
|
}
|
|
if metadataJSON != "" && metadataJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(metadataJSON), &turn.Metadata); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize turn metadata: %w", err)
|
|
}
|
|
}
|
|
return turn, nil
|
|
}
|
|
|
|
// GetActiveTurnBySessionID gets the currently active (non-completed) turn for a session
|
|
func (r *Repository) GetActiveTurnBySessionID(ctx context.Context, sessionID string) (*models.Turn, error) {
|
|
turn := &models.Turn{}
|
|
var metadataJSON string
|
|
var completedAt sql.NullTime
|
|
err := r.db.QueryRowContext(ctx, `
|
|
SELECT id, task_session_id, task_id, started_at, completed_at, metadata, created_at, updated_at
|
|
FROM task_session_turns
|
|
WHERE task_session_id = ? AND completed_at IS NULL
|
|
ORDER BY started_at DESC LIMIT 1
|
|
`, sessionID).Scan(&turn.ID, &turn.TaskSessionID, &turn.TaskID, &turn.StartedAt, &completedAt, &metadataJSON, &turn.CreatedAt, &turn.UpdatedAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if completedAt.Valid {
|
|
turn.CompletedAt = &completedAt.Time
|
|
}
|
|
if metadataJSON != "" && metadataJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(metadataJSON), &turn.Metadata); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize turn metadata: %w", err)
|
|
}
|
|
}
|
|
return turn, nil
|
|
}
|
|
|
|
// UpdateTurn updates an existing turn
|
|
func (r *Repository) UpdateTurn(ctx context.Context, turn *models.Turn) error {
|
|
turn.UpdatedAt = time.Now().UTC()
|
|
|
|
metadataJSON := "{}"
|
|
if turn.Metadata != nil {
|
|
metadataBytes, err := json.Marshal(turn.Metadata)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to serialize turn metadata: %w", err)
|
|
}
|
|
metadataJSON = string(metadataBytes)
|
|
}
|
|
|
|
_, err := r.db.ExecContext(ctx, `
|
|
UPDATE task_session_turns
|
|
SET completed_at = ?, metadata = ?, updated_at = ?
|
|
WHERE id = ?
|
|
`, turn.CompletedAt, metadataJSON, turn.UpdatedAt, turn.ID)
|
|
|
|
return err
|
|
}
|
|
|
|
// CompleteTurn marks a turn as completed with the current time
|
|
func (r *Repository) CompleteTurn(ctx context.Context, id string) error {
|
|
now := time.Now().UTC()
|
|
_, err := r.db.ExecContext(ctx, `
|
|
UPDATE task_session_turns
|
|
SET completed_at = ?, updated_at = ?
|
|
WHERE id = ?
|
|
`, now, now, id)
|
|
return err
|
|
}
|
|
|
|
// ListTurnsBySession returns all turns for a session ordered by start time
|
|
func (r *Repository) ListTurnsBySession(ctx context.Context, sessionID string) ([]*models.Turn, error) {
|
|
rows, err := r.db.QueryContext(ctx, `
|
|
SELECT id, task_session_id, task_id, started_at, completed_at, metadata, created_at, updated_at
|
|
FROM task_session_turns WHERE task_session_id = ? ORDER BY started_at ASC
|
|
`, sessionID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
var result []*models.Turn
|
|
for rows.Next() {
|
|
turn := &models.Turn{}
|
|
var metadataJSON string
|
|
var completedAt sql.NullTime
|
|
err := rows.Scan(&turn.ID, &turn.TaskSessionID, &turn.TaskID, &turn.StartedAt, &completedAt, &metadataJSON, &turn.CreatedAt, &turn.UpdatedAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if completedAt.Valid {
|
|
turn.CompletedAt = &completedAt.Time
|
|
}
|
|
if metadataJSON != "" && metadataJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(metadataJSON), &turn.Metadata); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize turn metadata: %w", err)
|
|
}
|
|
}
|
|
result = append(result, turn)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// Task Session operations
|
|
|
|
// CreateTaskSession creates a new agent session
|
|
func (r *Repository) CreateTaskSession(ctx context.Context, session *models.TaskSession) error {
|
|
if session.ID == "" {
|
|
session.ID = uuid.New().String()
|
|
}
|
|
now := time.Now().UTC()
|
|
session.StartedAt = now
|
|
session.UpdatedAt = now
|
|
if session.State == "" {
|
|
session.State = models.TaskSessionStateCreated
|
|
}
|
|
|
|
metadataJSON, err := json.Marshal(session.Metadata)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to serialize agent session metadata: %w", err)
|
|
}
|
|
agentProfileSnapshotJSON, err := json.Marshal(session.AgentProfileSnapshot)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to serialize agent profile snapshot: %w", err)
|
|
}
|
|
executorSnapshotJSON, err := json.Marshal(session.ExecutorSnapshot)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to serialize executor snapshot: %w", err)
|
|
}
|
|
environmentSnapshotJSON, err := json.Marshal(session.EnvironmentSnapshot)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to serialize environment snapshot: %w", err)
|
|
}
|
|
repositorySnapshotJSON, err := json.Marshal(session.RepositorySnapshot)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to serialize repository snapshot: %w", err)
|
|
}
|
|
_, err = r.db.ExecContext(ctx, `
|
|
INSERT INTO task_sessions (
|
|
id, task_id, agent_execution_id, container_id, agent_profile_id, executor_id, environment_id,
|
|
repository_id, base_branch,
|
|
agent_profile_snapshot, executor_snapshot, environment_snapshot, repository_snapshot,
|
|
state, error_message, metadata, started_at, completed_at, updated_at,
|
|
is_primary, workflow_step_id, review_status
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`, session.ID, session.TaskID, session.AgentExecutionID, session.ContainerID, session.AgentProfileID,
|
|
session.ExecutorID, session.EnvironmentID, session.RepositoryID, session.BaseBranch,
|
|
string(agentProfileSnapshotJSON), string(executorSnapshotJSON), string(environmentSnapshotJSON), string(repositorySnapshotJSON),
|
|
string(session.State), session.ErrorMessage, string(metadataJSON),
|
|
session.StartedAt, session.CompletedAt, session.UpdatedAt,
|
|
commonsqlite.BoolToInt(session.IsPrimary), session.WorkflowStepID, session.ReviewStatus)
|
|
|
|
return err
|
|
}
|
|
|
|
// GetTaskSession retrieves an agent session by ID
|
|
func (r *Repository) GetTaskSession(ctx context.Context, id string) (*models.TaskSession, error) {
|
|
session := &models.TaskSession{}
|
|
var state string
|
|
var metadataJSON string
|
|
var agentProfileSnapshotJSON string
|
|
var executorSnapshotJSON string
|
|
var environmentSnapshotJSON string
|
|
var repositorySnapshotJSON string
|
|
var completedAt sql.NullTime
|
|
var isPrimary int
|
|
var workflowStepID sql.NullString
|
|
var reviewStatus sql.NullString
|
|
|
|
err := r.db.QueryRowContext(ctx, `
|
|
SELECT id, task_id, agent_execution_id, container_id, agent_profile_id, executor_id, environment_id,
|
|
repository_id, base_branch,
|
|
agent_profile_snapshot, executor_snapshot, environment_snapshot, repository_snapshot,
|
|
state, error_message, metadata, started_at, completed_at, updated_at,
|
|
is_primary, workflow_step_id, review_status
|
|
FROM task_sessions WHERE id = ?
|
|
`, id).Scan(
|
|
&session.ID, &session.TaskID, &session.AgentExecutionID, &session.ContainerID, &session.AgentProfileID,
|
|
&session.ExecutorID, &session.EnvironmentID,
|
|
&session.RepositoryID, &session.BaseBranch,
|
|
&agentProfileSnapshotJSON, &executorSnapshotJSON, &environmentSnapshotJSON, &repositorySnapshotJSON,
|
|
&state, &session.ErrorMessage, &metadataJSON, &session.StartedAt, &completedAt, &session.UpdatedAt,
|
|
&isPrimary, &workflowStepID, &reviewStatus,
|
|
)
|
|
|
|
if err == sql.ErrNoRows {
|
|
return nil, fmt.Errorf("agent session not found: %s", id)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
session.State = models.TaskSessionState(state)
|
|
session.IsPrimary = isPrimary == 1
|
|
if workflowStepID.Valid {
|
|
session.WorkflowStepID = &workflowStepID.String
|
|
}
|
|
if reviewStatus.Valid {
|
|
session.ReviewStatus = &reviewStatus.String
|
|
}
|
|
if completedAt.Valid {
|
|
session.CompletedAt = &completedAt.Time
|
|
}
|
|
if metadataJSON != "" && metadataJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(metadataJSON), &session.Metadata); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize agent session metadata: %w", err)
|
|
}
|
|
}
|
|
if agentProfileSnapshotJSON != "" && agentProfileSnapshotJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(agentProfileSnapshotJSON), &session.AgentProfileSnapshot); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize agent profile snapshot: %w", err)
|
|
}
|
|
}
|
|
if executorSnapshotJSON != "" && executorSnapshotJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(executorSnapshotJSON), &session.ExecutorSnapshot); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize executor snapshot: %w", err)
|
|
}
|
|
}
|
|
if environmentSnapshotJSON != "" && environmentSnapshotJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(environmentSnapshotJSON), &session.EnvironmentSnapshot); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize environment snapshot: %w", err)
|
|
}
|
|
}
|
|
if repositorySnapshotJSON != "" && repositorySnapshotJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(repositorySnapshotJSON), &session.RepositorySnapshot); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize repository snapshot: %w", err)
|
|
}
|
|
}
|
|
|
|
worktrees, err := r.ListTaskSessionWorktrees(ctx, session.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load session worktrees: %w", err)
|
|
}
|
|
session.Worktrees = worktrees
|
|
|
|
return session, nil
|
|
}
|
|
|
|
// GetTaskSessionByTaskID retrieves the most recent agent session for a task
|
|
func (r *Repository) GetTaskSessionByTaskID(ctx context.Context, taskID string) (*models.TaskSession, error) {
|
|
session := &models.TaskSession{}
|
|
var state string
|
|
var metadataJSON string
|
|
var agentProfileSnapshotJSON string
|
|
var executorSnapshotJSON string
|
|
var environmentSnapshotJSON string
|
|
var repositorySnapshotJSON string
|
|
var completedAt sql.NullTime
|
|
var isPrimary int
|
|
var workflowStepID sql.NullString
|
|
var reviewStatus sql.NullString
|
|
|
|
err := r.db.QueryRowContext(ctx, `
|
|
SELECT id, task_id, agent_execution_id, container_id, agent_profile_id, executor_id, environment_id,
|
|
repository_id, base_branch,
|
|
agent_profile_snapshot, executor_snapshot, environment_snapshot, repository_snapshot,
|
|
state, error_message, metadata, started_at, completed_at, updated_at,
|
|
is_primary, workflow_step_id, review_status
|
|
FROM task_sessions WHERE task_id = ? ORDER BY started_at DESC LIMIT 1
|
|
`, taskID).Scan(
|
|
&session.ID, &session.TaskID, &session.AgentExecutionID, &session.ContainerID, &session.AgentProfileID,
|
|
&session.ExecutorID, &session.EnvironmentID,
|
|
&session.RepositoryID, &session.BaseBranch,
|
|
&agentProfileSnapshotJSON, &executorSnapshotJSON, &environmentSnapshotJSON, &repositorySnapshotJSON,
|
|
&state, &session.ErrorMessage, &metadataJSON, &session.StartedAt, &completedAt, &session.UpdatedAt,
|
|
&isPrimary, &workflowStepID, &reviewStatus,
|
|
)
|
|
|
|
if err == sql.ErrNoRows {
|
|
return nil, fmt.Errorf("agent session not found for task: %s", taskID)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
session.State = models.TaskSessionState(state)
|
|
session.IsPrimary = isPrimary == 1
|
|
if workflowStepID.Valid {
|
|
session.WorkflowStepID = &workflowStepID.String
|
|
}
|
|
if reviewStatus.Valid {
|
|
session.ReviewStatus = &reviewStatus.String
|
|
}
|
|
if completedAt.Valid {
|
|
session.CompletedAt = &completedAt.Time
|
|
}
|
|
if metadataJSON != "" && metadataJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(metadataJSON), &session.Metadata); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize agent session metadata: %w", err)
|
|
}
|
|
}
|
|
if agentProfileSnapshotJSON != "" && agentProfileSnapshotJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(agentProfileSnapshotJSON), &session.AgentProfileSnapshot); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize agent profile snapshot: %w", err)
|
|
}
|
|
}
|
|
if executorSnapshotJSON != "" && executorSnapshotJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(executorSnapshotJSON), &session.ExecutorSnapshot); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize executor snapshot: %w", err)
|
|
}
|
|
}
|
|
if environmentSnapshotJSON != "" && environmentSnapshotJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(environmentSnapshotJSON), &session.EnvironmentSnapshot); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize environment snapshot: %w", err)
|
|
}
|
|
}
|
|
if repositorySnapshotJSON != "" && repositorySnapshotJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(repositorySnapshotJSON), &session.RepositorySnapshot); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize repository snapshot: %w", err)
|
|
}
|
|
}
|
|
|
|
worktrees, err := r.ListTaskSessionWorktrees(ctx, session.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load session worktrees: %w", err)
|
|
}
|
|
session.Worktrees = worktrees
|
|
|
|
return session, nil
|
|
}
|
|
|
|
// GetActiveTaskSessionByTaskID retrieves the active (running/waiting) agent session for a task
|
|
func (r *Repository) GetActiveTaskSessionByTaskID(ctx context.Context, taskID string) (*models.TaskSession, error) {
|
|
session := &models.TaskSession{}
|
|
var state string
|
|
var metadataJSON string
|
|
var agentProfileSnapshotJSON string
|
|
var executorSnapshotJSON string
|
|
var environmentSnapshotJSON string
|
|
var repositorySnapshotJSON string
|
|
var completedAt sql.NullTime
|
|
var isPrimary int
|
|
var workflowStepID sql.NullString
|
|
var reviewStatus sql.NullString
|
|
|
|
err := r.db.QueryRowContext(ctx, `
|
|
SELECT id, task_id, agent_execution_id, container_id, agent_profile_id, executor_id, environment_id,
|
|
repository_id, base_branch,
|
|
agent_profile_snapshot, executor_snapshot, environment_snapshot, repository_snapshot,
|
|
state, error_message, metadata, started_at, completed_at, updated_at,
|
|
is_primary, workflow_step_id, review_status
|
|
FROM task_sessions
|
|
WHERE task_id = ? AND state IN ('CREATED', 'STARTING', 'RUNNING', 'WAITING_FOR_INPUT')
|
|
ORDER BY started_at DESC LIMIT 1
|
|
`, taskID).Scan(
|
|
&session.ID, &session.TaskID, &session.AgentExecutionID, &session.ContainerID, &session.AgentProfileID,
|
|
&session.ExecutorID, &session.EnvironmentID,
|
|
&session.RepositoryID, &session.BaseBranch,
|
|
&agentProfileSnapshotJSON, &executorSnapshotJSON, &environmentSnapshotJSON, &repositorySnapshotJSON,
|
|
&state, &session.ErrorMessage, &metadataJSON, &session.StartedAt, &completedAt, &session.UpdatedAt,
|
|
&isPrimary, &workflowStepID, &reviewStatus,
|
|
)
|
|
|
|
if err == sql.ErrNoRows {
|
|
return nil, fmt.Errorf("no active agent session for task: %s", taskID)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
session.State = models.TaskSessionState(state)
|
|
session.IsPrimary = isPrimary == 1
|
|
if workflowStepID.Valid {
|
|
session.WorkflowStepID = &workflowStepID.String
|
|
}
|
|
if reviewStatus.Valid {
|
|
session.ReviewStatus = &reviewStatus.String
|
|
}
|
|
if completedAt.Valid {
|
|
session.CompletedAt = &completedAt.Time
|
|
}
|
|
if metadataJSON != "" && metadataJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(metadataJSON), &session.Metadata); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize agent session metadata: %w", err)
|
|
}
|
|
}
|
|
if agentProfileSnapshotJSON != "" && agentProfileSnapshotJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(agentProfileSnapshotJSON), &session.AgentProfileSnapshot); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize agent profile snapshot: %w", err)
|
|
}
|
|
}
|
|
if executorSnapshotJSON != "" && executorSnapshotJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(executorSnapshotJSON), &session.ExecutorSnapshot); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize executor snapshot: %w", err)
|
|
}
|
|
}
|
|
if environmentSnapshotJSON != "" && environmentSnapshotJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(environmentSnapshotJSON), &session.EnvironmentSnapshot); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize environment snapshot: %w", err)
|
|
}
|
|
}
|
|
if repositorySnapshotJSON != "" && repositorySnapshotJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(repositorySnapshotJSON), &session.RepositorySnapshot); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize repository snapshot: %w", err)
|
|
}
|
|
}
|
|
|
|
worktrees, err := r.ListTaskSessionWorktrees(ctx, session.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load session worktrees: %w", err)
|
|
}
|
|
session.Worktrees = worktrees
|
|
|
|
return session, nil
|
|
}
|
|
|
|
// UpdateTaskSession updates an existing agent session
|
|
func (r *Repository) UpdateTaskSession(ctx context.Context, session *models.TaskSession) error {
|
|
session.UpdatedAt = time.Now().UTC()
|
|
|
|
metadataJSON, err := json.Marshal(session.Metadata)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to serialize agent session metadata: %w", err)
|
|
}
|
|
agentProfileSnapshotJSON, err := json.Marshal(session.AgentProfileSnapshot)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to serialize agent profile snapshot: %w", err)
|
|
}
|
|
executorSnapshotJSON, err := json.Marshal(session.ExecutorSnapshot)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to serialize executor snapshot: %w", err)
|
|
}
|
|
environmentSnapshotJSON, err := json.Marshal(session.EnvironmentSnapshot)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to serialize environment snapshot: %w", err)
|
|
}
|
|
repositorySnapshotJSON, err := json.Marshal(session.RepositorySnapshot)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to serialize repository snapshot: %w", err)
|
|
}
|
|
|
|
result, err := r.db.ExecContext(ctx, `
|
|
UPDATE task_sessions SET
|
|
agent_execution_id = ?, container_id = ?, agent_profile_id = ?, executor_id = ?, environment_id = ?,
|
|
repository_id = ?, base_branch = ?,
|
|
agent_profile_snapshot = ?, executor_snapshot = ?, environment_snapshot = ?, repository_snapshot = ?,
|
|
state = ?, error_message = ?, metadata = ?, completed_at = ?, updated_at = ?,
|
|
is_primary = ?, workflow_step_id = ?, review_status = ?
|
|
WHERE id = ?
|
|
`, session.AgentExecutionID, session.ContainerID, session.AgentProfileID, session.ExecutorID, session.EnvironmentID,
|
|
session.RepositoryID, session.BaseBranch,
|
|
string(agentProfileSnapshotJSON), string(executorSnapshotJSON), string(environmentSnapshotJSON), string(repositorySnapshotJSON),
|
|
string(session.State), session.ErrorMessage, string(metadataJSON), session.CompletedAt, session.UpdatedAt,
|
|
commonsqlite.BoolToInt(session.IsPrimary), session.WorkflowStepID, session.ReviewStatus,
|
|
session.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rows, _ := result.RowsAffected()
|
|
if rows == 0 {
|
|
return fmt.Errorf("agent session not found: %s", session.ID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UpdateTaskSessionState updates just the state and error message of an agent session
|
|
func (r *Repository) UpdateTaskSessionState(ctx context.Context, id string, status models.TaskSessionState, errorMessage string) error {
|
|
now := time.Now().UTC()
|
|
|
|
var completedAt *time.Time
|
|
if status == models.TaskSessionStateCompleted || status == models.TaskSessionStateFailed || status == models.TaskSessionStateCancelled {
|
|
completedAt = &now
|
|
}
|
|
|
|
result, err := r.db.ExecContext(ctx, `
|
|
UPDATE task_sessions SET state = ?, error_message = ?, completed_at = ?, updated_at = ? WHERE id = ?
|
|
`, string(status), errorMessage, completedAt, now, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rows, _ := result.RowsAffected()
|
|
if rows == 0 {
|
|
return fmt.Errorf("agent session not found: %s", id)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ClearSessionExecutionID clears the agent_execution_id for a session.
|
|
// This is used when a stale execution ID needs to be removed (e.g., after a failed resume on startup).
|
|
func (r *Repository) ClearSessionExecutionID(ctx context.Context, id string) error {
|
|
now := time.Now().UTC()
|
|
result, err := r.db.ExecContext(ctx, `
|
|
UPDATE task_sessions SET agent_execution_id = '', container_id = '', updated_at = ? WHERE id = ?
|
|
`, now, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rows, _ := result.RowsAffected()
|
|
if rows == 0 {
|
|
return fmt.Errorf("agent session not found: %s", id)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ListTaskSessions returns all agent sessions for a task
|
|
func (r *Repository) ListTaskSessions(ctx context.Context, taskID string) ([]*models.TaskSession, error) {
|
|
rows, err := r.db.QueryContext(ctx, `
|
|
SELECT id, task_id, agent_execution_id, container_id, agent_profile_id, executor_id, environment_id,
|
|
repository_id, base_branch,
|
|
agent_profile_snapshot, executor_snapshot, environment_snapshot, repository_snapshot,
|
|
state, error_message, metadata, started_at, completed_at, updated_at,
|
|
is_primary, workflow_step_id, review_status
|
|
FROM task_sessions WHERE task_id = ? ORDER BY started_at DESC
|
|
`, taskID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
sessions, err := r.scanTaskSessions(ctx, rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, session := range sessions {
|
|
worktrees, err := r.ListTaskSessionWorktrees(ctx, session.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load session worktrees: %w", err)
|
|
}
|
|
session.Worktrees = worktrees
|
|
}
|
|
return sessions, nil
|
|
}
|
|
|
|
// ListActiveTaskSessions returns all active agent sessions across all tasks
|
|
func (r *Repository) ListActiveTaskSessions(ctx context.Context) ([]*models.TaskSession, error) {
|
|
rows, err := r.db.QueryContext(ctx, `
|
|
SELECT id, task_id, agent_execution_id, container_id, agent_profile_id, executor_id, environment_id,
|
|
repository_id, base_branch,
|
|
agent_profile_snapshot, executor_snapshot, environment_snapshot, repository_snapshot,
|
|
state, error_message, metadata, started_at, completed_at, updated_at,
|
|
is_primary, workflow_step_id, review_status
|
|
FROM task_sessions WHERE state IN ('CREATED', 'STARTING', 'RUNNING', 'WAITING_FOR_INPUT') ORDER BY started_at DESC
|
|
`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
sessions, err := r.scanTaskSessions(ctx, rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, session := range sessions {
|
|
worktrees, err := r.ListTaskSessionWorktrees(ctx, session.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load session worktrees: %w", err)
|
|
}
|
|
session.Worktrees = worktrees
|
|
}
|
|
return sessions, nil
|
|
}
|
|
|
|
// ListActiveTaskSessionsByTaskID returns all active agent sessions for a specific task
|
|
func (r *Repository) ListActiveTaskSessionsByTaskID(ctx context.Context, taskID string) ([]*models.TaskSession, error) {
|
|
rows, err := r.db.QueryContext(ctx, `
|
|
SELECT id, task_id, agent_execution_id, container_id, agent_profile_id, executor_id, environment_id,
|
|
repository_id, base_branch,
|
|
agent_profile_snapshot, executor_snapshot, environment_snapshot, repository_snapshot,
|
|
state, error_message, metadata, started_at, completed_at, updated_at,
|
|
is_primary, workflow_step_id, review_status
|
|
FROM task_sessions WHERE task_id = ? AND state IN ('CREATED', 'STARTING', 'RUNNING', 'WAITING_FOR_INPUT') ORDER BY started_at DESC
|
|
`, taskID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
sessions, err := r.scanTaskSessions(ctx, rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, session := range sessions {
|
|
worktrees, err := r.ListTaskSessionWorktrees(ctx, session.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load session worktrees: %w", err)
|
|
}
|
|
session.Worktrees = worktrees
|
|
}
|
|
return sessions, nil
|
|
}
|
|
|
|
func (r *Repository) HasActiveTaskSessionsByAgentProfile(ctx context.Context, agentProfileID string) (bool, error) {
|
|
var exists int
|
|
err := r.db.QueryRowContext(ctx, `
|
|
SELECT 1 FROM task_sessions
|
|
WHERE agent_profile_id = ? AND state IN ('CREATED', 'STARTING', 'RUNNING', 'WAITING_FOR_INPUT')
|
|
LIMIT 1
|
|
`, agentProfileID).Scan(&exists)
|
|
if err == sql.ErrNoRows {
|
|
return false, nil
|
|
}
|
|
return err == nil, err
|
|
}
|
|
|
|
func (r *Repository) HasActiveTaskSessionsByEnvironment(ctx context.Context, environmentID string) (bool, error) {
|
|
var exists int
|
|
err := r.db.QueryRowContext(ctx, `
|
|
SELECT 1 FROM task_sessions
|
|
WHERE environment_id = ? AND state IN ('CREATED', 'STARTING', 'RUNNING', 'WAITING_FOR_INPUT')
|
|
LIMIT 1
|
|
`, environmentID).Scan(&exists)
|
|
if err == sql.ErrNoRows {
|
|
return false, nil
|
|
}
|
|
return err == nil, err
|
|
}
|
|
|
|
func (r *Repository) HasActiveTaskSessionsByRepository(ctx context.Context, repositoryID string) (bool, error) {
|
|
var exists int
|
|
err := r.db.QueryRowContext(ctx, `
|
|
SELECT 1
|
|
FROM task_sessions s
|
|
INNER JOIN task_repositories tr ON tr.task_id = s.task_id
|
|
WHERE s.state IN ('CREATED', 'STARTING', 'RUNNING', 'WAITING_FOR_INPUT')
|
|
AND tr.repository_id = ?
|
|
LIMIT 1
|
|
`, repositoryID).Scan(&exists)
|
|
if err == sql.ErrNoRows {
|
|
return false, nil
|
|
}
|
|
return err == nil, err
|
|
}
|
|
|
|
// scanTaskSessions is a helper to scan multiple agent session rows
|
|
func (r *Repository) scanTaskSessions(ctx context.Context, rows *sql.Rows) ([]*models.TaskSession, error) {
|
|
var result []*models.TaskSession
|
|
for rows.Next() {
|
|
session := &models.TaskSession{}
|
|
var state string
|
|
var metadataJSON string
|
|
var agentProfileSnapshotJSON string
|
|
var executorSnapshotJSON string
|
|
var environmentSnapshotJSON string
|
|
var repositorySnapshotJSON string
|
|
var completedAt sql.NullTime
|
|
var isPrimary int
|
|
var workflowStepID sql.NullString
|
|
var reviewStatus sql.NullString
|
|
|
|
err := rows.Scan(
|
|
&session.ID, &session.TaskID, &session.AgentExecutionID, &session.ContainerID, &session.AgentProfileID,
|
|
&session.ExecutorID, &session.EnvironmentID,
|
|
&session.RepositoryID, &session.BaseBranch,
|
|
&agentProfileSnapshotJSON, &executorSnapshotJSON, &environmentSnapshotJSON, &repositorySnapshotJSON,
|
|
&state, &session.ErrorMessage, &metadataJSON, &session.StartedAt, &completedAt, &session.UpdatedAt,
|
|
&isPrimary, &workflowStepID, &reviewStatus,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
session.State = models.TaskSessionState(state)
|
|
session.IsPrimary = isPrimary == 1
|
|
if workflowStepID.Valid {
|
|
session.WorkflowStepID = &workflowStepID.String
|
|
}
|
|
if reviewStatus.Valid {
|
|
session.ReviewStatus = &reviewStatus.String
|
|
}
|
|
if completedAt.Valid {
|
|
session.CompletedAt = &completedAt.Time
|
|
}
|
|
if metadataJSON != "" && metadataJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(metadataJSON), &session.Metadata); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize agent session metadata: %w", err)
|
|
}
|
|
}
|
|
if agentProfileSnapshotJSON != "" && agentProfileSnapshotJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(agentProfileSnapshotJSON), &session.AgentProfileSnapshot); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize agent profile snapshot: %w", err)
|
|
}
|
|
}
|
|
if executorSnapshotJSON != "" && executorSnapshotJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(executorSnapshotJSON), &session.ExecutorSnapshot); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize executor snapshot: %w", err)
|
|
}
|
|
}
|
|
if environmentSnapshotJSON != "" && environmentSnapshotJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(environmentSnapshotJSON), &session.EnvironmentSnapshot); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize environment snapshot: %w", err)
|
|
}
|
|
}
|
|
if repositorySnapshotJSON != "" && repositorySnapshotJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(repositorySnapshotJSON), &session.RepositorySnapshot); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize repository snapshot: %w", err)
|
|
}
|
|
}
|
|
|
|
result = append(result, session)
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
// DeleteTaskSession deletes an agent session by ID
|
|
func (r *Repository) DeleteTaskSession(ctx context.Context, id string) error {
|
|
result, err := r.db.ExecContext(ctx, `DELETE FROM task_sessions WHERE id = ?`, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rows, _ := result.RowsAffected()
|
|
if rows == 0 {
|
|
return fmt.Errorf("agent session not found: %s", id)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Task Session Worktree operations
|
|
|
|
func (r *Repository) CreateTaskSessionWorktree(ctx context.Context, sessionWorktree *models.TaskSessionWorktree) error {
|
|
if sessionWorktree.ID == "" {
|
|
sessionWorktree.ID = uuid.New().String()
|
|
}
|
|
now := time.Now().UTC()
|
|
sessionWorktree.CreatedAt = now
|
|
updatedAt := now
|
|
|
|
_, err := r.db.ExecContext(ctx, `
|
|
INSERT INTO task_session_worktrees (
|
|
id, session_id, worktree_id, repository_id, position,
|
|
worktree_path, worktree_branch, created_at, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(session_id, worktree_id) DO UPDATE SET
|
|
repository_id = excluded.repository_id,
|
|
position = excluded.position,
|
|
worktree_path = excluded.worktree_path,
|
|
worktree_branch = excluded.worktree_branch,
|
|
updated_at = excluded.updated_at
|
|
`,
|
|
sessionWorktree.ID,
|
|
sessionWorktree.SessionID,
|
|
sessionWorktree.WorktreeID,
|
|
sessionWorktree.RepositoryID,
|
|
sessionWorktree.Position,
|
|
sessionWorktree.WorktreePath,
|
|
sessionWorktree.WorktreeBranch,
|
|
sessionWorktree.CreatedAt,
|
|
updatedAt,
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) ListTaskSessionWorktrees(ctx context.Context, sessionID string) ([]*models.TaskSessionWorktree, error) {
|
|
rows, err := r.db.QueryContext(ctx, `
|
|
SELECT
|
|
tsw.id, tsw.session_id, tsw.worktree_id, tsw.repository_id, tsw.position,
|
|
tsw.worktree_path, tsw.worktree_branch, tsw.created_at
|
|
FROM task_session_worktrees tsw
|
|
WHERE tsw.session_id = ?
|
|
ORDER BY tsw.position ASC, tsw.created_at ASC
|
|
`, sessionID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
var worktrees []*models.TaskSessionWorktree
|
|
for rows.Next() {
|
|
var wt models.TaskSessionWorktree
|
|
err := rows.Scan(
|
|
&wt.ID,
|
|
&wt.SessionID,
|
|
&wt.WorktreeID,
|
|
&wt.RepositoryID,
|
|
&wt.Position,
|
|
&wt.WorktreePath,
|
|
&wt.WorktreeBranch,
|
|
&wt.CreatedAt,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
worktrees = append(worktrees, &wt)
|
|
}
|
|
return worktrees, rows.Err()
|
|
}
|
|
|
|
func (r *Repository) DeleteTaskSessionWorktree(ctx context.Context, id string) error {
|
|
result, err := r.db.ExecContext(ctx, `DELETE FROM task_session_worktrees WHERE id = ?`, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rows, _ := result.RowsAffected()
|
|
if rows == 0 {
|
|
return fmt.Errorf("task session worktree not found: %s", id)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Repository) DeleteTaskSessionWorktreesBySession(ctx context.Context, sessionID string) error {
|
|
_, err := r.db.ExecContext(ctx, `DELETE FROM task_session_worktrees WHERE session_id = ?`, sessionID)
|
|
return err
|
|
}
|
|
|
|
// GetPrimarySessionByTaskID retrieves the primary session for a task
|
|
func (r *Repository) GetPrimarySessionByTaskID(ctx context.Context, taskID string) (*models.TaskSession, error) {
|
|
session := &models.TaskSession{}
|
|
var state string
|
|
var metadataJSON string
|
|
var agentProfileSnapshotJSON string
|
|
var executorSnapshotJSON string
|
|
var environmentSnapshotJSON string
|
|
var repositorySnapshotJSON string
|
|
var completedAt sql.NullTime
|
|
var isPrimary int
|
|
var workflowStepID sql.NullString
|
|
var reviewStatus sql.NullString
|
|
|
|
err := r.db.QueryRowContext(ctx, `
|
|
SELECT id, task_id, agent_execution_id, container_id, agent_profile_id, executor_id, environment_id,
|
|
repository_id, base_branch,
|
|
agent_profile_snapshot, executor_snapshot, environment_snapshot, repository_snapshot,
|
|
state, error_message, metadata, started_at, completed_at, updated_at,
|
|
is_primary, workflow_step_id, review_status
|
|
FROM task_sessions WHERE task_id = ? AND is_primary = 1 LIMIT 1
|
|
`, taskID).Scan(
|
|
&session.ID, &session.TaskID, &session.AgentExecutionID, &session.ContainerID, &session.AgentProfileID,
|
|
&session.ExecutorID, &session.EnvironmentID,
|
|
&session.RepositoryID, &session.BaseBranch,
|
|
&agentProfileSnapshotJSON, &executorSnapshotJSON, &environmentSnapshotJSON, &repositorySnapshotJSON,
|
|
&state, &session.ErrorMessage, &metadataJSON, &session.StartedAt, &completedAt, &session.UpdatedAt,
|
|
&isPrimary, &workflowStepID, &reviewStatus,
|
|
)
|
|
|
|
if err == sql.ErrNoRows {
|
|
return nil, fmt.Errorf("no primary session found for task: %s", taskID)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
session.State = models.TaskSessionState(state)
|
|
session.IsPrimary = isPrimary == 1
|
|
if workflowStepID.Valid {
|
|
session.WorkflowStepID = &workflowStepID.String
|
|
}
|
|
if reviewStatus.Valid {
|
|
session.ReviewStatus = &reviewStatus.String
|
|
}
|
|
if completedAt.Valid {
|
|
session.CompletedAt = &completedAt.Time
|
|
}
|
|
if metadataJSON != "" && metadataJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(metadataJSON), &session.Metadata); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize agent session metadata: %w", err)
|
|
}
|
|
}
|
|
if agentProfileSnapshotJSON != "" && agentProfileSnapshotJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(agentProfileSnapshotJSON), &session.AgentProfileSnapshot); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize agent profile snapshot: %w", err)
|
|
}
|
|
}
|
|
if executorSnapshotJSON != "" && executorSnapshotJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(executorSnapshotJSON), &session.ExecutorSnapshot); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize executor snapshot: %w", err)
|
|
}
|
|
}
|
|
if environmentSnapshotJSON != "" && environmentSnapshotJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(environmentSnapshotJSON), &session.EnvironmentSnapshot); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize environment snapshot: %w", err)
|
|
}
|
|
}
|
|
if repositorySnapshotJSON != "" && repositorySnapshotJSON != "{}" {
|
|
if err := json.Unmarshal([]byte(repositorySnapshotJSON), &session.RepositorySnapshot); err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize repository snapshot: %w", err)
|
|
}
|
|
}
|
|
|
|
worktrees, err := r.ListTaskSessionWorktrees(ctx, session.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load session worktrees: %w", err)
|
|
}
|
|
session.Worktrees = worktrees
|
|
|
|
return session, nil
|
|
}
|
|
|
|
// GetPrimarySessionIDsByTaskIDs returns a map of task ID to primary session ID for the given task IDs.
|
|
// Tasks without a primary session are not included in the result.
|
|
func (r *Repository) GetPrimarySessionIDsByTaskIDs(ctx context.Context, taskIDs []string) (map[string]string, error) {
|
|
if len(taskIDs) == 0 {
|
|
return make(map[string]string), nil
|
|
}
|
|
|
|
// Build placeholders for IN clause
|
|
placeholders := make([]string, len(taskIDs))
|
|
args := make([]interface{}, len(taskIDs))
|
|
for i, id := range taskIDs {
|
|
placeholders[i] = "?"
|
|
args[i] = id
|
|
}
|
|
|
|
query := fmt.Sprintf(`
|
|
SELECT id, task_id FROM task_sessions
|
|
WHERE task_id IN (%s) AND is_primary = 1
|
|
`, strings.Join(placeholders, ","))
|
|
|
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
result := make(map[string]string)
|
|
for rows.Next() {
|
|
var sessionID, taskID string
|
|
if err := rows.Scan(&sessionID, &taskID); err != nil {
|
|
return nil, err
|
|
}
|
|
result[taskID] = sessionID
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
// GetSessionCountsByTaskIDs returns a map of task ID to session count for the given task IDs.
|
|
func (r *Repository) GetSessionCountsByTaskIDs(ctx context.Context, taskIDs []string) (map[string]int, error) {
|
|
if len(taskIDs) == 0 {
|
|
return make(map[string]int), nil
|
|
}
|
|
|
|
// Build placeholders for IN clause
|
|
placeholders := make([]string, len(taskIDs))
|
|
args := make([]interface{}, len(taskIDs))
|
|
for i, id := range taskIDs {
|
|
placeholders[i] = "?"
|
|
args[i] = id
|
|
}
|
|
|
|
query := fmt.Sprintf(`
|
|
SELECT task_id, COUNT(*) as count FROM task_sessions
|
|
WHERE task_id IN (%s)
|
|
GROUP BY task_id
|
|
`, strings.Join(placeholders, ","))
|
|
|
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
result := make(map[string]int)
|
|
for rows.Next() {
|
|
var taskID string
|
|
var count int
|
|
if err := rows.Scan(&taskID, &count); err != nil {
|
|
return nil, err
|
|
}
|
|
result[taskID] = count
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
// GetPrimarySessionInfoByTaskIDs returns a map of task ID to primary session for the given task IDs.
|
|
// Only returns the review_status field from the session.
|
|
func (r *Repository) GetPrimarySessionInfoByTaskIDs(ctx context.Context, taskIDs []string) (map[string]*models.TaskSession, error) {
|
|
if len(taskIDs) == 0 {
|
|
return make(map[string]*models.TaskSession), nil
|
|
}
|
|
|
|
// Build placeholders for IN clause
|
|
placeholders := make([]string, len(taskIDs))
|
|
args := make([]interface{}, len(taskIDs))
|
|
for i, id := range taskIDs {
|
|
placeholders[i] = "?"
|
|
args[i] = id
|
|
}
|
|
|
|
query := fmt.Sprintf(`
|
|
SELECT task_id, review_status FROM task_sessions
|
|
WHERE task_id IN (%s) AND is_primary = 1
|
|
`, strings.Join(placeholders, ","))
|
|
|
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
result := make(map[string]*models.TaskSession)
|
|
for rows.Next() {
|
|
var taskID string
|
|
var reviewStatus sql.NullString
|
|
if err := rows.Scan(&taskID, &reviewStatus); err != nil {
|
|
return nil, err
|
|
}
|
|
session := &models.TaskSession{
|
|
TaskID: taskID,
|
|
}
|
|
if reviewStatus.Valid {
|
|
session.ReviewStatus = &reviewStatus.String
|
|
}
|
|
result[taskID] = session
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
// SetSessionPrimary marks a session as primary and clears primary flag on other sessions for the same task
|
|
func (r *Repository) SetSessionPrimary(ctx context.Context, sessionID string) error {
|
|
now := time.Now().UTC()
|
|
|
|
// First, get the task_id for this session
|
|
var taskID string
|
|
err := r.db.QueryRowContext(ctx, `SELECT task_id FROM task_sessions WHERE id = ?`, sessionID).Scan(&taskID)
|
|
if err == sql.ErrNoRows {
|
|
return fmt.Errorf("session not found: %s", sessionID)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Clear primary flag on all sessions for this task
|
|
_, err = r.db.ExecContext(ctx, `
|
|
UPDATE task_sessions SET is_primary = 0, updated_at = ? WHERE task_id = ?
|
|
`, now, taskID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set primary flag on the specified session
|
|
result, err := r.db.ExecContext(ctx, `
|
|
UPDATE task_sessions SET is_primary = 1, updated_at = ? WHERE id = ?
|
|
`, now, sessionID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rows, _ := result.RowsAffected()
|
|
if rows == 0 {
|
|
return fmt.Errorf("session not found: %s", sessionID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UpdateSessionWorkflowStep updates the workflow step for a session
|
|
func (r *Repository) UpdateSessionWorkflowStep(ctx context.Context, sessionID string, stepID string) error {
|
|
now := time.Now().UTC()
|
|
|
|
result, err := r.db.ExecContext(ctx, `
|
|
UPDATE task_sessions SET workflow_step_id = ?, updated_at = ? WHERE id = ?
|
|
`, stepID, now, sessionID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rows, _ := result.RowsAffected()
|
|
if rows == 0 {
|
|
return fmt.Errorf("session not found: %s", sessionID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UpdateSessionReviewStatus updates the review status of a session
|
|
func (r *Repository) UpdateSessionReviewStatus(ctx context.Context, sessionID string, status string) error {
|
|
now := time.Now().UTC()
|
|
|
|
result, err := r.db.ExecContext(ctx, `
|
|
UPDATE task_sessions SET review_status = ?, updated_at = ? WHERE id = ?
|
|
`, status, now, sessionID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rows, _ := result.RowsAffected()
|
|
if rows == 0 {
|
|
return fmt.Errorf("session not found: %s", sessionID)
|
|
}
|
|
return nil
|
|
}
|