ForkSession creates a new session whose history is a deep copy of the parent session up to (but excluding) the Nth user message, with a fork-numbered title (" (fork N)"). userMessageOrdinal counts user-role messages in the flat list returned by Session.GetAllMessages. The read-then-write of
(ctx context.Context, sessionID string, userMessageOrdinal int)
| 401 | // to keep two concurrent forks on the same parent from racing on the |
| 402 | // auto-numbered title. |
| 403 | func (sm *SessionManager) ForkSession(ctx context.Context, sessionID string, userMessageOrdinal int) (*session.Session, error) { |
| 404 | sm.mux.Lock() |
| 405 | defer sm.mux.Unlock() |
| 406 | |
| 407 | parent, err := sm.sessionStore.GetSession(ctx, sessionID) |
| 408 | if err != nil { |
| 409 | return nil, err |
| 410 | } |
| 411 | |
| 412 | itemIndex, err := userMessageOrdinalToItemIndex(parent, userMessageOrdinal) |
| 413 | if err != nil { |
| 414 | return nil, err |
| 415 | } |
| 416 | |
| 417 | forked, err := session.ForkSession(parent, itemIndex) |
| 418 | if err != nil { |
| 419 | return nil, err |
| 420 | } |
| 421 | |
| 422 | // Sibling-aware title so repeated forks of the same parent get |
| 423 | // (fork 1), (fork 2), … instead of colliding on (fork 1). |
| 424 | siblings, err := sm.sessionStore.GetSessions(ctx) |
| 425 | if err != nil { |
| 426 | return nil, err |
| 427 | } |
| 428 | siblingTitles := make([]string, 0, len(siblings)) |
| 429 | for _, s := range siblings { |
| 430 | siblingTitles = append(siblingTitles, s.Title) |
| 431 | } |
| 432 | forked.Title = session.NextForkTitle(parent.Title, siblingTitles) |
| 433 | |
| 434 | if err := sm.sessionStore.AddSession(ctx, forked); err != nil { |
| 435 | return nil, err |
| 436 | } |
| 437 | return forked, nil |
| 438 | } |
| 439 | |
| 440 | // userMessageOrdinalToItemIndex maps a 0-based user-message ordinal |
| 441 | // into an index in the parent's Session.Messages Item slice. Returns |