656 lines
27 KiB
Go
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()
|
|
}
|