137 lines
4.4 KiB
Go
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
|
|
}
|