Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
482
backend/internal/cache/claude_thinking_replay_cache.go
vendored
Normal file
482
backend/internal/cache/claude_thinking_replay_cache.go
vendored
Normal file
|
|
@ -0,0 +1,482 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
homekv "github.com/router-for-me/CLIProxyAPI/v7/internal/home"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
// ClaudeThinkingReplayCacheTTL limits how long signed assistant turns stay replayable.
|
||||
ClaudeThinkingReplayCacheTTL = 1 * time.Hour
|
||||
|
||||
// ClaudeThinkingReplayCacheMaxEntries bounds process memory used by Claude replay continuity.
|
||||
ClaudeThinkingReplayCacheMaxEntries = 10240
|
||||
|
||||
// ClaudeThinkingReplayCacheEvictBatchSize leaves headroom after reaching capacity.
|
||||
ClaudeThinkingReplayCacheEvictBatchSize = 128
|
||||
|
||||
// ClaudeThinkingReplayCacheMaxBytesPerSession bounds all cached assistant turns for one session.
|
||||
ClaudeThinkingReplayCacheMaxBytesPerSession = 8 << 20
|
||||
|
||||
// ClaudeThinkingReplayCacheMaxTurnsPerSession bounds the number of assistant turns per session.
|
||||
ClaudeThinkingReplayCacheMaxTurnsPerSession = 64
|
||||
|
||||
// ClaudeThinkingReplayCacheMaxBlocksPerTurn prevents pathological content arrays.
|
||||
ClaudeThinkingReplayCacheMaxBlocksPerTurn = 512
|
||||
|
||||
// ClaudeThinkingReplayCacheMaxTotalBytes bounds aggregate in-process Claude replay content.
|
||||
ClaudeThinkingReplayCacheMaxTotalBytes = 256 << 20
|
||||
|
||||
claudeThinkingReplayCacheMaxSerializedBytes = ClaudeThinkingReplayCacheMaxBytesPerSession + 1024
|
||||
)
|
||||
|
||||
type claudeThinkingReplayEntry struct {
|
||||
Contents [][]byte
|
||||
Timestamp time.Time
|
||||
Generation string
|
||||
Deleted bool
|
||||
}
|
||||
|
||||
// ClaudeThinkingReplaySnapshot identifies the exact replay generation read for one request.
|
||||
type ClaudeThinkingReplaySnapshot = KimiThinkingReplaySnapshot
|
||||
|
||||
type claudeThinkingReplayHomeValue struct {
|
||||
Generation string `json:"generation"`
|
||||
Deleted bool `json:"deleted,omitempty"`
|
||||
Contents []json.RawMessage `json:"contents,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
claudeThinkingReplayMu sync.Mutex
|
||||
claudeThinkingReplayEntries = make(map[string]claudeThinkingReplayEntry)
|
||||
claudeThinkingReplayTotalBytes int
|
||||
)
|
||||
|
||||
var currentClaudeThinkingReplayKVClient = func() (kimiThinkingReplayKVClient, bool, error) {
|
||||
return homekv.CurrentKVClient()
|
||||
}
|
||||
|
||||
// CacheClaudeThinkingReplayBestEffort stores one complete signed assistant content array.
|
||||
func CacheClaudeThinkingReplayBestEffort(ctx context.Context, modelFamily, sessionKey string, content []byte) bool {
|
||||
key := claudeThinkingReplayCacheKey(modelFamily, sessionKey)
|
||||
if key == "" || !validClaudeThinkingReplayContent(content) {
|
||||
return false
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
contents := [][]byte{append([]byte(nil), content...)}
|
||||
generation := uuid.NewString()
|
||||
if client, homeMode, errClient := currentClaudeThinkingReplayKVClient(); homeMode {
|
||||
if errClient != nil {
|
||||
log.Errorf("home kv best-effort Claude thinking replay set failed: %v", errClient)
|
||||
return false
|
||||
}
|
||||
raw, errMarshal := marshalClaudeThinkingReplayHomeValue(generation, false, contents)
|
||||
if errMarshal != nil {
|
||||
log.Errorf("home kv best-effort Claude thinking replay set failed: %v", errMarshal)
|
||||
return false
|
||||
}
|
||||
written, errSet := client.KVSet(ctx, claudeThinkingReplayKVKey(modelFamily, sessionKey), raw, homekv.KVSetOptions{EX: ClaudeThinkingReplayCacheTTL})
|
||||
if errSet != nil {
|
||||
log.Errorf("home kv best-effort Claude thinking replay set failed: %v", errSet)
|
||||
return false
|
||||
}
|
||||
return written
|
||||
}
|
||||
|
||||
storeClaudeThinkingReplayLocal(key, contents, generation, false, time.Now())
|
||||
return true
|
||||
}
|
||||
|
||||
// GetClaudeThinkingReplayRequired retrieves all cached assistant turns for request-time replay.
|
||||
func GetClaudeThinkingReplayRequired(ctx context.Context, modelFamily, sessionKey string) ([][]byte, bool, error) {
|
||||
contents, _, found, errGet := GetClaudeThinkingReplayWithSnapshotRequired(ctx, modelFamily, sessionKey)
|
||||
return contents, found, errGet
|
||||
}
|
||||
|
||||
// GetClaudeThinkingReplayWithSnapshotRequired retrieves replay content and the exact cache state read.
|
||||
func GetClaudeThinkingReplayWithSnapshotRequired(ctx context.Context, modelFamily, sessionKey string) ([][]byte, ClaudeThinkingReplaySnapshot, bool, error) {
|
||||
key := claudeThinkingReplayCacheKey(modelFamily, sessionKey)
|
||||
if key == "" {
|
||||
return nil, ClaudeThinkingReplaySnapshot{}, false, nil
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
client, homeMode, errClient := currentClaudeThinkingReplayKVClient()
|
||||
if homeMode {
|
||||
if errClient != nil {
|
||||
return nil, ClaudeThinkingReplaySnapshot{loaded: true}, false, errClient
|
||||
}
|
||||
kvKey := claudeThinkingReplayKVKey(modelFamily, sessionKey)
|
||||
raw, errRead := readOrReserveClaudeThinkingReplayHomeValue(ctx, client, kvKey)
|
||||
if errRead != nil {
|
||||
return nil, ClaudeThinkingReplaySnapshot{loaded: true}, false, errRead
|
||||
}
|
||||
snapshot := ClaudeThinkingReplaySnapshot{raw: append([]byte(nil), raw...), loaded: true, found: true}
|
||||
contents, generation, deleted, okDecode := decodeClaudeThinkingReplayHomeValue(raw)
|
||||
if !okDecode {
|
||||
return nil, snapshot, false, fmt.Errorf("invalid Claude thinking replay content")
|
||||
}
|
||||
snapshot.generation = generation
|
||||
if _, errExpire := client.KVExpire(ctx, kvKey, ClaudeThinkingReplayCacheTTL); errExpire != nil {
|
||||
log.Warnf("home kv Claude thinking replay expire failed: %v", errExpire)
|
||||
}
|
||||
if deleted {
|
||||
return nil, snapshot, false, nil
|
||||
}
|
||||
return cloneClaudeThinkingReplayContents(contents), snapshot, len(contents) > 0, nil
|
||||
}
|
||||
|
||||
cacheCleanupOnce.Do(startCacheCleanup)
|
||||
now := time.Now()
|
||||
claudeThinkingReplayMu.Lock()
|
||||
defer claudeThinkingReplayMu.Unlock()
|
||||
entry, ok := claudeThinkingReplayEntries[key]
|
||||
if !ok || now.Sub(entry.Timestamp) > ClaudeThinkingReplayCacheTTL {
|
||||
if ok {
|
||||
claudeThinkingReplayTotalBytes -= claudeThinkingReplayEntryBytes(entry.Contents)
|
||||
delete(claudeThinkingReplayEntries, key)
|
||||
}
|
||||
entry = reserveClaudeThinkingReplayLocalLocked(key, now)
|
||||
}
|
||||
entry.Timestamp = now
|
||||
claudeThinkingReplayEntries[key] = entry
|
||||
snapshot := ClaudeThinkingReplaySnapshot{generation: entry.Generation, loaded: true, found: true}
|
||||
if entry.Deleted {
|
||||
return nil, snapshot, false, nil
|
||||
}
|
||||
return cloneClaudeThinkingReplayContents(entry.Contents), snapshot, len(entry.Contents) > 0, nil
|
||||
}
|
||||
|
||||
// ReplaceClaudeThinkingReplayIfUnchanged appends a completed assistant turn only if the request snapshot is current.
|
||||
func ReplaceClaudeThinkingReplayIfUnchanged(ctx context.Context, modelFamily, sessionKey string, snapshot ClaudeThinkingReplaySnapshot, content []byte) (bool, error) {
|
||||
key := claudeThinkingReplayCacheKey(modelFamily, sessionKey)
|
||||
if key == "" || !validClaudeThinkingReplayContent(content) {
|
||||
return false, nil
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if !snapshot.loaded {
|
||||
return CacheClaudeThinkingReplayBestEffort(ctx, modelFamily, sessionKey, content), nil
|
||||
}
|
||||
client, homeMode, errClient := currentClaudeThinkingReplayKVClient()
|
||||
if homeMode {
|
||||
if errClient != nil {
|
||||
return false, errClient
|
||||
}
|
||||
contents, _, deleted, okDecode := decodeClaudeThinkingReplayHomeValue(snapshot.raw)
|
||||
if !okDecode {
|
||||
return false, fmt.Errorf("invalid Claude thinking replay snapshot")
|
||||
}
|
||||
if deleted {
|
||||
contents = nil
|
||||
}
|
||||
contents = appendClaudeThinkingReplayContent(contents, content)
|
||||
generation := uuid.NewString()
|
||||
raw, errMarshal := marshalClaudeThinkingReplayHomeValue(generation, false, contents)
|
||||
if errMarshal != nil {
|
||||
return false, errMarshal
|
||||
}
|
||||
return client.KVCompareAndSwap(ctx, claudeThinkingReplayKVKey(modelFamily, sessionKey), snapshot.raw, snapshot.found, raw, ClaudeThinkingReplayCacheTTL)
|
||||
}
|
||||
|
||||
claudeThinkingReplayMu.Lock()
|
||||
defer claudeThinkingReplayMu.Unlock()
|
||||
entry, found := claudeThinkingReplayEntries[key]
|
||||
if found != snapshot.found || (found && entry.Generation != snapshot.generation) {
|
||||
return false, nil
|
||||
}
|
||||
contents := appendClaudeThinkingReplayContent(entry.Contents, content)
|
||||
claudeThinkingReplayTotalBytes -= claudeThinkingReplayEntryBytes(entry.Contents)
|
||||
claudeThinkingReplayTotalBytes += claudeThinkingReplayEntryBytes(contents)
|
||||
claudeThinkingReplayEntries[key] = claudeThinkingReplayEntry{
|
||||
Contents: contents,
|
||||
Timestamp: time.Now(),
|
||||
Generation: uuid.NewString(),
|
||||
}
|
||||
enforceClaudeThinkingReplayLimitsLocked()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// DeleteClaudeThinkingReplayIfUnchanged clears replay state only if the request snapshot is current.
|
||||
func DeleteClaudeThinkingReplayIfUnchanged(ctx context.Context, modelFamily, sessionKey string, snapshot ClaudeThinkingReplaySnapshot) (bool, error) {
|
||||
key := claudeThinkingReplayCacheKey(modelFamily, sessionKey)
|
||||
if key == "" {
|
||||
return false, nil
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if !snapshot.loaded {
|
||||
return true, DeleteClaudeThinkingReplayRequired(ctx, modelFamily, sessionKey)
|
||||
}
|
||||
generation := uuid.NewString()
|
||||
client, homeMode, errClient := currentClaudeThinkingReplayKVClient()
|
||||
if homeMode {
|
||||
if errClient != nil {
|
||||
return false, errClient
|
||||
}
|
||||
tombstone, errMarshal := marshalClaudeThinkingReplayHomeValue(generation, true, nil)
|
||||
if errMarshal != nil {
|
||||
return false, errMarshal
|
||||
}
|
||||
return client.KVCompareAndSwap(ctx, claudeThinkingReplayKVKey(modelFamily, sessionKey), snapshot.raw, snapshot.found, tombstone, ClaudeThinkingReplayCacheTTL)
|
||||
}
|
||||
|
||||
claudeThinkingReplayMu.Lock()
|
||||
defer claudeThinkingReplayMu.Unlock()
|
||||
entry, found := claudeThinkingReplayEntries[key]
|
||||
if found != snapshot.found || (found && entry.Generation != snapshot.generation) {
|
||||
return false, nil
|
||||
}
|
||||
claudeThinkingReplayTotalBytes -= claudeThinkingReplayEntryBytes(entry.Contents)
|
||||
claudeThinkingReplayEntries[key] = claudeThinkingReplayEntry{Timestamp: time.Now(), Generation: generation, Deleted: true}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// DeleteClaudeThinkingReplayRequired removes stale replay state unconditionally.
|
||||
func DeleteClaudeThinkingReplayRequired(ctx context.Context, modelFamily, sessionKey string) error {
|
||||
key := claudeThinkingReplayCacheKey(modelFamily, sessionKey)
|
||||
if key == "" {
|
||||
return nil
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
client, homeMode, errClient := currentClaudeThinkingReplayKVClient()
|
||||
if homeMode {
|
||||
if errClient != nil {
|
||||
return errClient
|
||||
}
|
||||
_, errDelete := client.KVDel(ctx, claudeThinkingReplayKVKey(modelFamily, sessionKey))
|
||||
return errDelete
|
||||
}
|
||||
claudeThinkingReplayMu.Lock()
|
||||
if entry, found := claudeThinkingReplayEntries[key]; found {
|
||||
claudeThinkingReplayTotalBytes -= claudeThinkingReplayEntryBytes(entry.Contents)
|
||||
delete(claudeThinkingReplayEntries, key)
|
||||
}
|
||||
claudeThinkingReplayMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearClaudeThinkingReplayCache clears only Claude replay state.
|
||||
func ClearClaudeThinkingReplayCache() {
|
||||
claudeThinkingReplayMu.Lock()
|
||||
claudeThinkingReplayEntries = make(map[string]claudeThinkingReplayEntry)
|
||||
claudeThinkingReplayTotalBytes = 0
|
||||
claudeThinkingReplayMu.Unlock()
|
||||
}
|
||||
|
||||
func readOrReserveClaudeThinkingReplayHomeValue(ctx context.Context, client kimiThinkingReplayKVClient, key string) ([]byte, error) {
|
||||
for attempt := 0; attempt < 4; attempt++ {
|
||||
raw, found, errGet := client.KVGet(ctx, key)
|
||||
if errGet != nil {
|
||||
return nil, errGet
|
||||
}
|
||||
if found {
|
||||
if len(raw) > claudeThinkingReplayCacheMaxSerializedBytes {
|
||||
return nil, fmt.Errorf("Claude thinking replay value exceeds size limit")
|
||||
}
|
||||
return raw, nil
|
||||
}
|
||||
tombstone, errMarshal := marshalClaudeThinkingReplayHomeValue(uuid.NewString(), true, nil)
|
||||
if errMarshal != nil {
|
||||
return nil, errMarshal
|
||||
}
|
||||
swapped, errReserve := client.KVCompareAndSwap(ctx, key, nil, false, tombstone, ClaudeThinkingReplayCacheTTL)
|
||||
if errReserve != nil {
|
||||
return nil, errReserve
|
||||
}
|
||||
if swapped {
|
||||
return tombstone, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("could not reserve absent Claude thinking replay state")
|
||||
}
|
||||
|
||||
func marshalClaudeThinkingReplayHomeValue(generation string, deleted bool, contents [][]byte) ([]byte, error) {
|
||||
value := claudeThinkingReplayHomeValue{Generation: generation, Deleted: deleted}
|
||||
if !deleted {
|
||||
value.Contents = make([]json.RawMessage, 0, len(contents))
|
||||
for _, content := range contents {
|
||||
value.Contents = append(value.Contents, json.RawMessage(append([]byte(nil), content...)))
|
||||
}
|
||||
}
|
||||
return json.Marshal(value)
|
||||
}
|
||||
|
||||
func decodeClaudeThinkingReplayHomeValue(raw []byte) ([][]byte, string, bool, bool) {
|
||||
if len(raw) == 0 || len(raw) > claudeThinkingReplayCacheMaxSerializedBytes || !gjson.ValidBytes(raw) {
|
||||
return nil, "", false, false
|
||||
}
|
||||
var value claudeThinkingReplayHomeValue
|
||||
if errUnmarshal := json.Unmarshal(raw, &value); errUnmarshal != nil || strings.TrimSpace(value.Generation) == "" {
|
||||
return nil, "", false, false
|
||||
}
|
||||
if value.Deleted {
|
||||
return nil, value.Generation, true, true
|
||||
}
|
||||
contents := make([][]byte, 0, len(value.Contents))
|
||||
for _, content := range value.Contents {
|
||||
if !validClaudeThinkingReplayContent(content) {
|
||||
return nil, "", false, false
|
||||
}
|
||||
contents = append(contents, append([]byte(nil), content...))
|
||||
}
|
||||
if len(contents) == 0 {
|
||||
return nil, "", false, false
|
||||
}
|
||||
return contents, value.Generation, false, true
|
||||
}
|
||||
|
||||
func reserveClaudeThinkingReplayLocalLocked(key string, now time.Time) claudeThinkingReplayEntry {
|
||||
entry := claudeThinkingReplayEntry{Timestamp: now, Generation: uuid.NewString(), Deleted: true}
|
||||
claudeThinkingReplayEntries[key] = entry
|
||||
enforceClaudeThinkingReplayLimitsLocked()
|
||||
return entry
|
||||
}
|
||||
|
||||
func storeClaudeThinkingReplayLocal(key string, contents [][]byte, generation string, deleted bool, now time.Time) {
|
||||
cacheCleanupOnce.Do(startCacheCleanup)
|
||||
claudeThinkingReplayMu.Lock()
|
||||
defer claudeThinkingReplayMu.Unlock()
|
||||
if previous, found := claudeThinkingReplayEntries[key]; found {
|
||||
claudeThinkingReplayTotalBytes -= claudeThinkingReplayEntryBytes(previous.Contents)
|
||||
}
|
||||
cloned := cloneClaudeThinkingReplayContents(contents)
|
||||
claudeThinkingReplayTotalBytes += claudeThinkingReplayEntryBytes(cloned)
|
||||
claudeThinkingReplayEntries[key] = claudeThinkingReplayEntry{Contents: cloned, Timestamp: now, Generation: generation, Deleted: deleted}
|
||||
enforceClaudeThinkingReplayLimitsLocked()
|
||||
}
|
||||
|
||||
func appendClaudeThinkingReplayContent(contents [][]byte, content []byte) [][]byte {
|
||||
cloned := cloneClaudeThinkingReplayContents(contents)
|
||||
for _, existing := range cloned {
|
||||
if claudeThinkingReplayJSONEqual(existing, content) {
|
||||
return cloned
|
||||
}
|
||||
}
|
||||
cloned = append(cloned, append([]byte(nil), content...))
|
||||
for len(cloned) > ClaudeThinkingReplayCacheMaxTurnsPerSession || claudeThinkingReplayEntryBytes(cloned) > ClaudeThinkingReplayCacheMaxBytesPerSession {
|
||||
if len(cloned) == 0 {
|
||||
break
|
||||
}
|
||||
cloned = cloned[1:]
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
func cloneClaudeThinkingReplayContents(contents [][]byte) [][]byte {
|
||||
cloned := make([][]byte, 0, len(contents))
|
||||
for _, content := range contents {
|
||||
cloned = append(cloned, append([]byte(nil), content...))
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
func claudeThinkingReplayEntryBytes(contents [][]byte) int {
|
||||
total := 0
|
||||
for _, content := range contents {
|
||||
total += len(content)
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
func claudeThinkingReplayCacheKey(modelFamily, sessionKey string) string {
|
||||
modelFamily = strings.TrimSpace(modelFamily)
|
||||
sessionKey = strings.TrimSpace(sessionKey)
|
||||
if modelFamily == "" || sessionKey == "" {
|
||||
return ""
|
||||
}
|
||||
return strings.Join([]string{"claude-thinking-replay", modelFamily, sessionKey}, "\x00")
|
||||
}
|
||||
|
||||
func claudeThinkingReplayKVKey(modelFamily, sessionKey string) string {
|
||||
return "cpa:claude:thinking-replay:" + homekv.HashKeyPart(strings.TrimSpace(modelFamily)) + ":" + homekv.HashKeyPart(strings.TrimSpace(sessionKey))
|
||||
}
|
||||
|
||||
func validClaudeThinkingReplayContent(content []byte) bool {
|
||||
if len(content) == 0 || len(content) > ClaudeThinkingReplayCacheMaxBytesPerSession || !gjson.ValidBytes(content) {
|
||||
return false
|
||||
}
|
||||
root := gjson.ParseBytes(content)
|
||||
return root.IsArray() && len(root.Array()) > 0 && len(root.Array()) <= ClaudeThinkingReplayCacheMaxBlocksPerTurn
|
||||
}
|
||||
|
||||
func claudeThinkingReplayJSONEqual(left, right []byte) bool {
|
||||
leftCanonical, leftOK := claudeThinkingReplayCanonicalJSON(left)
|
||||
rightCanonical, rightOK := claudeThinkingReplayCanonicalJSON(right)
|
||||
return leftOK && rightOK && bytes.Equal(leftCanonical, rightCanonical)
|
||||
}
|
||||
|
||||
func claudeThinkingReplayCanonicalJSON(raw []byte) ([]byte, bool) {
|
||||
decoder := json.NewDecoder(bytes.NewReader(raw))
|
||||
decoder.UseNumber()
|
||||
var value any
|
||||
if errDecode := decoder.Decode(&value); errDecode != nil {
|
||||
return nil, false
|
||||
}
|
||||
canonical, errMarshal := json.Marshal(value)
|
||||
return canonical, errMarshal == nil
|
||||
}
|
||||
|
||||
func enforceClaudeThinkingReplayLimitsLocked() {
|
||||
for len(claudeThinkingReplayEntries) > ClaudeThinkingReplayCacheMaxEntries || claudeThinkingReplayTotalBytes > ClaudeThinkingReplayCacheMaxTotalBytes {
|
||||
if len(claudeThinkingReplayEntries) == 0 {
|
||||
claudeThinkingReplayTotalBytes = 0
|
||||
return
|
||||
}
|
||||
evictOldestClaudeThinkingReplayEntriesLocked(ClaudeThinkingReplayCacheEvictBatchSize)
|
||||
}
|
||||
}
|
||||
|
||||
func evictOldestClaudeThinkingReplayEntriesLocked(count int) {
|
||||
if count <= 0 || len(claudeThinkingReplayEntries) == 0 {
|
||||
return
|
||||
}
|
||||
type candidate struct {
|
||||
key string
|
||||
timestamp time.Time
|
||||
}
|
||||
candidates := make([]candidate, 0, len(claudeThinkingReplayEntries))
|
||||
for key, entry := range claudeThinkingReplayEntries {
|
||||
candidates = append(candidates, candidate{key: key, timestamp: entry.Timestamp})
|
||||
}
|
||||
sort.Slice(candidates, func(i, j int) bool {
|
||||
return candidates[i].timestamp.Before(candidates[j].timestamp)
|
||||
})
|
||||
if count > len(candidates) {
|
||||
count = len(candidates)
|
||||
}
|
||||
for i := 0; i < count; i++ {
|
||||
entry := claudeThinkingReplayEntries[candidates[i].key]
|
||||
claudeThinkingReplayTotalBytes -= claudeThinkingReplayEntryBytes(entry.Contents)
|
||||
delete(claudeThinkingReplayEntries, candidates[i].key)
|
||||
}
|
||||
}
|
||||
|
||||
func purgeExpiredClaudeThinkingReplayCache(now time.Time) {
|
||||
claudeThinkingReplayMu.Lock()
|
||||
for key, entry := range claudeThinkingReplayEntries {
|
||||
if now.Sub(entry.Timestamp) > ClaudeThinkingReplayCacheTTL {
|
||||
claudeThinkingReplayTotalBytes -= claudeThinkingReplayEntryBytes(entry.Contents)
|
||||
delete(claudeThinkingReplayEntries, key)
|
||||
}
|
||||
}
|
||||
claudeThinkingReplayMu.Unlock()
|
||||
}
|
||||
Loading…
Reference in a new issue