Files
CLIProxyAPI/internal/cache/bounded_lru.go
sususu f1f5506c0b fix(devin): align wire protocol, harden streaming, and resolve multi-turn tool/signature parity
- Wire parity: align Connect-RPC Sentry-Trace, User-Agent suppression, float32 double pattern, and dynamic 732-char hex device fingerprint
- Session ordinal & cache: implement process-scoped Field 15.2 with bounded LRU (5000 entries) and Field 15.4=14 user boundary; prioritize stable session_id over previous_interaction_id to preserve prompt caching
- Streaming robustness: unblock hung TCP reads on client cancellation via context watcher; accurately propagate stream read errors and trailer errors instead of swallowing truncated frames
- Thought signature & reasoning: emit raw delta signatures directly in active thought steps; eliminate redundant tail base64 re-encoding; ensure 1:1 assistant signature and thinking alignment across multi-turn history
- Tool call de-multiplexing: route parallel tool calls by tc.Index in both streaming step events and non-streaming aggregations
- Security & transport: escape OAuth callback error HTML against reflected XSS, enforce strict state validation, and isolate Devin HTTP transport with tr.Clone()
2026-09-13 23:34:43 +08:00

102 lines
2.4 KiB
Go

package cache
import (
"container/list"
"sync"
)
type boundedLRUEntry[K comparable, V any] struct {
key K
value V
}
// BoundedLRU stores at most capacity values and evicts the least recently used
// value when a new key crosses the bound. The optional eviction callback runs
// after the cache lock is released.
type BoundedLRU[K comparable, V any] struct {
mu sync.Mutex
capacity int
entries map[K]*list.Element
order *list.List
onEvict func(K, V)
}
func NewBoundedLRU[K comparable, V any](capacity int, onEvict func(K, V)) *BoundedLRU[K, V] {
if capacity < 1 {
capacity = 1
}
return &BoundedLRU[K, V]{
capacity: capacity,
entries: make(map[K]*list.Element, capacity),
order: list.New(),
onEvict: onEvict,
}
}
// GetOrAdd returns the cached value or creates and stores one while holding the
// cache lock. The create function must not call back into this cache.
func (cache *BoundedLRU[K, V]) GetOrAdd(key K, create func() V) V {
cache.mu.Lock()
if element, ok := cache.entries[key]; ok {
cache.order.MoveToFront(element)
value := element.Value.(boundedLRUEntry[K, V]).value
cache.mu.Unlock()
return value
}
value := create()
element := cache.order.PushFront(boundedLRUEntry[K, V]{key: key, value: value})
cache.entries[key] = element
var evicted boundedLRUEntry[K, V]
didEvict := false
if cache.order.Len() > cache.capacity {
oldest := cache.order.Back()
evicted = oldest.Value.(boundedLRUEntry[K, V])
delete(cache.entries, evicted.key)
cache.order.Remove(oldest)
didEvict = true
}
cache.mu.Unlock()
if didEvict && cache.onEvict != nil {
cache.onEvict(evicted.key, evicted.value)
}
return value
}
func (cache *BoundedLRU[K, V]) Get(key K) (V, bool) {
cache.mu.Lock()
defer cache.mu.Unlock()
if element, ok := cache.entries[key]; ok {
cache.order.MoveToFront(element)
return element.Value.(boundedLRUEntry[K, V]).value, true
}
var zero V
return zero, false
}
func (cache *BoundedLRU[K, V]) Delete(key K) bool {
cache.mu.Lock()
element, ok := cache.entries[key]
if !ok {
cache.mu.Unlock()
return false
}
entry := element.Value.(boundedLRUEntry[K, V])
delete(cache.entries, key)
cache.order.Remove(element)
cache.mu.Unlock()
if cache.onEvict != nil {
cache.onEvict(entry.key, entry.value)
}
return true
}
func (cache *BoundedLRU[K, V]) Len() int {
cache.mu.Lock()
defer cache.mu.Unlock()
return len(cache.entries)
}