opencode/internal/session/session.go

256 lines
6.8 KiB
Go
Raw Normal View History

2025-03-24 02:19:08 +08:00
package session
import (
"context"
2025-03-28 05:35:48 +08:00
"database/sql"
2025-05-12 21:43:34 +08:00
"fmt"
"sync"
"time"
2025-03-24 02:19:08 +08:00
"github.com/google/uuid"
2025-05-13 23:02:39 +08:00
"github.com/sst/opencode/internal/db"
"github.com/sst/opencode/internal/pubsub"
2025-03-24 02:19:08 +08:00
)
type Session struct {
2025-03-24 05:25:31 +08:00
ID string
2025-03-28 05:35:48 +08:00
ParentSessionID string
2025-03-24 05:25:31 +08:00
Title string
MessageCount int64
PromptTokens int64
CompletionTokens int64
Cost float64
Summary string
2025-05-13 23:45:58 +08:00
SummarizedAt time.Time
CreatedAt time.Time
UpdatedAt time.Time
2025-03-24 02:19:08 +08:00
}
2025-05-12 21:43:34 +08:00
const (
EventSessionCreated pubsub.EventType = "session_created"
EventSessionUpdated pubsub.EventType = "session_updated"
EventSessionDeleted pubsub.EventType = "session_deleted"
)
2025-03-24 02:19:08 +08:00
type Service interface {
2025-05-12 21:43:34 +08:00
pubsub.Subscriber[Session]
2025-04-13 19:17:17 +08:00
Create(ctx context.Context, title string) (Session, error)
CreateTaskSession(ctx context.Context, toolCallID, parentSessionID, title string) (Session, error)
Get(ctx context.Context, id string) (Session, error)
List(ctx context.Context) ([]Session, error)
2025-05-12 21:43:34 +08:00
Update(ctx context.Context, session Session) (Session, error)
2025-04-13 19:17:17 +08:00
Delete(ctx context.Context, id string) error
2025-03-24 02:19:08 +08:00
}
type service struct {
2025-05-12 21:43:34 +08:00
db *db.Queries
broker *pubsub.Broker[Session]
mu sync.RWMutex
}
var globalSessionService *service
func InitService(dbConn *sql.DB) error {
if globalSessionService != nil {
return fmt.Errorf("session service already initialized")
}
queries := db.New(dbConn)
broker := pubsub.NewBroker[Session]()
globalSessionService = &service{
db: queries,
broker: broker,
}
return nil
2025-03-24 02:19:08 +08:00
}
2025-05-12 21:43:34 +08:00
func GetService() Service {
if globalSessionService == nil {
panic("session service not initialized. Call session.InitService() first.")
}
return globalSessionService
}
2025-04-13 19:17:17 +08:00
func (s *service) Create(ctx context.Context, title string) (Session, error) {
2025-05-12 21:43:34 +08:00
s.mu.Lock()
defer s.mu.Unlock()
if title == "" {
title = "New Session - " + time.Now().Format("2006-01-02 15:04:05")
}
dbSessParams := db.CreateSessionParams{
2025-03-24 02:19:08 +08:00
ID: uuid.New().String(),
Title: title,
2025-05-12 21:43:34 +08:00
}
dbSession, err := s.db.CreateSession(ctx, dbSessParams)
2025-03-24 02:19:08 +08:00
if err != nil {
2025-05-12 21:43:34 +08:00
return Session{}, fmt.Errorf("db.CreateSession: %w", err)
2025-03-24 02:19:08 +08:00
}
2025-05-12 21:43:34 +08:00
2025-03-24 02:19:08 +08:00
session := s.fromDBItem(dbSession)
2025-05-12 21:43:34 +08:00
s.broker.Publish(EventSessionCreated, session)
2025-03-24 02:19:08 +08:00
return session, nil
}
2025-04-13 19:17:17 +08:00
func (s *service) CreateTaskSession(ctx context.Context, toolCallID, parentSessionID, title string) (Session, error) {
2025-05-12 21:43:34 +08:00
s.mu.Lock()
defer s.mu.Unlock()
if title == "" {
title = "Task Session - " + time.Now().Format("2006-01-02 15:04:05")
}
if toolCallID == "" {
toolCallID = uuid.New().String()
}
dbSessParams := db.CreateSessionParams{
2025-03-28 05:35:48 +08:00
ID: toolCallID,
2025-05-12 21:43:34 +08:00
ParentSessionID: sql.NullString{String: parentSessionID, Valid: parentSessionID != ""},
2025-03-28 05:35:48 +08:00
Title: title,
}
2025-05-12 21:43:34 +08:00
dbSession, err := s.db.CreateSession(ctx, dbSessParams)
if err != nil {
2025-05-12 21:43:34 +08:00
return Session{}, fmt.Errorf("db.CreateTaskSession: %w", err)
}
session := s.fromDBItem(dbSession)
2025-05-12 21:43:34 +08:00
s.broker.Publish(EventSessionCreated, session)
return session, nil
}
2025-05-12 21:43:34 +08:00
func (s *service) Get(ctx context.Context, id string) (Session, error) {
s.mu.RLock()
defer s.mu.RUnlock()
dbSession, err := s.db.GetSessionByID(ctx, id)
2025-03-24 02:19:08 +08:00
if err != nil {
2025-05-12 21:43:34 +08:00
if err == sql.ErrNoRows {
return Session{}, fmt.Errorf("session ID '%s' not found", id)
}
return Session{}, fmt.Errorf("db.GetSessionByID: %w", err)
2025-03-24 02:19:08 +08:00
}
2025-05-12 21:43:34 +08:00
return s.fromDBItem(dbSession), nil
2025-03-24 02:19:08 +08:00
}
2025-05-12 21:43:34 +08:00
func (s *service) List(ctx context.Context) ([]Session, error) {
s.mu.RLock()
defer s.mu.RUnlock()
dbSessions, err := s.db.ListSessions(ctx)
2025-03-24 02:19:08 +08:00
if err != nil {
2025-05-12 21:43:34 +08:00
return nil, fmt.Errorf("db.ListSessions: %w", err)
2025-03-24 02:19:08 +08:00
}
2025-05-12 21:43:34 +08:00
sessions := make([]Session, len(dbSessions))
for i, dbSess := range dbSessions {
sessions[i] = s.fromDBItem(dbSess)
}
return sessions, nil
2025-03-24 02:19:08 +08:00
}
2025-05-12 21:43:34 +08:00
func (s *service) Update(ctx context.Context, session Session) (Session, error) {
s.mu.Lock()
defer s.mu.Unlock()
2025-05-12 21:43:34 +08:00
if session.ID == "" {
return Session{}, fmt.Errorf("cannot update session with empty ID")
}
2025-05-14 00:07:34 +08:00
2025-05-12 21:43:34 +08:00
params := db.UpdateSessionParams{
2025-03-24 05:25:31 +08:00
ID: session.ID,
Title: session.Title,
PromptTokens: session.PromptTokens,
CompletionTokens: session.CompletionTokens,
Cost: session.Cost,
2025-05-12 21:43:34 +08:00
Summary: sql.NullString{String: session.Summary, Valid: session.Summary != ""},
2025-05-14 02:08:43 +08:00
SummarizedAt: sql.NullString{String: session.SummarizedAt.UTC().Format(time.RFC3339Nano), Valid: !session.SummarizedAt.IsZero()},
2025-05-12 21:43:34 +08:00
}
dbSession, err := s.db.UpdateSession(ctx, params)
2025-03-24 02:19:08 +08:00
if err != nil {
2025-05-12 21:43:34 +08:00
return Session{}, fmt.Errorf("db.UpdateSession: %w", err)
2025-03-24 02:19:08 +08:00
}
2025-05-12 21:43:34 +08:00
updatedSession := s.fromDBItem(dbSession)
s.broker.Publish(EventSessionUpdated, updatedSession)
return updatedSession, nil
2025-03-24 02:19:08 +08:00
}
2025-05-12 21:43:34 +08:00
func (s *service) Delete(ctx context.Context, id string) error {
s.mu.Lock()
dbSess, err := s.db.GetSessionByID(ctx, id)
2025-03-24 02:19:08 +08:00
if err != nil {
2025-05-12 21:43:34 +08:00
s.mu.Unlock()
if err == sql.ErrNoRows {
return fmt.Errorf("session ID '%s' not found for deletion", id)
}
return fmt.Errorf("db.GetSessionByID before delete: %w", err)
2025-03-24 02:19:08 +08:00
}
2025-05-12 21:43:34 +08:00
sessionToPublish := s.fromDBItem(dbSess)
s.mu.Unlock()
s.mu.Lock()
defer s.mu.Unlock()
err = s.db.DeleteSession(ctx, id)
if err != nil {
return fmt.Errorf("db.DeleteSession: %w", err)
2025-03-24 02:19:08 +08:00
}
2025-05-12 21:43:34 +08:00
s.broker.Publish(EventSessionDeleted, sessionToPublish)
return nil
}
func (s *service) Subscribe(ctx context.Context) <-chan pubsub.Event[Session] {
return s.broker.Subscribe(ctx)
2025-03-24 02:19:08 +08:00
}
2025-05-12 21:43:34 +08:00
func (s *service) fromDBItem(item db.Session) Session {
2025-05-13 23:45:58 +08:00
var summarizedAt time.Time
if item.SummarizedAt.Valid {
2025-05-14 02:08:43 +08:00
parsedTime, err := time.Parse(time.RFC3339Nano, item.SummarizedAt.String)
if err == nil {
summarizedAt = parsedTime
}
2025-05-13 23:45:58 +08:00
}
2025-05-14 00:07:34 +08:00
2025-05-14 02:08:43 +08:00
createdAt, _ := time.Parse(time.RFC3339Nano, item.CreatedAt)
updatedAt, _ := time.Parse(time.RFC3339Nano, item.UpdatedAt)
2025-03-24 02:19:08 +08:00
return Session{
2025-03-24 05:25:31 +08:00
ID: item.ID,
2025-03-28 05:35:48 +08:00
ParentSessionID: item.ParentSessionID.String,
2025-03-24 05:25:31 +08:00
Title: item.Title,
MessageCount: item.MessageCount,
PromptTokens: item.PromptTokens,
CompletionTokens: item.CompletionTokens,
Cost: item.Cost,
Summary: item.Summary.String,
2025-05-13 23:45:58 +08:00
SummarizedAt: summarizedAt,
2025-05-14 02:08:43 +08:00
CreatedAt: createdAt,
UpdatedAt: updatedAt,
2025-03-24 02:19:08 +08:00
}
}
2025-05-12 21:43:34 +08:00
func Create(ctx context.Context, title string) (Session, error) {
return GetService().Create(ctx, title)
}
func CreateTaskSession(ctx context.Context, toolCallID, parentSessionID, title string) (Session, error) {
return GetService().CreateTaskSession(ctx, toolCallID, parentSessionID, title)
}
func Get(ctx context.Context, id string) (Session, error) {
return GetService().Get(ctx, id)
}
func List(ctx context.Context) ([]Session, error) {
return GetService().List(ctx)
}
func Update(ctx context.Context, session Session) (Session, error) {
return GetService().Update(ctx, session)
}
func Delete(ctx context.Context, id string) error {
return GetService().Delete(ctx, id)
}
func Subscribe(ctx context.Context) <-chan pubsub.Event[Session] {
return GetService().Subscribe(ctx)
2025-03-24 02:19:08 +08:00
}