summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/storage/db.go44
-rw-r--r--internal/storage/db_test.go42
-rw-r--r--internal/task/task.go7
3 files changed, 86 insertions, 7 deletions
diff --git a/internal/storage/db.go b/internal/storage/db.go
index 043009c..2b7e33f 100644
--- a/internal/storage/db.go
+++ b/internal/storage/db.go
@@ -83,6 +83,7 @@ func (s *DB) migrate() error {
`ALTER TABLE executions ADD COLUMN sandbox_dir TEXT`,
`ALTER TABLE tasks ADD COLUMN summary TEXT`,
`ALTER TABLE tasks ADD COLUMN interactions_json TEXT NOT NULL DEFAULT '[]'`,
+ `ALTER TABLE executions ADD COLUMN changestats_json TEXT`,
}
for _, m := range migrations {
if _, err := s.db.Exec(m); err != nil {
@@ -366,6 +367,8 @@ type Execution struct {
SessionID string // claude --session-id; persisted for resume
SandboxDir string // preserved sandbox path when task is BLOCKED; resume must run here
+ Changestats *task.Changestats // stored as JSON; nil if not yet recorded
+
// In-memory only: set when creating a resume execution, not stored in DB.
ResumeSessionID string
ResumeAnswer string
@@ -375,24 +378,33 @@ type Execution struct {
// CreateExecution inserts an execution record.
func (s *DB) CreateExecution(e *Execution) error {
+ var changestatsJSON *string
+ if e.Changestats != nil {
+ b, err := json.Marshal(e.Changestats)
+ if err != nil {
+ return fmt.Errorf("marshaling changestats: %w", err)
+ }
+ s := string(b)
+ changestatsJSON = &s
+ }
_, err := s.db.Exec(`
- INSERT INTO executions (id, task_id, start_time, end_time, exit_code, status, stdout_path, stderr_path, artifact_dir, cost_usd, error_msg, session_id, sandbox_dir)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+ INSERT INTO executions (id, task_id, start_time, end_time, exit_code, status, stdout_path, stderr_path, artifact_dir, cost_usd, error_msg, session_id, sandbox_dir, changestats_json)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
e.ID, e.TaskID, e.StartTime.UTC(), e.EndTime.UTC(), e.ExitCode, e.Status,
- e.StdoutPath, e.StderrPath, e.ArtifactDir, e.CostUSD, e.ErrorMsg, e.SessionID, e.SandboxDir,
+ e.StdoutPath, e.StderrPath, e.ArtifactDir, e.CostUSD, e.ErrorMsg, e.SessionID, e.SandboxDir, changestatsJSON,
)
return err
}
// GetExecution retrieves an execution by ID.
func (s *DB) GetExecution(id string) (*Execution, error) {
- row := s.db.QueryRow(`SELECT id, task_id, start_time, end_time, exit_code, status, stdout_path, stderr_path, artifact_dir, cost_usd, error_msg, session_id, sandbox_dir FROM executions WHERE id = ?`, id)
+ row := s.db.QueryRow(`SELECT id, task_id, start_time, end_time, exit_code, status, stdout_path, stderr_path, artifact_dir, cost_usd, error_msg, session_id, sandbox_dir, changestats_json FROM executions WHERE id = ?`, id)
return scanExecution(row)
}
// ListExecutions returns executions for a task.
func (s *DB) ListExecutions(taskID string) ([]*Execution, error) {
- rows, err := s.db.Query(`SELECT id, task_id, start_time, end_time, exit_code, status, stdout_path, stderr_path, artifact_dir, cost_usd, error_msg, session_id, sandbox_dir FROM executions WHERE task_id = ? ORDER BY start_time DESC`, taskID)
+ rows, err := s.db.Query(`SELECT id, task_id, start_time, end_time, exit_code, status, stdout_path, stderr_path, artifact_dir, cost_usd, error_msg, session_id, sandbox_dir, changestats_json FROM executions WHERE task_id = ? ORDER BY start_time DESC`, taskID)
if err != nil {
return nil, err
}
@@ -411,7 +423,7 @@ func (s *DB) ListExecutions(taskID string) ([]*Execution, error) {
// GetLatestExecution returns the most recent execution for a task.
func (s *DB) GetLatestExecution(taskID string) (*Execution, error) {
- row := s.db.QueryRow(`SELECT id, task_id, start_time, end_time, exit_code, status, stdout_path, stderr_path, artifact_dir, cost_usd, error_msg, session_id, sandbox_dir FROM executions WHERE task_id = ? ORDER BY start_time DESC LIMIT 1`, taskID)
+ row := s.db.QueryRow(`SELECT id, task_id, start_time, end_time, exit_code, status, stdout_path, stderr_path, artifact_dir, cost_usd, error_msg, session_id, sandbox_dir, changestats_json FROM executions WHERE task_id = ? ORDER BY start_time DESC LIMIT 1`, taskID)
return scanExecution(row)
}
@@ -647,16 +659,34 @@ func scanExecution(row scanner) (*Execution, error) {
var e Execution
var sessionID sql.NullString
var sandboxDir sql.NullString
+ var changestatsJSON sql.NullString
err := row.Scan(&e.ID, &e.TaskID, &e.StartTime, &e.EndTime, &e.ExitCode, &e.Status,
- &e.StdoutPath, &e.StderrPath, &e.ArtifactDir, &e.CostUSD, &e.ErrorMsg, &sessionID, &sandboxDir)
+ &e.StdoutPath, &e.StderrPath, &e.ArtifactDir, &e.CostUSD, &e.ErrorMsg, &sessionID, &sandboxDir, &changestatsJSON)
if err != nil {
return nil, err
}
e.SessionID = sessionID.String
e.SandboxDir = sandboxDir.String
+ if changestatsJSON.Valid && changestatsJSON.String != "" {
+ var cs task.Changestats
+ if err := json.Unmarshal([]byte(changestatsJSON.String), &cs); err != nil {
+ return nil, fmt.Errorf("unmarshaling changestats: %w", err)
+ }
+ e.Changestats = &cs
+ }
return &e, nil
}
+// UpdateExecutionChangestats stores git change metrics for a completed execution.
+func (s *DB) UpdateExecutionChangestats(execID string, stats *task.Changestats) error {
+ b, err := json.Marshal(stats)
+ if err != nil {
+ return fmt.Errorf("marshaling changestats: %w", err)
+ }
+ _, err = s.db.Exec(`UPDATE executions SET changestats_json = ? WHERE id = ?`, string(b), execID)
+ return err
+}
+
func scanExecutionRows(rows *sql.Rows) (*Execution, error) {
return scanExecution(rows)
}
diff --git a/internal/storage/db_test.go b/internal/storage/db_test.go
index 31be246..a16311d 100644
--- a/internal/storage/db_test.go
+++ b/internal/storage/db_test.go
@@ -893,3 +893,45 @@ func TestAppendTaskInteraction_NotFound(t *testing.T) {
}
}
+func TestExecution_StoreAndRetrieveChangestats(t *testing.T) {
+ db := testDB(t)
+ now := time.Now().UTC().Truncate(time.Second)
+ db.CreateTask(makeTestTask("cs-task", now))
+
+ exec := &Execution{
+ ID: "cs-exec",
+ TaskID: "cs-task",
+ StartTime: now,
+ Status: "COMPLETED",
+ }
+ if err := db.CreateExecution(exec); err != nil {
+ t.Fatalf("CreateExecution: %v", err)
+ }
+
+ stats := &task.Changestats{
+ FilesChanged: 5,
+ LinesAdded: 127,
+ LinesRemoved: 43,
+ }
+ if err := db.UpdateExecutionChangestats("cs-exec", stats); err != nil {
+ t.Fatalf("UpdateExecutionChangestats: %v", err)
+ }
+
+ got, err := db.GetExecution("cs-exec")
+ if err != nil {
+ t.Fatalf("GetExecution: %v", err)
+ }
+ if got.Changestats == nil {
+ t.Fatal("Changestats: want non-nil, got nil")
+ }
+ if got.Changestats.FilesChanged != 5 {
+ t.Errorf("FilesChanged: want 5, got %d", got.Changestats.FilesChanged)
+ }
+ if got.Changestats.LinesAdded != 127 {
+ t.Errorf("LinesAdded: want 127, got %d", got.Changestats.LinesAdded)
+ }
+ if got.Changestats.LinesRemoved != 43 {
+ t.Errorf("LinesRemoved: want 43, got %d", got.Changestats.LinesRemoved)
+ }
+}
+
diff --git a/internal/task/task.go b/internal/task/task.go
index 69da5f3..b85b07c 100644
--- a/internal/task/task.go
+++ b/internal/task/task.go
@@ -48,6 +48,13 @@ type RetryConfig struct {
Backoff string `yaml:"backoff" json:"backoff"` // "linear", "exponential"
}
+// Changestats records file/line change metrics from an agent execution.
+type Changestats struct {
+ FilesChanged int `json:"files_changed"`
+ LinesAdded int `json:"lines_added"`
+ LinesRemoved int `json:"lines_removed"`
+}
+
// Interaction records a single question/answer exchange between an agent and the user.
type Interaction struct {
QuestionText string `json:"question_text"`