-
Notifications
You must be signed in to change notification settings - Fork 2
feat: implement LRU cache and sophisticated token counting (issues #4 and #5) #127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -58,6 +58,271 @@ $OPTIMIZATION_QUALITY = 11 # Maximum compression quality | |||||||||||||||||||||||||||||||||||
| $HASH_PREFIX = "hash:" | ||||||||||||||||||||||||||||||||||||
| $HASH_LENGTH = 32 | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # ============================================================================= | ||||||||||||||||||||||||||||||||||||
| # LRU CACHE CLASSES (Issue #5) | ||||||||||||||||||||||||||||||||||||
| # ============================================================================= | ||||||||||||||||||||||||||||||||||||
| class LruCacheEntry { | ||||||||||||||||||||||||||||||||||||
| [object]$Value | ||||||||||||||||||||||||||||||||||||
| [datetime]$Timestamp | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| LruCacheEntry([object]$value) { | ||||||||||||||||||||||||||||||||||||
| $this.Value = $value | ||||||||||||||||||||||||||||||||||||
| $this.Timestamp = Get-Date | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| class LruCache { | ||||||||||||||||||||||||||||||||||||
| [System.Collections.Specialized.OrderedDictionary]$Cache | ||||||||||||||||||||||||||||||||||||
| [int]$MaxSize | ||||||||||||||||||||||||||||||||||||
| [int]$TtlSeconds | ||||||||||||||||||||||||||||||||||||
| [int]$HitCount = 0 | ||||||||||||||||||||||||||||||||||||
| [int]$MissCount = 0 | ||||||||||||||||||||||||||||||||||||
| [int]$EvictionCount = 0 | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| LruCache([int]$maxSize, [int]$ttlSeconds) { | ||||||||||||||||||||||||||||||||||||
| $this.Cache = [System.Collections.Specialized.OrderedDictionary]::new() | ||||||||||||||||||||||||||||||||||||
| $this.MaxSize = $maxSize | ||||||||||||||||||||||||||||||||||||
| $this.TtlSeconds = $ttlSeconds | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # Get value from cache (returns $null if not found or expired) | ||||||||||||||||||||||||||||||||||||
| [object] Get([string]$key) { | ||||||||||||||||||||||||||||||||||||
| if (-not $this.Cache.Contains($key)) { | ||||||||||||||||||||||||||||||||||||
| $this.MissCount++ | ||||||||||||||||||||||||||||||||||||
| return $null | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| $entry = $this.Cache[$key] | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # Check TTL expiration | ||||||||||||||||||||||||||||||||||||
| if ($this.TtlSeconds -gt 0) { | ||||||||||||||||||||||||||||||||||||
| $age = ((Get-Date) - $entry.Timestamp).TotalSeconds | ||||||||||||||||||||||||||||||||||||
| if ($age -gt $this.TtlSeconds) { | ||||||||||||||||||||||||||||||||||||
| $this.Cache.Remove($key) | ||||||||||||||||||||||||||||||||||||
| $this.MissCount++ | ||||||||||||||||||||||||||||||||||||
| $this.EvictionCount++ | ||||||||||||||||||||||||||||||||||||
| return $null | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # Move to end (most recently used) by removing and re-adding | ||||||||||||||||||||||||||||||||||||
| $value = $entry.Value | ||||||||||||||||||||||||||||||||||||
| $this.Cache.Remove($key) | ||||||||||||||||||||||||||||||||||||
| $this.Cache[$key] = [LruCacheEntry]::new($value) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| $this.HitCount++ | ||||||||||||||||||||||||||||||||||||
| return $value | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # Set value in cache | ||||||||||||||||||||||||||||||||||||
| [void] Set([string]$key, [object]$value) { | ||||||||||||||||||||||||||||||||||||
| # Remove if already exists (to re-insert at end) | ||||||||||||||||||||||||||||||||||||
| if ($this.Cache.Contains($key)) { | ||||||||||||||||||||||||||||||||||||
| $this.Cache.Remove($key) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # Evict least recently used if at capacity | ||||||||||||||||||||||||||||||||||||
| if ($this.Cache.Count -ge $this.MaxSize) { | ||||||||||||||||||||||||||||||||||||
| # First key is least recently used (OrderedDictionary maintains insertion order) | ||||||||||||||||||||||||||||||||||||
| $firstKey = @($this.Cache.Keys)[0] | ||||||||||||||||||||||||||||||||||||
| $this.Cache.Remove($firstKey) | ||||||||||||||||||||||||||||||||||||
| $this.EvictionCount++ | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # Insert at end (most recently used) | ||||||||||||||||||||||||||||||||||||
| $this.Cache[$key] = [LruCacheEntry]::new($value) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # Check if key exists and is not expired | ||||||||||||||||||||||||||||||||||||
| [bool] ContainsKey([string]$key) { | ||||||||||||||||||||||||||||||||||||
| return $null -ne $this.Get($key) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+141
to
+143
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ContainsKey has unexpected side effects. Calling Apply this diff to implement a side-effect-free check: # Check if key exists and is not expired
[bool] ContainsKey([string]$key) {
- return $null -ne $this.Get($key)
+ if (-not $this.Cache.Contains($key)) {
+ return $false
+ }
+ # Check TTL without updating counters or position
+ $entry = $this.Cache[$key]
+ if ($this.TtlSeconds -gt 0) {
+ $age = ((Get-Date) - $entry.Timestamp).TotalSeconds
+ if ($age -gt $this.TtlSeconds) {
+ return $false
+ }
+ }
+ return $true
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # Clear all entries | ||||||||||||||||||||||||||||||||||||
| [void] Clear() { | ||||||||||||||||||||||||||||||||||||
| $this.Cache.Clear() | ||||||||||||||||||||||||||||||||||||
| $this.HitCount = 0 | ||||||||||||||||||||||||||||||||||||
| $this.MissCount = 0 | ||||||||||||||||||||||||||||||||||||
| $this.EvictionCount = 0 | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # Get cache statistics | ||||||||||||||||||||||||||||||||||||
| [hashtable] GetStats() { | ||||||||||||||||||||||||||||||||||||
| $totalRequests = $this.HitCount + $this.MissCount | ||||||||||||||||||||||||||||||||||||
| return @{ | ||||||||||||||||||||||||||||||||||||
| Size = $this.Cache.Count | ||||||||||||||||||||||||||||||||||||
| MaxSize = $this.MaxSize | ||||||||||||||||||||||||||||||||||||
| HitCount = $this.HitCount | ||||||||||||||||||||||||||||||||||||
| MissCount = $this.MissCount | ||||||||||||||||||||||||||||||||||||
| EvictionCount = $this.EvictionCount | ||||||||||||||||||||||||||||||||||||
| HitRate = if ($totalRequests -gt 0) { | ||||||||||||||||||||||||||||||||||||
| [Math]::Round(($this.HitCount / $totalRequests) * 100, 2) | ||||||||||||||||||||||||||||||||||||
| } else { 0 } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # Cleanup expired entries (call periodically) | ||||||||||||||||||||||||||||||||||||
| [int] CleanupExpired() { | ||||||||||||||||||||||||||||||||||||
| if ($this.TtlSeconds -le 0) { return 0 } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| $removed = 0 | ||||||||||||||||||||||||||||||||||||
| $keysToRemove = @() | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| foreach ($key in $this.Cache.Keys) { | ||||||||||||||||||||||||||||||||||||
| $entry = $this.Cache[$key] | ||||||||||||||||||||||||||||||||||||
| $age = ((Get-Date) - $entry.Timestamp).TotalSeconds | ||||||||||||||||||||||||||||||||||||
| if ($age -gt $this.TtlSeconds) { | ||||||||||||||||||||||||||||||||||||
| $keysToRemove += $key | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| foreach ($key in $keysToRemove) { | ||||||||||||||||||||||||||||||||||||
| $this.Cache.Remove($key) | ||||||||||||||||||||||||||||||||||||
| $removed++ | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| $this.EvictionCount += $removed | ||||||||||||||||||||||||||||||||||||
| return $removed | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # ============================================================================= | ||||||||||||||||||||||||||||||||||||
| # TOKEN COUNTER CLASS (Issue #4) | ||||||||||||||||||||||||||||||||||||
| # ============================================================================= | ||||||||||||||||||||||||||||||||||||
| class TokenCounter { | ||||||||||||||||||||||||||||||||||||
| [string]$ApiKey | ||||||||||||||||||||||||||||||||||||
| [string]$Model | ||||||||||||||||||||||||||||||||||||
| [LruCache]$Cache | ||||||||||||||||||||||||||||||||||||
| [int]$ApiCallCount = 0 | ||||||||||||||||||||||||||||||||||||
| [int]$CacheHitCount = 0 | ||||||||||||||||||||||||||||||||||||
| [int]$EstimationCount = 0 | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| TokenCounter([string]$apiKey, [string]$model) { | ||||||||||||||||||||||||||||||||||||
| $this.ApiKey = $apiKey | ||||||||||||||||||||||||||||||||||||
| $this.Model = $model | ||||||||||||||||||||||||||||||||||||
| # Use LRU cache: Max 200 entries, TTL 30 minutes (1800 seconds) | ||||||||||||||||||||||||||||||||||||
| $this.Cache = [LruCache]::new(200, 1800) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # Primary method: try API first, fall back to estimation | ||||||||||||||||||||||||||||||||||||
| [int] CountTokens([string]$text, [string]$contentType) { | ||||||||||||||||||||||||||||||||||||
| # Check cache first (using content hash as key) | ||||||||||||||||||||||||||||||||||||
| $textHash = [System.BitConverter]::ToString( | ||||||||||||||||||||||||||||||||||||
| [System.Security.Cryptography.SHA256]::Create().ComputeHash( | ||||||||||||||||||||||||||||||||||||
| [System.Text.Encoding]::UTF8.GetBytes($text) | ||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||
| ).Replace("-", "") | ||||||||||||||||||||||||||||||||||||
ooples marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||
| $cacheKey = "${contentType}:${textHash}" | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| $cached = $this.Cache.Get($cacheKey) | ||||||||||||||||||||||||||||||||||||
| if ($null -ne $cached) { | ||||||||||||||||||||||||||||||||||||
| $this.CacheHitCount++ | ||||||||||||||||||||||||||||||||||||
| return $cached | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # Try API call if key is available | ||||||||||||||||||||||||||||||||||||
| if ($this.ApiKey) { | ||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||
| $tokenCount = $this.CountTokensViaAPI($text) | ||||||||||||||||||||||||||||||||||||
| $this.ApiCallCount++ | ||||||||||||||||||||||||||||||||||||
| $this.Cache.Set($cacheKey, $tokenCount) | ||||||||||||||||||||||||||||||||||||
| return $tokenCount | ||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||
| # API failed, fall back to estimation | ||||||||||||||||||||||||||||||||||||
| Write-Log "Token counting API failed: $($_.Exception.Message), falling back to estimation" "WARN" | ||||||||||||||||||||||||||||||||||||
ooples marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # Fallback to improved estimation | ||||||||||||||||||||||||||||||||||||
| $estimated = $this.EstimateTokens($text, $contentType) | ||||||||||||||||||||||||||||||||||||
| $this.EstimationCount++ | ||||||||||||||||||||||||||||||||||||
| $this.Cache.Set($cacheKey, $estimated) | ||||||||||||||||||||||||||||||||||||
| return $estimated | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # Google AI API integration | ||||||||||||||||||||||||||||||||||||
| [int] CountTokensViaAPI([string]$text) { | ||||||||||||||||||||||||||||||||||||
| $requestBody = @{ | ||||||||||||||||||||||||||||||||||||
| contents = @( | ||||||||||||||||||||||||||||||||||||
| @{ | ||||||||||||||||||||||||||||||||||||
| parts = @( | ||||||||||||||||||||||||||||||||||||
| @{ | ||||||||||||||||||||||||||||||||||||
| text = $text | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||
| } | ConvertTo-Json -Depth 10 -Compress | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| $uri = "https://generativelanguage.googleapis.com/v1beta/models/$($this.Model):countTokens?key=$($this.ApiKey)" | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| $response = Invoke-RestMethod -Uri $uri -Method POST -ContentType "application/json" -Body $requestBody -TimeoutSec 5 | ||||||||||||||||||||||||||||||||||||
ooples marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| return $response.totalTokens | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # Improved estimation with content-type awareness | ||||||||||||||||||||||||||||||||||||
| [int] EstimateTokens([string]$text, [string]$contentType) { | ||||||||||||||||||||||||||||||||||||
| $baseRatio = [Math]::Ceiling($text.Length / 4.0) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| switch ($contentType) { | ||||||||||||||||||||||||||||||||||||
| "code" { | ||||||||||||||||||||||||||||||||||||
| # Code has more tokens per character due to symbols/keywords | ||||||||||||||||||||||||||||||||||||
| return [Math]::Ceiling($baseRatio * 1.2) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| "json" { | ||||||||||||||||||||||||||||||||||||
| # JSON structures add token overhead for delimiters | ||||||||||||||||||||||||||||||||||||
| return [Math]::Ceiling($baseRatio * 1.15) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| "markdown" { | ||||||||||||||||||||||||||||||||||||
| # Markdown formatting adds token overhead | ||||||||||||||||||||||||||||||||||||
| return [Math]::Ceiling($baseRatio * 1.1) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| "text" { | ||||||||||||||||||||||||||||||||||||
| # Plain text is slightly less than base ratio | ||||||||||||||||||||||||||||||||||||
| return [Math]::Ceiling($baseRatio * 0.95) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| default { | ||||||||||||||||||||||||||||||||||||
| return $baseRatio | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # Content type detection based on file extension or tool name | ||||||||||||||||||||||||||||||||||||
| [string] DetectContentType([string]$identifier) { | ||||||||||||||||||||||||||||||||||||
| switch -Regex ($identifier) { | ||||||||||||||||||||||||||||||||||||
| '\.(cs|ps1|ts|js|py|java|cpp|c|h|go|rs|rb|php)$' { return "code" } | ||||||||||||||||||||||||||||||||||||
| '\.(json|jsonc)$' { return "json" } | ||||||||||||||||||||||||||||||||||||
| '\.(md|markdown)$' { return "markdown" } | ||||||||||||||||||||||||||||||||||||
| '^Read$|^Grep$|^Bash$' { return "code" } | ||||||||||||||||||||||||||||||||||||
ooples marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||
| default { return "text" } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # Get cache statistics | ||||||||||||||||||||||||||||||||||||
| [hashtable] GetStats() { | ||||||||||||||||||||||||||||||||||||
| $cacheStats = $this.Cache.GetStats() | ||||||||||||||||||||||||||||||||||||
| $totalCalls = $this.ApiCallCount + $this.CacheHitCount + $this.EstimationCount | ||||||||||||||||||||||||||||||||||||
ooples marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||
| return @{ | ||||||||||||||||||||||||||||||||||||
| ApiCalls = $this.ApiCallCount | ||||||||||||||||||||||||||||||||||||
| CacheHits = $this.CacheHitCount | ||||||||||||||||||||||||||||||||||||
| EstimationCount = $this.EstimationCount | ||||||||||||||||||||||||||||||||||||
| CacheSize = $cacheStats.Size | ||||||||||||||||||||||||||||||||||||
| CacheHitRate = $cacheStats.HitRate | ||||||||||||||||||||||||||||||||||||
| TotalCalls = $totalCalls | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # Initialize global TokenCounter (singleton pattern) | ||||||||||||||||||||||||||||||||||||
| if (-not $script:TokenCounter) { | ||||||||||||||||||||||||||||||||||||
| $apiKey = $env:GOOGLE_AI_API_KEY | ||||||||||||||||||||||||||||||||||||
| if (-not $apiKey) { | ||||||||||||||||||||||||||||||||||||
| Write-Host "WARN: GOOGLE_AI_API_KEY not set, falling back to estimation only" -ForegroundColor Yellow | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| $script:TokenCounter = [TokenCounter]::new($apiKey, "gemini-2.0-flash-exp") | ||||||||||||||||||||||||||||||||||||
ooples marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # PHASE 2 FIX: Deterministic cache key generation | ||||||||||||||||||||||||||||||||||||
| # Fixes 0% cache hit rate by ensuring identical operations produce identical keys | ||||||||||||||||||||||||||||||||||||
| function Get-DeterministicCacheKey { | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Preserve original timestamp when moving entry to end.
When promoting an entry to the most-recently-used position, creating a new
LruCacheEntryresets the timestamp to the current time. This incorrectly extends the TTL on every access, meaning cached entries will never expire if accessed frequently enough.Apply this diff to preserve the original timestamp:
# Move to end (most recently used) by removing and re-adding $value = $entry.Value + $originalTimestamp = $entry.Timestamp $this.Cache.Remove($key) - $this.Cache[$key] = [LruCacheEntry]::new($value) + $newEntry = [LruCacheEntry]::new($value) + $newEntry.Timestamp = $originalTimestamp + $this.Cache[$key] = $newEntry📝 Committable suggestion
🤖 Prompt for AI Agents