Files
Duy /zuey/ 43049d3b3f feat(http): unblock CLI P6 backend APIs (#44)
* feat(api): unblock trace follow and provider reconnect

* test(cron): stabilize run log execution test

* feat(http): add CLI P6 backend endpoints
2026-05-22 20:59:47 +07:00

108 lines
3.4 KiB
Go

//go:build sqlite || sqliteonly
package sqlitestore
import (
"context"
"encoding/json"
"maps"
"strconv"
"time"
"github.com/google/uuid"
"github.com/nextlevelbuilder/goclaw/internal/providers"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
func (s *SQLiteSessionStore) BranchSession(ctx context.Context, sourceKey string, opts store.SessionBranchOpts) (*store.SessionData, int, error) {
if opts.NewKey == "" || opts.UpToIndex < 0 {
return nil, 0, store.ErrInvalidSessionBranch
}
if s.Get(ctx, opts.NewKey) != nil {
return nil, 0, store.ErrSessionAlreadyExists
}
source := s.Get(ctx, sourceKey)
if source == nil {
return nil, 0, store.ErrSessionNotFound
}
s.mu.RLock()
sourceSnapshot := *source
sourceMsgs := make([]providers.Message, len(source.Messages))
copy(sourceMsgs, source.Messages)
sourceSnapshot.Messages = sourceMsgs
sourceMeta := make(map[string]string, len(source.Metadata))
maps.Copy(sourceMeta, source.Metadata)
sourceSnapshot.Metadata = sourceMeta
s.mu.RUnlock()
if opts.UpToIndex > len(sourceSnapshot.Messages) {
return nil, 0, store.ErrInvalidSessionBranch
}
now := time.Now().UTC()
copied := make([]providers.Message, opts.UpToIndex)
copy(copied, sourceSnapshot.Messages[:opts.UpToIndex])
meta := make(map[string]string, len(sourceSnapshot.Metadata)+len(opts.Metadata)+3)
maps.Copy(meta, sourceSnapshot.Metadata)
maps.Copy(meta, opts.Metadata)
meta["branched_from"] = sourceKey
meta["branched_from_index"] = strconv.Itoa(opts.UpToIndex)
meta["branched_at"] = now.Format(time.RFC3339Nano)
branch := &store.SessionData{
Key: opts.NewKey,
Messages: copied,
Summary: sourceSnapshot.Summary,
Created: now,
Updated: now,
AgentUUID: sourceSnapshot.AgentUUID,
UserID: sourceSnapshot.UserID,
Model: sourceSnapshot.Model,
Provider: sourceSnapshot.Provider,
Channel: "branch",
Label: opts.Label,
Metadata: meta,
ContextWindow: sourceSnapshot.ContextWindow,
}
if sourceSnapshot.TeamID != nil {
teamID := *sourceSnapshot.TeamID
branch.TeamID = &teamID
}
msgsJSON, _ := json.Marshal(branch.Messages)
metaJSON := []byte("{}")
if len(branch.Metadata) > 0 {
metaJSON, _ = json.Marshal(branch.Metadata)
}
res, err := s.db.ExecContext(ctx,
`INSERT INTO sessions (id, session_key, messages, summary, model, provider, channel,
input_tokens, output_tokens, compaction_count,
memory_flush_compaction_count, memory_flush_at,
label, spawned_by, spawn_depth, agent_id, user_id, metadata, updated_at, team_id, tenant_id, created_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
ON CONFLICT(session_key, tenant_id) DO NOTHING`,
uuid.Must(uuid.NewV7()), branch.Key, msgsJSON,
nilStr(branch.Summary), nilStr(branch.Model), nilStr(branch.Provider), nilStr(branch.Channel),
branch.InputTokens, branch.OutputTokens, branch.CompactionCount,
branch.MemoryFlushCompactionCount, branch.MemoryFlushAt,
nilStr(branch.Label), nilStr(branch.SpawnedBy), branch.SpawnDepth,
nilSessionUUID(branch.AgentUUID), nilStr(branch.UserID), metaJSON, branch.Updated,
branch.TeamID, tenantIDForInsert(ctx), branch.Created,
)
if err != nil {
return nil, 0, err
}
if n, _ := res.RowsAffected(); n == 0 {
return nil, 0, store.ErrSessionAlreadyExists
}
s.mu.Lock()
s.cache[sessionCacheKey(ctx, opts.NewKey)] = branch
s.mu.Unlock()
return branch, len(copied), nil
}