- Add unbounded CLI sessions agent-task with plan and code review logs - Add edge console with tests - Add node run manager for session lifecycle management - Add router and store tests - Update transport session handling - Add runtime proto definitions - Update configurations and packages
161 lines
4 KiB
Go
161 lines
4 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"time"
|
|
|
|
_ "modernc.org/sqlite"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
const schema = `
|
|
CREATE TABLE IF NOT EXISTS runs (
|
|
run_id TEXT PRIMARY KEY,
|
|
adapter TEXT NOT NULL,
|
|
model TEXT NOT NULL,
|
|
session_id TEXT NOT NULL DEFAULT 'default',
|
|
background INTEGER NOT NULL DEFAULT 0,
|
|
status TEXT NOT NULL DEFAULT 'running',
|
|
created_at DATETIME NOT NULL,
|
|
completed_at DATETIME,
|
|
error TEXT
|
|
);
|
|
`
|
|
|
|
// RunRecord holds the persisted state of an execution.
|
|
type RunRecord struct {
|
|
RunID string
|
|
Adapter string
|
|
Model string
|
|
SessionID string
|
|
Background bool
|
|
Status string
|
|
CreatedAt time.Time
|
|
CompletedAt *time.Time
|
|
Error string
|
|
}
|
|
|
|
// Store persists run history to SQLite.
|
|
type Store struct {
|
|
db *sql.DB
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// New opens (or creates) the SQLite database and runs the schema migration.
|
|
func New(dsn string, logger *zap.Logger) (*Store, error) {
|
|
db, err := sql.Open("sqlite", dsn)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("store: open %s: %w", dsn, err)
|
|
}
|
|
db.SetMaxOpenConns(1) // SQLite is single-writer
|
|
if _, err := db.Exec(schema); err != nil {
|
|
_ = db.Close()
|
|
return nil, fmt.Errorf("store: migrate: %w", err)
|
|
}
|
|
if err := migrateRunsTable(db); err != nil {
|
|
_ = db.Close()
|
|
return nil, fmt.Errorf("store: migrate runs: %w", err)
|
|
}
|
|
logger.Info("store ready", zap.String("dsn", dsn))
|
|
return &Store{db: db, logger: logger}, nil
|
|
}
|
|
|
|
func migrateRunsTable(db *sql.DB) error {
|
|
cols, err := tableColumns(db, "runs")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !cols["session_id"] {
|
|
if _, err := db.Exec(`ALTER TABLE runs ADD COLUMN session_id TEXT NOT NULL DEFAULT 'default'`); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !cols["background"] {
|
|
if _, err := db.Exec(`ALTER TABLE runs ADD COLUMN background INTEGER NOT NULL DEFAULT 0`); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func tableColumns(db *sql.DB, table string) (map[string]bool, error) {
|
|
rows, err := db.Query(fmt.Sprintf("PRAGMA table_info(%s)", table))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
cols := make(map[string]bool)
|
|
for rows.Next() {
|
|
var (
|
|
cid int
|
|
name string
|
|
ctype string
|
|
notnull int
|
|
dfltValue sql.NullString
|
|
pk int
|
|
)
|
|
if err := rows.Scan(&cid, &name, &ctype, ¬null, &dfltValue, &pk); err != nil {
|
|
return nil, err
|
|
}
|
|
cols[name] = true
|
|
}
|
|
return cols, rows.Err()
|
|
}
|
|
|
|
// Close closes the underlying database connection.
|
|
func (s *Store) Close() error {
|
|
return s.db.Close()
|
|
}
|
|
|
|
// InsertRun records a new run.
|
|
func (s *Store) InsertRun(ctx context.Context, r RunRecord) error {
|
|
sessionID := r.SessionID
|
|
if sessionID == "" {
|
|
sessionID = "default"
|
|
}
|
|
bg := 0
|
|
if r.Background {
|
|
bg = 1
|
|
}
|
|
_, err := s.db.ExecContext(ctx,
|
|
`INSERT INTO runs (run_id, adapter, model, session_id, background, status, created_at) VALUES (?,?,?,?,?,?,?)`,
|
|
r.RunID, r.Adapter, r.Model, sessionID, bg, r.Status, r.CreatedAt.UTC(),
|
|
)
|
|
return err
|
|
}
|
|
|
|
// CompleteRun marks a run as completed, failed, or cancelled.
|
|
func (s *Store) CompleteRun(ctx context.Context, runID, status, errMsg string) error {
|
|
_, err := s.db.ExecContext(ctx,
|
|
`UPDATE runs SET status=?, completed_at=?, error=? WHERE run_id=?`,
|
|
status, time.Now().UTC(), errMsg, runID,
|
|
)
|
|
return err
|
|
}
|
|
|
|
// GetRun retrieves a run record by ID.
|
|
func (s *Store) GetRun(ctx context.Context, runID string) (*RunRecord, error) {
|
|
row := s.db.QueryRowContext(ctx,
|
|
`SELECT run_id, adapter, model, session_id, background, status, created_at, completed_at, error FROM runs WHERE run_id=?`,
|
|
runID,
|
|
)
|
|
var r RunRecord
|
|
var bg int
|
|
var completedAt sql.NullTime
|
|
var errMsg sql.NullString
|
|
if err := row.Scan(&r.RunID, &r.Adapter, &r.Model, &r.SessionID, &bg, &r.Status, &r.CreatedAt, &completedAt, &errMsg); err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
r.Background = bg != 0
|
|
if completedAt.Valid {
|
|
r.CompletedAt = &completedAt.Time
|
|
}
|
|
r.Error = errMsg.String
|
|
return &r, nil
|
|
}
|