extractSessionIDs returns (primaryID, fallbackID) for session affinity. primaryID: full hash including assistant response (stable after first turn) fallbackID: short hash without assistant (used to inherit binding from first turn)
(headers http.Header, payload []byte, metadata map[string]any)
| 520 | // primaryID: full hash including assistant response (stable after first turn) |
| 521 | // fallbackID: short hash without assistant (used to inherit binding from first turn) |
| 522 | func extractSessionIDs(headers http.Header, payload []byte, metadata map[string]any) (string, string) { |
| 523 | // 1. metadata.user_id with Claude Code session format (highest priority) |
| 524 | if len(payload) > 0 { |
| 525 | userID := gjson.GetBytes(payload, "metadata.user_id").String() |
| 526 | if userID != "" { |
| 527 | // Old format: user_{hash}_account__session_{uuid} |
| 528 | if matches := sessionPattern.FindStringSubmatch(userID); len(matches) >= 2 { |
| 529 | id := "claude:" + matches[1] |
| 530 | return id, "" |
| 531 | } |
| 532 | // New format: JSON object with session_id field |
| 533 | // e.g. {"device_id":"...","account_uuid":"...","session_id":"uuid"} |
| 534 | if len(userID) > 0 && userID[0] == '{' { |
| 535 | if sid := gjson.Get(userID, "session_id").String(); sid != "" { |
| 536 | return "claude:" + sid, "" |
| 537 | } |
| 538 | } |
| 539 | } |
| 540 | } |
| 541 | |
| 542 | // 2. X-Session-ID header |
| 543 | if headers != nil { |
| 544 | if sid := headers.Get("X-Session-ID"); sid != "" { |
| 545 | return "header:" + sid, "" |
| 546 | } |
| 547 | } |
| 548 | |
| 549 | // 3. Session_id header (Codex) |
| 550 | if headers != nil { |
| 551 | if sid := headers.Get("Session-Id"); sid != "" { |
| 552 | return "codex:" + sid, "" |
| 553 | } |
| 554 | if sid := headers.Get("Session_id"); sid != "" { |
| 555 | return "codex:" + sid, "" |
| 556 | } |
| 557 | } |
| 558 | |
| 559 | // 4. X-Client-Request-Id header (PI) |
| 560 | if headers != nil { |
| 561 | if rid := headers.Get("X-Client-Request-Id"); rid != "" { |
| 562 | return "clientreq:" + rid, "" |
| 563 | } |
| 564 | } |
| 565 | |
| 566 | if len(payload) == 0 { |
| 567 | return "", "" |
| 568 | } |
| 569 | |
| 570 | // 6. metadata.user_id (non-Claude Code format) |
| 571 | userID := gjson.GetBytes(payload, "metadata.user_id").String() |
| 572 | if userID != "" { |
| 573 | return "user:" + userID, "" |
| 574 | } |
| 575 | |
| 576 | // 7. conversation_id field |
| 577 | if convID := gjson.GetBytes(payload, "conversation_id").String(); convID != "" { |
| 578 | return "conv:" + convID, "" |
| 579 | } |
no test coverage detected