vibe-proxy/backend/internal/runtime/executor/helps/claude_diagnostics.go
2026-08-24 00:10:41 +02:00

137 lines
4.4 KiB
Go

package helps
import (
"crypto/sha256"
"encoding/hex"
"sort"
"strings"
"sync"
"time"
)
const (
claudeDiagnosticsTTL = time.Hour
claudeDiagnosticsCleanupPeriod = 15 * time.Minute
claudeDiagnosticsMaxEntries = 4096
claudeDiagnosticsEvictBatchSize = 256
)
type claudeDiagnosticsEntry struct {
previousMessageID string
minimumSequence uint64
committedSequence uint64
lastAccess uint64
expiresAt time.Time
}
var claudeDiagnosticsState = struct {
sync.Mutex
entries map[string]claudeDiagnosticsEntry
lastCleanup time.Time
nextSequence uint64
nextAccess uint64
}{entries: make(map[string]claudeDiagnosticsEntry)}
// BeginClaudeDiagnostics starts one request generation for a stable credential
// identity and Claude conversation. It returns the last successfully completed
// upstream message ID, if any. Only a SHA-256 digest of the credential identity
// and session is retained as the cache key, so access-token rotation does not
// interrupt continuity.
func BeginClaudeDiagnostics(credentialIdentity, sessionID string) (key string, sequence uint64, previousMessageID string) {
credentialIdentity = strings.TrimSpace(credentialIdentity)
sessionID = strings.TrimSpace(sessionID)
if credentialIdentity == "" || sessionID == "" {
return "", 0, ""
}
digest := sha256.Sum256([]byte(credentialIdentity + "\x00" + sessionID))
key = hex.EncodeToString(digest[:])
now := time.Now()
claudeDiagnosticsState.Lock()
defer claudeDiagnosticsState.Unlock()
cleanupClaudeDiagnosticsLocked(now)
entry, found := claudeDiagnosticsState.entries[key]
newGeneration := !found || (!entry.expiresAt.IsZero() && now.After(entry.expiresAt))
if newGeneration && !found {
evictClaudeDiagnosticsLocked()
}
claudeDiagnosticsState.nextSequence++
sequence = claudeDiagnosticsState.nextSequence
if newGeneration {
entry = claudeDiagnosticsEntry{minimumSequence: sequence}
}
claudeDiagnosticsState.nextAccess++
entry.lastAccess = claudeDiagnosticsState.nextAccess
entry.expiresAt = now.Add(claudeDiagnosticsTTL)
claudeDiagnosticsState.entries[key] = entry
return key, sequence, entry.previousMessageID
}
// CommitClaudeDiagnostics advances continuity only after a response completes.
// A response from an older concurrently-started request cannot overwrite a
// newer committed generation, including after TTL expiry or capacity eviction.
func CommitClaudeDiagnostics(key string, sequence uint64, messageID string) {
key = strings.TrimSpace(key)
messageID = strings.TrimSpace(messageID)
if key == "" || sequence == 0 || messageID == "" {
return
}
now := time.Now()
claudeDiagnosticsState.Lock()
defer claudeDiagnosticsState.Unlock()
entry, ok := claudeDiagnosticsState.entries[key]
if !ok || sequence < entry.minimumSequence || sequence < entry.committedSequence {
return
}
claudeDiagnosticsState.nextAccess++
entry.previousMessageID = messageID
entry.committedSequence = sequence
entry.lastAccess = claudeDiagnosticsState.nextAccess
entry.expiresAt = now.Add(claudeDiagnosticsTTL)
claudeDiagnosticsState.entries[key] = entry
}
func cleanupClaudeDiagnosticsLocked(now time.Time) {
if !claudeDiagnosticsState.lastCleanup.IsZero() && now.Sub(claudeDiagnosticsState.lastCleanup) < claudeDiagnosticsCleanupPeriod {
return
}
for key, entry := range claudeDiagnosticsState.entries {
if !entry.expiresAt.IsZero() && now.After(entry.expiresAt) {
delete(claudeDiagnosticsState.entries, key)
}
}
claudeDiagnosticsState.lastCleanup = now
}
func evictClaudeDiagnosticsLocked() {
if len(claudeDiagnosticsState.entries) < claudeDiagnosticsMaxEntries {
return
}
type candidate struct {
key string
lastAccess uint64
}
candidates := make([]candidate, 0, len(claudeDiagnosticsState.entries))
for key, entry := range claudeDiagnosticsState.entries {
candidates = append(candidates, candidate{key: key, lastAccess: entry.lastAccess})
}
sort.Slice(candidates, func(i, j int) bool {
return candidates[i].lastAccess < candidates[j].lastAccess
})
count := min(claudeDiagnosticsEvictBatchSize, len(candidates))
for _, candidate := range candidates[:count] {
delete(claudeDiagnosticsState.entries, candidate.key)
}
}
func resetClaudeDiagnosticsForTest() {
claudeDiagnosticsState.Lock()
defer claudeDiagnosticsState.Unlock()
claudeDiagnosticsState.entries = make(map[string]claudeDiagnosticsEntry)
claudeDiagnosticsState.lastCleanup = time.Time{}
claudeDiagnosticsState.nextSequence = 0
claudeDiagnosticsState.nextAccess = 0
}