vibe-proxy/backend/internal/cache/antigravity_reasoning_replay_cache.go
2026-08-24 00:10:41 +02:00

656 lines
27 KiB
Go

package cache
import (
"bytes"
"context"
"crypto/rand"
"encoding/json"
"fmt"
"sort"
"strings"
"sync"
"time"
homekv "github.com/router-for-me/CLIProxyAPI/v7/internal/home"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
const (
// AntigravityReasoningReplayCacheTTL limits how long encrypted reasoning replay
// items stay in process memory.
AntigravityReasoningReplayCacheTTL = 1 * time.Hour
// AntigravityReasoningReplayCacheMaxEntries bounds process memory for replay
// continuity. Oldest entries are evicted first.
AntigravityReasoningReplayCacheMaxEntries = 10240
// AntigravityReasoningReplayCacheEvictBatchSize leaves headroom after the cache
// reaches capacity so high write volume does not rescan the map every turn.
AntigravityReasoningReplayCacheEvictBatchSize = 128
minAntigravityThoughtSignatureReplayLen = 16
// AntigravityReasoningReplayCacheMaxItemsPerEntry and MaxBytesPerEntry
// bound one logical conversation. Oversized chains are not partially cached,
// because dropping an arbitrary prefix would break native signature ordering.
AntigravityReasoningReplayCacheMaxItemsPerEntry = 4096
AntigravityReasoningReplayCacheMaxBytesPerEntry = 16 << 20
// JSON encodes each normalized []byte item as base64. Leave enough room for
// that expansion while rejecting oversized Home values before unmarshalling.
antigravityReasoningReplayCacheMaxSerializedBytes = 24 << 20
)
type antigravityReasoningReplayEntry struct {
Items [][]byte
Timestamp time.Time
Revision uint64
Branch string
Deleted bool
}
const antigravityReasoningReplayGenerationItemType = "cpa_antigravity_replay_generation"
// AntigravityReasoningReplaySnapshot identifies the exact replay state read for
// one request. Its fields are intentionally opaque outside this package.
type AntigravityReasoningReplaySnapshot struct {
raw []byte
items [][]byte
loaded bool
found bool
revision uint64
branch string
evictionEpoch uint64
}
var (
antigravityReasoningReplayMu sync.Mutex
antigravityReasoningReplayEntries = make(map[string]antigravityReasoningReplayEntry)
antigravityReasoningReplayNextRevision uint64
antigravityReasoningReplayEvictionEpoch uint64
)
type antigravityReasoningReplayKVClient interface {
KVGet(ctx context.Context, key string) ([]byte, bool, error)
KVSet(ctx context.Context, key string, value []byte, opts homekv.KVSetOptions) (bool, error)
KVCompareAndSwap(ctx context.Context, key string, expected []byte, expectedExists bool, value []byte, ttl time.Duration) (bool, error)
KVExpire(ctx context.Context, key string, ttl time.Duration) (bool, error)
}
var currentAntigravityReasoningReplayKVClient = func() (antigravityReasoningReplayKVClient, bool, error) {
return homekv.CurrentKVClient()
}
// CacheAntigravityReasoningReplayItem stores a final GPT/Codex reasoning item for
// stateless replay. The stored item is normalized to the minimal shape accepted
// by Responses input replay.
func CacheAntigravityReasoningReplayItem(modelName, sessionKey string, item []byte) bool {
return CacheAntigravityReasoningReplayItems(modelName, sessionKey, [][]byte{item})
}
// CacheAntigravityReasoningReplayItems stores the final GPT/Codex assistant output
// items needed to replay a stateless next turn.
func CacheAntigravityReasoningReplayItems(modelName, sessionKey string, items [][]byte) bool {
return CacheAntigravityReasoningReplayItemsBestEffort(context.Background(), modelName, sessionKey, items)
}
// CacheAntigravityReasoningReplayItemsBestEffort stores replay items for completed response paths.
func CacheAntigravityReasoningReplayItemsBestEffort(ctx context.Context, modelName, sessionKey string, items [][]byte) bool {
key := antigravityReasoningReplayCacheKey(modelName, sessionKey)
if key == "" {
return false
}
normalized, ok := normalizeAntigravityReasoningReplayItems(items)
if !ok {
return false
}
if client, homeMode, errClient := currentAntigravityReasoningReplayKVClient(); homeMode {
if errClient != nil {
log.Errorf("home kv best-effort antigravity reasoning replay set failed prefix=cpa:antigravity:*: %v", errClient)
return false
}
raw, errMarshal := marshalAntigravityReasoningReplayHomeValue(normalized, "")
if errMarshal != nil {
log.Errorf("home kv best-effort antigravity reasoning replay set failed prefix=cpa:antigravity:*: %v", errMarshal)
return false
}
written, errSet := client.KVSet(ctx, antigravityReasoningReplayKVKey(modelName, sessionKey), raw, homekv.KVSetOptions{EX: AntigravityReasoningReplayCacheTTL})
if errSet != nil {
log.Errorf("home kv best-effort antigravity reasoning replay set failed prefix=cpa:antigravity:*: %v", errSet)
return false
}
return written
}
cacheCleanupOnce.Do(startCacheCleanup)
now := time.Now()
antigravityReasoningReplayMu.Lock()
defer antigravityReasoningReplayMu.Unlock()
antigravityReasoningReplayNextRevision++
antigravityReasoningReplayEntries[key] = antigravityReasoningReplayEntry{
Items: normalized,
Timestamp: now,
Revision: antigravityReasoningReplayNextRevision,
Branch: newAntigravityReasoningReplayGeneration(),
}
if len(antigravityReasoningReplayEntries) > AntigravityReasoningReplayCacheMaxEntries {
evictOldestAntigravityReasoningReplayEntries(AntigravityReasoningReplayCacheEvictBatchSize)
}
return true
}
// GetAntigravityReasoningReplayItem retrieves a normalized reasoning replay item.
func GetAntigravityReasoningReplayItem(modelName, sessionKey string) ([]byte, bool) {
items, ok := GetAntigravityReasoningReplayItems(modelName, sessionKey)
if !ok || len(items) == 0 {
return nil, false
}
return items[0], true
}
// GetAntigravityReasoningReplayItems retrieves normalized assistant output items.
func GetAntigravityReasoningReplayItems(modelName, sessionKey string) ([][]byte, bool) {
items, ok, err := GetAntigravityReasoningReplayItemsRequired(context.Background(), modelName, sessionKey)
if err == nil {
return items, ok
}
return nil, false
}
// GetAntigravityReasoningReplayItemsRequired retrieves replay items for request-time paths.
func GetAntigravityReasoningReplayItemsRequired(ctx context.Context, modelName, sessionKey string) ([][]byte, bool, error) {
items, _, found, errGet := GetAntigravityReasoningReplayItemsWithSnapshotRequired(ctx, modelName, sessionKey)
return items, found, errGet
}
// GetAntigravityReasoningReplayItemsWithSnapshotRequired retrieves replay items
// and the exact cache state that guarded this request.
func GetAntigravityReasoningReplayItemsWithSnapshotRequired(ctx context.Context, modelName, sessionKey string) ([][]byte, AntigravityReasoningReplaySnapshot, bool, error) {
key := antigravityReasoningReplayCacheKey(modelName, sessionKey)
if key == "" {
return nil, AntigravityReasoningReplaySnapshot{}, false, nil
}
client, homeMode, errClient := currentAntigravityReasoningReplayKVClient()
if homeMode {
if errClient != nil {
return nil, AntigravityReasoningReplaySnapshot{}, false, errClient
}
kvKey := antigravityReasoningReplayKVKey(modelName, sessionKey)
var raw []byte
found := false
for attempt := 0; attempt < 4; attempt++ {
currentRaw, currentFound, errGet := client.KVGet(ctx, kvKey)
if errGet != nil {
return nil, AntigravityReasoningReplaySnapshot{loaded: true}, false, errGet
}
if currentFound {
raw = currentRaw
found = true
break
}
reservation := newAntigravityReasoningReplayTombstone()
swapped, errReserve := client.KVCompareAndSwap(ctx, kvKey, nil, false, reservation, AntigravityReasoningReplayCacheTTL)
if errReserve != nil {
return nil, AntigravityReasoningReplaySnapshot{loaded: true}, false, errReserve
}
if swapped {
raw = reservation
found = true
break
}
}
if !found {
return nil, AntigravityReasoningReplaySnapshot{loaded: true}, false, fmt.Errorf("could not fence absent antigravity reasoning replay state")
}
if len(raw) > antigravityReasoningReplayCacheMaxSerializedBytes {
return nil, AntigravityReasoningReplaySnapshot{loaded: true, found: true}, false, nil
}
snapshot := AntigravityReasoningReplaySnapshot{raw: append([]byte(nil), raw...), loaded: true, found: true}
homeItems, deleted, _, branch, okDecode := decodeAntigravityReasoningReplayHomeValue(raw)
snapshot.branch = branch
if !okDecode || deleted || len(homeItems) == 0 {
return nil, snapshot, false, nil
}
if len(homeItems) > AntigravityReasoningReplayCacheMaxItemsPerEntry {
return nil, snapshot, false, nil
}
normalized, okNormalize := normalizeAntigravityReasoningReplayItems(homeItems)
if !okNormalize || len(normalized) != len(homeItems) {
return nil, snapshot, false, nil
}
snapshot.items = cloneAntigravityReasoningReplayItems(normalized)
if _, errExpire := client.KVExpire(ctx, kvKey, AntigravityReasoningReplayCacheTTL); errExpire != nil {
return nil, snapshot, false, errExpire
}
return normalized, snapshot, true, nil
}
cacheCleanupOnce.Do(startCacheCleanup)
now := time.Now()
antigravityReasoningReplayMu.Lock()
defer antigravityReasoningReplayMu.Unlock()
entry, ok := antigravityReasoningReplayEntries[key]
if !ok {
return nil, reserveAntigravityReasoningReplayAbsentLocked(key, now), false, nil
}
if now.Sub(entry.Timestamp) > AntigravityReasoningReplayCacheTTL {
antigravityReasoningReplayEvictionEpoch++
delete(antigravityReasoningReplayEntries, key)
return nil, reserveAntigravityReasoningReplayAbsentLocked(key, now), false, nil
}
entry.Timestamp = now
antigravityReasoningReplayEntries[key] = entry
snapshot := AntigravityReasoningReplaySnapshot{loaded: true, found: true, revision: entry.Revision, branch: entry.Branch, evictionEpoch: antigravityReasoningReplayEvictionEpoch}
if entry.Deleted || len(entry.Items) == 0 {
return nil, snapshot, false, nil
}
snapshot.items = cloneAntigravityReasoningReplayItems(entry.Items)
return cloneAntigravityReasoningReplayItems(entry.Items), snapshot, true, nil
}
// reserveAntigravityReasoningReplayAbsentLocked fences a local miss with a
// per-key tombstone so eviction of an unrelated key cannot invalidate it.
// antigravityReasoningReplayMu must be held by the caller.
func reserveAntigravityReasoningReplayAbsentLocked(key string, now time.Time) AntigravityReasoningReplaySnapshot {
if len(antigravityReasoningReplayEntries) >= AntigravityReasoningReplayCacheMaxEntries {
evictOldestAntigravityReasoningReplayEntries(AntigravityReasoningReplayCacheEvictBatchSize)
}
antigravityReasoningReplayNextRevision++
entry := antigravityReasoningReplayEntry{
Timestamp: now,
Revision: antigravityReasoningReplayNextRevision,
Branch: newAntigravityReasoningReplayGeneration(),
Deleted: true,
}
antigravityReasoningReplayEntries[key] = entry
return AntigravityReasoningReplaySnapshot{
loaded: true,
found: true,
revision: entry.Revision,
branch: entry.Branch,
evictionEpoch: antigravityReasoningReplayEvictionEpoch,
}
}
// ReplaceAntigravityReasoningReplayItemsIfUnchanged publishes a completed chain
// only when no newer request has changed the state read by this request.
func ReplaceAntigravityReasoningReplayItemsIfUnchanged(ctx context.Context, modelName, sessionKey string, snapshot AntigravityReasoningReplaySnapshot, items [][]byte) (bool, error) {
key := antigravityReasoningReplayCacheKey(modelName, sessionKey)
if key == "" {
return false, nil
}
normalized, okNormalize := normalizeAntigravityReasoningReplayItems(items)
if !okNormalize {
return false, fmt.Errorf("invalid antigravity reasoning replay items")
}
if !snapshot.loaded {
return CacheAntigravityReasoningReplayItemsBestEffort(ctx, modelName, sessionKey, normalized), nil
}
client, homeMode, errClient := currentAntigravityReasoningReplayKVClient()
if homeMode {
if errClient != nil {
return false, errClient
}
kvKey := antigravityReasoningReplayKVKey(modelName, sessionKey)
expectedRaw := snapshot.raw
expectedFound := snapshot.found
branch := snapshot.branch
if branch == "" || !antigravityReasoningReplayItemsPrefix(snapshot.items, normalized) {
branch = newAntigravityReasoningReplayGeneration()
}
for attempt := 0; attempt < 4; attempt++ {
raw, errMarshal := marshalAntigravityReasoningReplayHomeValue(normalized, branch)
if errMarshal != nil {
return false, errMarshal
}
swapped, errCAS := client.KVCompareAndSwap(ctx, kvKey, expectedRaw, expectedFound, raw, AntigravityReasoningReplayCacheTTL)
if errCAS != nil || swapped {
return swapped, errCAS
}
currentRaw, currentFound, errGet := client.KVGet(ctx, kvKey)
if errGet != nil || !currentFound {
return false, errGet
}
if len(currentRaw) > antigravityReasoningReplayCacheMaxSerializedBytes {
return false, nil
}
currentItems, deleted, _, currentBranch, okDecode := decodeAntigravityReasoningReplayHomeValue(currentRaw)
if !okDecode || deleted || snapshot.branch == "" || currentBranch != snapshot.branch {
return false, nil
}
normalizedCurrent, okNormalizeCurrent := normalizeAntigravityReasoningReplayItems(currentItems)
if !okNormalizeCurrent || len(normalizedCurrent) != len(currentItems) || !antigravityReasoningReplayItemsPrefix(normalizedCurrent, normalized) {
return false, nil
}
expectedRaw = currentRaw
expectedFound = true
}
return false, nil
}
cacheCleanupOnce.Do(startCacheCleanup)
now := time.Now()
antigravityReasoningReplayMu.Lock()
defer antigravityReasoningReplayMu.Unlock()
entry, found := antigravityReasoningReplayEntries[key]
matchesSnapshot := found == snapshot.found && ((found && entry.Revision == snapshot.revision) || (!found && snapshot.evictionEpoch == antigravityReasoningReplayEvictionEpoch))
isDescendant := found && !entry.Deleted && snapshot.branch != "" && entry.Branch == snapshot.branch && antigravityReasoningReplayItemsPrefix(entry.Items, normalized)
if !matchesSnapshot && !isDescendant {
return false, nil
}
branch := snapshot.branch
if branch == "" || (matchesSnapshot && !antigravityReasoningReplayItemsPrefix(snapshot.items, normalized)) {
branch = newAntigravityReasoningReplayGeneration()
}
antigravityReasoningReplayNextRevision++
antigravityReasoningReplayEntries[key] = antigravityReasoningReplayEntry{Items: normalized, Timestamp: now, Revision: antigravityReasoningReplayNextRevision, Branch: branch}
if len(antigravityReasoningReplayEntries) > AntigravityReasoningReplayCacheMaxEntries {
evictOldestAntigravityReasoningReplayEntries(AntigravityReasoningReplayCacheEvictBatchSize)
}
return true, nil
}
// DeleteAntigravityReasoningReplayItemsIfUnchanged clears replay state only when
// it still matches the state read for this request.
func DeleteAntigravityReasoningReplayItemsIfUnchanged(ctx context.Context, modelName, sessionKey string, snapshot AntigravityReasoningReplaySnapshot) (bool, error) {
key := antigravityReasoningReplayCacheKey(modelName, sessionKey)
if key == "" {
return false, nil
}
if !snapshot.loaded {
return true, DeleteAntigravityReasoningReplayItemRequired(ctx, modelName, sessionKey)
}
client, homeMode, errClient := currentAntigravityReasoningReplayKVClient()
if homeMode {
if errClient != nil {
return false, errClient
}
return client.KVCompareAndSwap(ctx, antigravityReasoningReplayKVKey(modelName, sessionKey), snapshot.raw, snapshot.found, newAntigravityReasoningReplayTombstone(), AntigravityReasoningReplayCacheTTL)
}
cacheCleanupOnce.Do(startCacheCleanup)
antigravityReasoningReplayMu.Lock()
defer antigravityReasoningReplayMu.Unlock()
entry, found := antigravityReasoningReplayEntries[key]
if found != snapshot.found || (found && entry.Revision != snapshot.revision) || (!found && snapshot.evictionEpoch != antigravityReasoningReplayEvictionEpoch) {
return false, nil
}
antigravityReasoningReplayNextRevision++
antigravityReasoningReplayEntries[key] = antigravityReasoningReplayEntry{Timestamp: time.Now(), Revision: antigravityReasoningReplayNextRevision, Branch: newAntigravityReasoningReplayGeneration(), Deleted: true}
if len(antigravityReasoningReplayEntries) > AntigravityReasoningReplayCacheMaxEntries {
evictOldestAntigravityReasoningReplayEntries(AntigravityReasoningReplayCacheEvictBatchSize)
}
return true, nil
}
// DeleteAntigravityReasoningReplayItem removes one replay item after upstream rejects
// it or the caller otherwise knows it is stale.
func DeleteAntigravityReasoningReplayItem(modelName, sessionKey string) {
if errDelete := DeleteAntigravityReasoningReplayItemRequired(context.Background(), modelName, sessionKey); errDelete != nil {
return
}
}
// DeleteAntigravityReasoningReplayItemRequired removes one replay item for request-time paths.
func DeleteAntigravityReasoningReplayItemRequired(ctx context.Context, modelName, sessionKey string) error {
key := antigravityReasoningReplayCacheKey(modelName, sessionKey)
if key == "" {
return nil
}
client, homeMode, errClient := currentAntigravityReasoningReplayKVClient()
if homeMode {
if errClient != nil {
return errClient
}
_, errSet := client.KVSet(ctx, antigravityReasoningReplayKVKey(modelName, sessionKey), newAntigravityReasoningReplayTombstone(), homekv.KVSetOptions{EX: AntigravityReasoningReplayCacheTTL})
return errSet
}
cacheCleanupOnce.Do(startCacheCleanup)
antigravityReasoningReplayMu.Lock()
antigravityReasoningReplayNextRevision++
antigravityReasoningReplayEntries[key] = antigravityReasoningReplayEntry{Timestamp: time.Now(), Revision: antigravityReasoningReplayNextRevision, Branch: newAntigravityReasoningReplayGeneration(), Deleted: true}
if len(antigravityReasoningReplayEntries) > AntigravityReasoningReplayCacheMaxEntries {
evictOldestAntigravityReasoningReplayEntries(AntigravityReasoningReplayCacheEvictBatchSize)
}
antigravityReasoningReplayMu.Unlock()
return nil
}
func newAntigravityReasoningReplayGeneration() string {
var nonce [16]byte
if _, errRead := rand.Read(nonce[:]); errRead != nil {
return fmt.Sprintf("fallback-%d", time.Now().UnixNano())
}
return fmt.Sprintf("%x", nonce[:])
}
func marshalAntigravityReasoningReplayHomeValue(items [][]byte, branch string) ([]byte, error) {
if branch == "" {
branch = newAntigravityReasoningReplayGeneration()
}
marker := []byte(`{"type":"","generation":"","branch":""}`)
marker, _ = sjson.SetBytes(marker, "type", antigravityReasoningReplayGenerationItemType)
marker, _ = sjson.SetBytes(marker, "generation", newAntigravityReasoningReplayGeneration())
marker, _ = sjson.SetBytes(marker, "branch", branch)
stored := make([][]byte, 0, len(items)+1)
stored = append(stored, marker)
stored = append(stored, items...)
return json.Marshal(stored)
}
func decodeAntigravityReasoningReplayHomeValue(raw []byte) (items [][]byte, deleted bool, generation, branch string, ok bool) {
if errUnmarshal := json.Unmarshal(raw, &items); errUnmarshal != nil {
return nil, false, "", "", false
}
if len(items) == 0 || strings.TrimSpace(gjson.GetBytes(items[0], "type").String()) != antigravityReasoningReplayGenerationItemType {
return items, false, "", "", true
}
marker := gjson.ParseBytes(items[0])
deleted = marker.Get("deleted").Bool()
generation = strings.TrimSpace(marker.Get("generation").String())
branch = strings.TrimSpace(marker.Get("branch").String())
return items[1:], deleted, generation, branch, true
}
func antigravityReasoningReplayItemsPrefix(prefix, items [][]byte) bool {
if len(prefix) > len(items) {
return false
}
for index := range prefix {
if !bytes.Equal(prefix[index], items[index]) {
return false
}
}
return true
}
func newAntigravityReasoningReplayTombstone() []byte {
marker := []byte(`{"type":"","generation":"","branch":"","deleted":true}`)
marker, _ = sjson.SetBytes(marker, "type", antigravityReasoningReplayGenerationItemType)
marker, _ = sjson.SetBytes(marker, "generation", newAntigravityReasoningReplayGeneration())
marker, _ = sjson.SetBytes(marker, "branch", newAntigravityReasoningReplayGeneration())
raw, _ := json.Marshal([][]byte{marker})
return raw
}
// ClearAntigravityReasoningReplayCache clears all Antigravity reasoning replay state.
func ClearAntigravityReasoningReplayCache() {
antigravityReasoningReplayMu.Lock()
antigravityReasoningReplayEntries = make(map[string]antigravityReasoningReplayEntry)
antigravityReasoningReplayEvictionEpoch++
antigravityReasoningReplayMu.Unlock()
}
func antigravityReasoningReplayCacheKey(modelName, sessionKey string) string {
modelName = strings.TrimSpace(modelName)
sessionKey = strings.TrimSpace(sessionKey)
if modelName == "" || sessionKey == "" {
return ""
}
// The session key is the continuity boundary. Keep this independent from
// the selected upstream Codex credential so auth failover can preserve replay.
return strings.Join([]string{"antigravity-reasoning-replay", modelName, sessionKey}, "\x00")
}
func antigravityReasoningReplayKVKey(modelName, sessionKey string) string {
return "cpa:antigravity:reasoning-replay:" + homekv.HashKeyPart(strings.TrimSpace(modelName)) + ":" + homekv.HashKeyPart(strings.TrimSpace(sessionKey))
}
func normalizeAntigravityReasoningReplayItems(items [][]byte) ([][]byte, bool) {
if len(items) > AntigravityReasoningReplayCacheMaxItemsPerEntry {
return nil, false
}
normalized := make([][]byte, 0, len(items))
totalBytes := 0
for _, item := range items {
normalizedItem, ok := normalizeAntigravityReasoningReplayItem(item)
if ok {
totalBytes += len(normalizedItem)
if totalBytes > AntigravityReasoningReplayCacheMaxBytesPerEntry {
return nil, false
}
normalized = append(normalized, normalizedItem)
}
}
return normalized, len(normalized) > 0
}
func normalizeAntigravityReasoningReplayItem(item []byte) ([]byte, bool) {
itemResult := gjson.ParseBytes(item)
switch strings.TrimSpace(itemResult.Get("type").String()) {
case "thought_signature":
return normalizeAntigravityThoughtSignatureReplayItem(itemResult)
case "function_call_part":
return normalizeAntigravityFunctionCallPartReplayItem(itemResult)
default:
return nil, false
}
}
func normalizeAntigravityThoughtSignatureReplayItem(itemResult gjson.Result) ([]byte, bool) {
sig := strings.TrimSpace(itemResult.Get("thoughtSignature").String())
if sig == "" {
sig = strings.TrimSpace(itemResult.Get("thought_signature").String())
}
if sig == "" || sig == "skip_thought_signature_validator" || len(sig) < minAntigravityThoughtSignatureReplayLen {
return nil, false
}
normalized := []byte(`{"type":"thought_signature"}`)
normalized, _ = sjson.SetBytes(normalized, "thoughtSignature", sig)
if contentIndex := itemResult.Get("contentIndex"); contentIndex.Type == gjson.Number {
normalized, _ = sjson.SetBytes(normalized, "contentIndex", contentIndex.Int())
}
if partIndex := itemResult.Get("partIndex"); partIndex.Type == gjson.Number {
normalized, _ = sjson.SetBytes(normalized, "partIndex", partIndex.Int())
}
if targetKind := strings.TrimSpace(itemResult.Get("targetKind").String()); targetKind == "text" || targetKind == "thought" {
normalized, _ = sjson.SetBytes(normalized, "targetKind", targetKind)
}
if targetHash := strings.TrimSpace(itemResult.Get("targetHash").String()); targetHash != "" {
normalized, _ = sjson.SetBytes(normalized, "targetHash", targetHash)
}
if targetOccurrence := itemResult.Get("targetOccurrence"); targetOccurrence.Type == gjson.Number && targetOccurrence.Int() >= 0 {
normalized, _ = sjson.SetBytes(normalized, "targetOccurrence", targetOccurrence.Int())
}
if contextHash := strings.TrimSpace(itemResult.Get("contextHash").String()); contextHash != "" {
normalized, _ = sjson.SetBytes(normalized, "contextHash", contextHash)
}
return normalized, true
}
func normalizeAntigravityFunctionCallPartReplayItem(itemResult gjson.Result) ([]byte, bool) {
callID := strings.TrimSpace(itemResult.Get("call_id").String())
if callID == "" {
callID = strings.TrimSpace(itemResult.Get("id").String())
}
name := strings.TrimSpace(itemResult.Get("name").String())
args := itemResult.Get("args")
if name == "" || !args.Exists() {
fc := itemResult.Get("functionCall")
if fc.Exists() {
if callID == "" {
callID = strings.TrimSpace(fc.Get("id").String())
}
if name == "" {
name = strings.TrimSpace(fc.Get("name").String())
}
if !args.Exists() {
args = fc.Get("args")
}
}
}
if name == "" || !args.Exists() {
return nil, false
}
normalized := []byte(`{"type":"function_call_part"}`)
if callID != "" {
normalized, _ = sjson.SetBytes(normalized, "call_id", callID)
}
normalized, _ = sjson.SetBytes(normalized, "name", name)
if args.Type == gjson.String {
normalized, _ = sjson.SetBytes(normalized, "args", args.String())
} else {
normalized, _ = sjson.SetRawBytes(normalized, "args", []byte(args.Raw))
}
sig := strings.TrimSpace(itemResult.Get("thoughtSignature").String())
if sig != "" && sig != "skip_thought_signature_validator" {
normalized, _ = sjson.SetBytes(normalized, "thoughtSignature", sig)
}
if contentIndex := itemResult.Get("contentIndex"); contentIndex.Type == gjson.Number {
normalized, _ = sjson.SetBytes(normalized, "contentIndex", contentIndex.Int())
}
if partIndex := itemResult.Get("partIndex"); partIndex.Type == gjson.Number {
normalized, _ = sjson.SetBytes(normalized, "partIndex", partIndex.Int())
}
if targetOccurrence := itemResult.Get("targetOccurrence"); targetOccurrence.Type == gjson.Number && targetOccurrence.Int() >= 0 {
normalized, _ = sjson.SetBytes(normalized, "targetOccurrence", targetOccurrence.Int())
}
if contextHash := strings.TrimSpace(itemResult.Get("contextHash").String()); contextHash != "" {
normalized, _ = sjson.SetBytes(normalized, "contextHash", contextHash)
}
return normalized, true
}
func cloneAntigravityReasoningReplayItems(items [][]byte) [][]byte {
cloned := make([][]byte, 0, len(items))
for _, item := range items {
cloned = append(cloned, append([]byte(nil), item...))
}
return cloned
}
func evictOldestAntigravityReasoningReplayEntries(count int) {
if count <= 0 || len(antigravityReasoningReplayEntries) == 0 {
return
}
type candidate struct {
key string
timestamp time.Time
}
candidates := make([]candidate, 0, len(antigravityReasoningReplayEntries))
for key, entry := range antigravityReasoningReplayEntries {
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++ {
antigravityReasoningReplayEvictionEpoch++
delete(antigravityReasoningReplayEntries, candidates[i].key)
}
}
func purgeExpiredAntigravityReasoningReplayCache(now time.Time) {
antigravityReasoningReplayMu.Lock()
for key, entry := range antigravityReasoningReplayEntries {
if now.Sub(entry.Timestamp) > AntigravityReasoningReplayCacheTTL {
antigravityReasoningReplayEvictionEpoch++
delete(antigravityReasoningReplayEntries, key)
}
}
antigravityReasoningReplayMu.Unlock()
}