package executor import ( "bytes" "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "hash" "io" "net/http" "reflect" "strings" internalcache "github.com/router-for-me/CLIProxyAPI/v7/internal/cache" homekv "github.com/router-for-me/CLIProxyAPI/v7/internal/home" "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps" internalsignature "github.com/router-for-me/CLIProxyAPI/v7/internal/signature" "github.com/router-for-me/CLIProxyAPI/v7/internal/util" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) // antigravityReplayLogKey returns a short, non-reversible tag for a replay // identifier. Session keys and tool call IDs are never logged verbatim. func antigravityReplayLogKey(value string) string { value = strings.TrimSpace(value) if value == "" { return "" } sum := sha256.Sum256([]byte(value)) return fmt.Sprintf("%x", sum[:8]) } // antigravityCountClaudeToolProvenanceIDs reports how many reserved // Claude-facing provenance IDs are still present in a Gemini-shaped payload. func antigravityCountClaudeToolProvenanceIDs(payload []byte) int { count := 0 contents := util.GetGJSONBytesNoCopy(payload, "request.contents") if !contents.IsArray() { return 0 } contents.ForEach(func(_, content gjson.Result) bool { parts := content.Get("parts") countPart := func(part gjson.Result) { for _, path := range []string{"functionCall.id", "functionResponse.id"} { if util.IsGeminiClaudeToolUseID(part.Get(path).String()) { count++ } } } if parts.IsArray() { parts.ForEach(func(_, part gjson.Result) bool { countPart(part) return true }) } else if parts.Type != gjson.Null { // Result.Array returns a non-array JSON value as one item. countPart(parts) } return true }) return count } type antigravityReasoningReplayScope struct { modelName string sessionKey string cacheSnapshot internalcache.AntigravityReasoningReplaySnapshot } func (s antigravityReasoningReplayScope) valid() bool { return strings.TrimSpace(s.modelName) != "" && strings.TrimSpace(s.sessionKey) != "" } func antigravityReasoningReplayScopeFromPayload(modelName string, payload []byte) antigravityReasoningReplayScope { sessionID := antigravityReplaySessionIDFromPayload(payload) if sessionID == "" { if stable := strings.TrimSpace(generateStableSessionID(payload)); stable != "" { sessionID = strings.TrimPrefix(stable, "-") if sessionID == "" { sessionID = stable } } } if sessionID == "" { return antigravityReasoningReplayScope{} } return antigravityReasoningReplayScope{ modelName: strings.TrimSpace(modelName), sessionKey: "session:" + sessionID, } } func antigravityReasoningReplayScopeFromRequest(ctx context.Context, modelName string, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, payload []byte) antigravityReasoningReplayScope { // Prefer an explicit downstream session over a provider sessionId synthesized // from request text. This keeps identical prompts in separate client sessions // from sharing an opaque Gemini reasoning chain. if sessionKey := antigravityReasoningReplayClientSessionKey(ctx, req, opts); sessionKey != "" { return antigravityReasoningReplayScope{modelName: modelName, sessionKey: sessionKey} } if scope := antigravityReasoningReplayScopeFromPayload(modelName, payload); scope.valid() { return scope } if scope := antigravityReasoningReplayScopeFromPayload(modelName, req.Payload); scope.valid() { return scope } _ = ctx return antigravityReasoningReplayScope{} } func antigravityReasoningReplayClientSessionKey(ctx context.Context, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) string { for _, raw := range [][]byte{opts.OriginalRequest, req.Payload} { if scope, ok := helps.ClaudeCodeExecutionScope(ctx, raw, opts.Headers); ok { if lane := antigravityClaudeReplaySystemLane(raw); lane != "" { return scope + ":context:" + lane } return scope } } if value := strings.TrimSpace(opts.Headers.Get("Session-Id")); value != "" { return "responses:" + value } for _, raw := range [][]byte{opts.OriginalRequest, req.Payload} { if len(raw) == 0 { continue } for _, path := range []string{"session_id", "metadata.session_id"} { if value := strings.TrimSpace(gjson.GetBytes(raw, path).String()); value != "" { return "responses:" + value } } } if value := metadataString(opts.Metadata, cliproxyexecutor.ExecutionSessionMetadataKey); value != "" { return "execution:" + value } if value := metadataString(req.Metadata, cliproxyexecutor.ExecutionSessionMetadataKey); value != "" { return "execution:" + value } for _, raw := range [][]byte{opts.OriginalRequest, req.Payload} { if value := strings.TrimSpace(gjson.GetBytes(raw, "prompt_cache_key").String()); value != "" { return "prompt-cache:" + value } } if value := helps.DerivedSessionID(opts.Metadata, req.Metadata); value != "" { return "derived:" + value } return "" } func antigravityClaudeReplaySystemLane(payload []byte) string { system := util.GetGJSONBytesNoCopy(payload, "system") if !system.Exists() { return "" } var value any if errUnmarshal := json.Unmarshal([]byte(system.Raw), &value); errUnmarshal != nil { return "" } value = antigravityClaudeReplayNormalizeSystem(value) normalized, errMarshal := json.Marshal(value) if errMarshal != nil { return "" } sum := sha256.Sum256(normalized) return fmt.Sprintf("%x", sum[:16]) } func antigravityClaudeReplayNormalizeSystem(value any) any { switch typed := value.(type) { case map[string]any: normalized := make(map[string]any, len(typed)) for key, child := range typed { if strings.EqualFold(strings.TrimSpace(key), "cache_control") { continue } normalized[key] = antigravityClaudeReplayNormalizeSystem(child) } return normalized case []any: normalized := make([]any, len(typed)) for index, child := range typed { normalized[index] = antigravityClaudeReplayNormalizeSystem(child) } return normalized default: return value } } func antigravityReplaySessionIDFromPayload(payload []byte) string { if len(payload) == 0 { return "" } for _, path := range []string{"sessionId", "session_id", "request.sessionId", "request.session_id"} { if id := strings.TrimSpace(gjson.GetBytes(payload, path).String()); id != "" { return id } } return "" } func antigravityReasoningReplayResolveContentIndex(payload []byte, cached int) int { contents := util.GetGJSONBytesNoCopy(payload, "request.contents") if !contents.IsArray() { return cached } arr := contents.Array() if cached >= 0 && cached < len(arr) { return cached } return -1 } // logAntigravityReasoningReplayDegraded reports that a replay-state operation // failed and the request continued without it. A Home that predates the CAS // command fails every call, and the Home client already warns once about that, // so those are logged at debug level to avoid one warning per request. func logAntigravityReasoningReplayDegraded(scope antigravityReasoningReplayScope, stage string, err error) { if err == nil { return } if errors.Is(err, homekv.ErrCompareAndSwapUnsupported) { log.Debugf("antigravity executor: reasoning replay %s unavailable on this Home (session=%s): %v", stage, antigravityReplayLogKey(scope.sessionKey), err) return } log.Warnf("antigravity executor: reasoning replay %s failed; continuing without replay (session=%s): %v", stage, antigravityReplayLogKey(scope.sessionKey), err) } func prepareAntigravityGeminiReasoningReplayPayload(ctx context.Context, modelName string, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, payload []byte) ([]byte, antigravityReasoningReplayScope, error) { if !antigravityUsesReasoningReplayCache(modelName) { return payload, antigravityReasoningReplayScope{}, nil } updated, scope, replayApplied, errReplay := applyAntigravityReasoningReplayCache(ctx, modelName, req, opts, payload) if errReplay != nil { // Replay state is an optimization, not a correctness requirement: a ledger // miss is already a tolerated outcome below. Failing the request here would // surface as an untyped executor error, which MarkResult treats as a // credential fault and uses to mark every candidate credential unavailable. // Degrade to "no replay this turn" instead. logAntigravityReasoningReplayDegraded(scope, "read", errReplay) updated = payload } updated = normalizeAntigravityGeminiFunctionResponseRoles(updated) if antigravityPayloadHasClaudeToolProvenanceID(updated) { // The replay ledger could not resolve every tool ID — the session lane // changed, the entry expired, the process restarted, or a turn never // committed. Degrade those calls instead of killing the conversation. degradedPayload, degradedCount := degradeAntigravityClaudeToolProvenanceIDs(updated) log.Warnf("antigravity executor: replay state missing for %d tool ID(s); rewriting them to synthetic IDs and continuing without reasoning replay for those calls", degradedCount) updated = degradedPayload } // An identity-only restore drops the cached signature, which can leave a model // turn's first function call unsigned. Gemini rejects that, so re-assert the // invariant the pre-replay sanitizer established. updated = antigravityRepairUnsignedFirstFunctionCalls(updated) if errPairing := internalsignature.ValidateGeminiFunctionCallPairing(updated); errPairing != nil { originalPairingValid := internalsignature.ValidateGeminiFunctionCallPairing(payload) == nil if replayApplied && originalPairingValid && scope.valid() { if _, errDelete := internalcache.DeleteAntigravityReasoningReplayItemsIfUnchanged(ctx, scope.modelName, scope.sessionKey, scope.cacheSnapshot); errDelete != nil { // Invalidation is best-effort cleanup. Returning it here would replace // the pairing diagnosis below with an untyped error. logAntigravityReasoningReplayDegraded(scope, "invalidate", errDelete) } } return payload, scope, statusErr{code: http.StatusBadRequest, msg: fmt.Sprintf("antigravity executor: invalid Gemini function call history: %v", errPairing)} } return updated, scope, nil } func clearAntigravityReasoningReplayOnInvalidSignature(ctx context.Context, scope antigravityReasoningReplayScope, statusCode int, body []byte) error { if !scope.valid() { return nil } if statusCode != http.StatusBadRequest { return nil } bodyText := strings.ToLower(string(body)) if !strings.Contains(bodyText, "thoughtsignature") && !strings.Contains(bodyText, "thought_signature") && !strings.Contains(bodyText, "signature") { return nil } _, errDelete := internalcache.DeleteAntigravityReasoningReplayItemsIfUnchanged(ctx, scope.modelName, scope.sessionKey, scope.cacheSnapshot) return errDelete } func applyAntigravityReasoningReplayCache(ctx context.Context, modelName string, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, payload []byte) ([]byte, antigravityReasoningReplayScope, bool, error) { scope := antigravityReasoningReplayScopeFromRequest(ctx, modelName, req, opts, payload) if !scope.valid() { return payload, scope, false, nil } items, snapshot, ok, err := internalcache.GetAntigravityReasoningReplayItemsWithSnapshotRequired(ctx, scope.modelName, scope.sessionKey) scope.cacheSnapshot = snapshot reservedBefore := antigravityCountClaudeToolProvenanceIDs(payload) if err != nil || !ok || len(items) == 0 { // A ledger miss on a payload that still carries reserved provenance IDs is // the signature of a session/lane switch, cache expiry, or a turn that never // committed. Log it so the two failure families stay distinguishable. if reservedBefore > 0 { log.Debugf("antigravity replay: ledger miss with %d reserved tool provenance ID(s) present (session=%s found=%t)", reservedBefore, antigravityReplayLogKey(scope.sessionKey), ok) } return payload, scope, false, err } var toolSchemas map[string]any if opts.SourceFormat.String() == "claude" { toolSchemas = antigravityReplayToolSchemasFromRequests(opts.OriginalRequest, req.Payload) } updated, changed := applyAntigravityReasoningReplayItems(payload, items, toolSchemas) if reservedBefore > 0 { log.Debugf("antigravity replay: ledger items=%d reserved before=%d after=%d applied=%t (session=%s)", len(items), reservedBefore, antigravityCountClaudeToolProvenanceIDs(updated), changed, antigravityReplayLogKey(scope.sessionKey)) } if !changed { return payload, scope, false, nil } return updated, scope, true, nil } func applyAntigravityReasoningReplayItems(payload []byte, items [][]byte, toolSchemas map[string]any) ([]byte, bool) { updated := payload changed := false index := newAntigravityReplayRequestIndex(updated) for itemIndex, item := range items { eligible := filterAntigravityReasoningReplayItemsForRequestWithIndex(index, [][]byte{item}, toolSchemas) if len(eligible) != 1 { continue } next, applied := insertAntigravityReasoningReplayItemsWithSchemas(index, updated, eligible, toolSchemas) if !applied { continue } updated = next changed = true // Replay application is intentionally sequential. Rebuild only after a // mutation so later items observe exactly the same payload as before. // The final item has no successor, so its rebuild would never be read. if itemIndex+1 < len(items) { index = newAntigravityReplayRequestIndex(updated) } } return updated, changed } func filterAntigravityReasoningReplayItemsForRequestWithSchemas(payload []byte, items [][]byte, toolSchemas map[string]any) [][]byte { index := newAntigravityReplayRequestIndex(payload) return filterAntigravityReasoningReplayItemsForRequestWithIndex(index, items, toolSchemas) } func filterAntigravityReasoningReplayItemsForRequestWithIndex( index *antigravityReplayRequestIndex, items [][]byte, toolSchemas map[string]any, ) [][]byte { filtered := make([][]byte, 0, len(items)) for _, item := range items { itemResult := gjson.ParseBytes(item) switch strings.TrimSpace(itemResult.Get("type").String()) { case "function_call_part": signature := strings.TrimSpace(itemResult.Get("thoughtSignature").String()) if location, foundCall := index.functionCallPartLocationForReplayWithSchemas(itemResult, toolSchemas); foundCall { currentID := strings.TrimSpace(location.functionCall.Get("id").String()) nativeID := strings.TrimSpace(itemResult.Get("call_id").String()) needsNativeRestore := currentID != nativeID || !bytes.Equal( antigravityCanonicalReplayJSON([]byte(location.functionCall.Get("args").Raw)), antigravityCanonicalReplayJSON([]byte(itemResult.Get("args").Raw)), ) if !needsNativeRestore && (signature == "" || antigravityHasNativeThoughtSignature(location.part.Get("thoughtSignature").String())) { continue } break } // Even without a context match, an exact opaque ID match can still // restore the native call identity. if _, foundProvenance := index.functionCallProvenanceLocation(itemResult, toolSchemas); foundProvenance { break } callID := strings.TrimSpace(itemResult.Get("call_id").String()) if callID == "" { continue } responseIndex, _, foundResponse := index.functionResponseContentIndexForReplay(itemResult) if !foundResponse { continue } contextMatches := index.contextMatches(itemResult, responseIndex) if !contextMatches && responseIndex > 0 { previousRole := index.contents[responseIndex-1].content.Get("role").String() contextMatches = strings.EqualFold(strings.TrimSpace(previousRole), "model") && index.contextMatches(itemResult, responseIndex-1) } if !contextMatches { continue } case "thought_signature": if index.hasThoughtSignatureAt(itemResult) { continue } default: continue } filtered = append(filtered, item) } return filtered } func antigravityExistingToolCallKeys(payload []byte) map[string]bool { existing := make(map[string]bool) contents := util.GetGJSONBytesNoCopy(payload, "request.contents") if !contents.IsArray() { return existing } for _, content := range contents.Array() { parts := content.Get("parts") if !parts.IsArray() { continue } for _, part := range parts.Array() { if fc := part.Get("functionCall"); fc.Exists() { for _, key := range antigravityReplayToolCallKeysFromPart(fc) { existing[key] = true } } } } return existing } func antigravityReplayToolCallKeys(itemResult gjson.Result) []string { callID := strings.TrimSpace(itemResult.Get("call_id").String()) if callID == "" { callID = strings.TrimSpace(itemResult.Get("id").String()) } name := strings.TrimSpace(itemResult.Get("name").String()) if name == "" { return nil } args := itemResult.Get("args").Raw key := antigravityFunctionCallKey(name, args, callID) if key == "" { return nil } return []string{key} } func antigravityReplayToolCallKeysFromPart(fc gjson.Result) []string { return antigravityReplayToolCallKeys(gjson.Parse(fc.Raw)) } func antigravityFunctionCallKey(name, argsRaw, callID string) string { name = strings.TrimSpace(name) if name == "" { return "" } if strings.TrimSpace(argsRaw) != "" { argsRaw = string(antigravityCanonicalReplayJSON([]byte(argsRaw))) } h := sha256.Sum256([]byte(strings.Join([]string{name, argsRaw, callID}, "\x00"))) return fmt.Sprintf("fc:%x", h[:8]) } func antigravityAnyKeyExists(existing map[string]bool, keys []string) bool { for _, key := range keys { if existing[key] { return true } } return false } func restoreAntigravityFunctionResponseReplayIdentity(payload []byte, currentID, nativeID, nativeName string) []byte { currentID = strings.TrimSpace(currentID) nativeID = strings.TrimSpace(nativeID) nativeName = strings.TrimSpace(nativeName) if currentID == "" || nativeID == "" || nativeName == "" || currentID == nativeID { return payload } out := payload contents := util.GetGJSONBytesNoCopy(out, "request.contents") contents.ForEach(func(contentKey, content gjson.Result) bool { content.Get("parts").ForEach(func(partKey, part gjson.Result) bool { response := part.Get("functionResponse") if !response.Exists() || strings.TrimSpace(response.Get("id").String()) != currentID { return true } responsePath := fmt.Sprintf("request.contents.%d.parts.%d.functionResponse", contentKey.Int(), partKey.Int()) out, _ = sjson.SetBytes(out, responsePath+".id", nativeID) out, _ = sjson.SetBytes(out, responsePath+".name", nativeName) return true }) return true }) return out } func (i *antigravityReplayRequestIndex) functionResponseContentIndexForReplay(itemResult gjson.Result) (int, string, bool) { callID := strings.TrimSpace(itemResult.Get("call_id").String()) name := strings.TrimSpace(itemResult.Get("name").String()) args := itemResult.Get("args") candidateIDs := []string{callID} if stableID := util.GeminiClaudeToolUseID(callID, name, args.Raw); stableID != "" && stableID != callID { candidateIDs = append(candidateIDs, stableID) } for _, candidateID := range candidateIDs { if contentIndex, ok := i.functionResponseContentIndex(candidateID); ok { return contentIndex, candidateID, true } } return -1, "", false } func (i *antigravityReplayRequestIndex) functionCallPartLocationForReplayWithSchemas( itemResult gjson.Result, toolSchemas map[string]any, ) (antigravityReplayIndexedPart, bool) { name := strings.TrimSpace(itemResult.Get("name").String()) args := itemResult.Get("args") if name == "" || !args.Exists() { return antigravityReplayIndexedPart{}, false } callID := strings.TrimSpace(itemResult.Get("call_id").String()) if callID == "" { callID = strings.TrimSpace(itemResult.Get("id").String()) } stableID := util.GeminiClaudeToolUseID(callID, name, args.Raw) candidateIDs := []string{callID} if stableID != "" && stableID != callID { candidateIDs = append(candidateIDs, stableID) } for _, candidateID := range candidateIDs { if candidateID == "" { continue } location, found := i.functionCallPartLocation(candidateID) if !found { continue } if i.contextMatches(itemResult, location.contentIndex) { if antigravityFunctionCallMatchesReplayItem(location.functionCall, itemResult, toolSchemas) { return location, true } log.Debugf("antigravity replay: located call %q at contents[%d].parts[%d] but name/args did not match ledger item (opaque_id=%t)", name, location.contentIndex, location.partIndex, util.IsGeminiClaudeToolUseID(candidateID)) return antigravityReplayIndexedPart{}, false } // The candidate ID matched exactly, so callID+name+args are already proven // identical. Only the surrounding context drifted, which invalidates the // cached signature but not the tool identity. log.Debugf("antigravity replay: exact tool ID match for %q at contents[%d].parts[%d] rejected by context hash (opaque_id=%t)", name, location.contentIndex, location.partIndex, util.IsGeminiClaudeToolUseID(candidateID)) return antigravityReplayIndexedPart{}, false } cachedContentIndex := int(itemResult.Get("contentIndex").Int()) if targetOccurrence := itemResult.Get("targetOccurrence"); targetOccurrence.Exists() { if cachedContentIndex < 0 || cachedContentIndex >= len(i.contents) || !i.contextMatches(itemResult, cachedContentIndex) { return antigravityReplayIndexedPart{}, false } wantedOccurrence := int(targetOccurrence.Int()) occurrence := 0 for partIndex, part := range i.contents[cachedContentIndex].parts { functionCall := part.Get("functionCall") functionCallID := functionCall.Get("id").String() mismatchedOpaqueID := util.IsGeminiClaudeToolUseID(functionCallID) && functionCallID != stableID if !functionCall.Exists() || mismatchedOpaqueID || !antigravityFunctionCallMatchesReplayItem(functionCall, itemResult, toolSchemas) { continue } if occurrence == wantedOccurrence { return antigravityReplayIndexedPart{ contentIndex: cachedContentIndex, partIndex: partIndex, part: part, functionCall: functionCall, }, true } occurrence++ } return antigravityReplayIndexedPart{}, false } matches := make([]antigravityReplayIndexedPart, 0, 1) for contentIndex, content := range i.contents { if !i.contextMatches(itemResult, contentIndex) { continue } for partIndex, part := range content.parts { functionCall := part.Get("functionCall") functionCallID := functionCall.Get("id").String() mismatchedOpaqueID := util.IsGeminiClaudeToolUseID(functionCallID) && functionCallID != stableID if !functionCall.Exists() || mismatchedOpaqueID { continue } if antigravityFunctionCallMatchesReplayItem(functionCall, itemResult, toolSchemas) { matches = append(matches, antigravityReplayIndexedPart{ contentIndex: contentIndex, partIndex: partIndex, part: part, functionCall: functionCall, }) } } } if len(matches) == 1 { return matches[0], true } return antigravityReplayIndexedPart{}, false } func (i *antigravityReplayRequestIndex) functionCallProvenanceLocation( itemResult gjson.Result, toolSchemas map[string]any, ) (antigravityReplayIndexedPart, bool) { name := strings.TrimSpace(itemResult.Get("name").String()) args := itemResult.Get("args") callID := strings.TrimSpace(itemResult.Get("call_id").String()) if name == "" || !args.Exists() || callID == "" { return antigravityReplayIndexedPart{}, false } stableID := util.GeminiClaudeToolUseID(callID, name, args.Raw) if stableID == "" || stableID == callID { return antigravityReplayIndexedPart{}, false } location, found := i.functionCallPartLocation(stableID) if !found || !antigravityFunctionCallMatchesReplayItem(location.functionCall, itemResult, toolSchemas) { return antigravityReplayIndexedPart{}, false } return location, true } // thoughtSignaturePartIndex resolves the part a thought_signature item belongs // to. It is the single locator shared by the eligibility check and the write // path, so the two can never disagree about the target part. // // A target hash pins the signature to a part whose own bytes are unchanged, // which is all Gemini validates: the signature's own integrity, never its // binding to the surrounding history. Drift elsewhere in the conversation // therefore costs this signature nothing, so it is deliberately not gated on // the context fingerprint. The positional fallback below has no such proof and // stays gated. func (i *antigravityReplayRequestIndex) thoughtSignaturePartIndex(itemResult gjson.Result) (contentIndex int, partIndex int, ok bool) { contentIndex = int(itemResult.Get("contentIndex").Int()) if i == nil || contentIndex < 0 || contentIndex >= len(i.contents) { return -1, -1, false } content := i.contents[contentIndex] if !strings.EqualFold(strings.TrimSpace(content.content.Get("role").String()), "model") { return -1, -1, false } parts := content.parts targetKind := strings.TrimSpace(itemResult.Get("targetKind").String()) targetHash := strings.TrimSpace(itemResult.Get("targetHash").String()) partIndex = -1 if targetHash != "" { if targetOccurrence := itemResult.Get("targetOccurrence"); targetOccurrence.Exists() { wantedOccurrence := int(targetOccurrence.Int()) occurrence := 0 for candidateIndex, part := range parts { kind, fingerprint := antigravityReplayPartFingerprint(part) if fingerprint != targetHash || (targetKind != "" && kind != targetKind) { continue } if occurrence == wantedOccurrence { partIndex = candidateIndex break } occurrence++ } } else { candidateIndex := int(itemResult.Get("partIndex").Int()) if candidateIndex >= 0 && candidateIndex < len(parts) { kind, fingerprint := antigravityReplayPartFingerprint(parts[candidateIndex]) if fingerprint == targetHash && (targetKind == "" || kind == targetKind) { partIndex = candidateIndex } } if partIndex < 0 { for candidateIndex, part := range parts { kind, fingerprint := antigravityReplayPartFingerprint(part) if fingerprint == targetHash && (targetKind == "" || kind == targetKind) { partIndex = candidateIndex break } } } } } else { // No target hash: nothing proves which part this signature belongs to, so // only a matching context fingerprint makes the positional guess safe. if !i.contextMatches(itemResult, contentIndex) { return -1, -1, false } candidateIndex := int(itemResult.Get("partIndex").Int()) if candidateIndex >= 0 && candidateIndex < len(parts) && parts[candidateIndex].Type != gjson.Null { if kind, _ := antigravityReplayPartFingerprint(parts[candidateIndex]); kind != "" { partIndex = candidateIndex } } // Legacy cache entries may point at a streamed signature-only part after // multiple text chunks. Attach them to the last semantic part in the same // model content, never to a different turn. if partIndex < 0 { for candidateIndex := len(parts) - 1; candidateIndex >= 0; candidateIndex-- { if kind, _ := antigravityReplayPartFingerprint(parts[candidateIndex]); kind != "" { partIndex = candidateIndex break } } } } if partIndex < 0 { return -1, -1, false } return contentIndex, partIndex, true } func (i *antigravityReplayRequestIndex) hasThoughtSignatureAt(itemResult gjson.Result) bool { contentIndex, partIndex, ok := i.thoughtSignaturePartIndex(itemResult) if !ok { return false } part := i.contents[contentIndex].parts[partIndex] return antigravityHasNativeThoughtSignature(part.Get("thoughtSignature").String()) } func (i *antigravityReplayRequestIndex) thoughtSignatureReplayPartPath(itemResult gjson.Result) (string, bool) { contentIndex, partIndex, ok := i.thoughtSignaturePartIndex(itemResult) if !ok { return "", false } return fmt.Sprintf("request.contents.%d.parts.%d", contentIndex, partIndex), true } func insertAntigravityModelFunctionCallBeforeContent(payload []byte, beforeIndex int, name, callID, thoughtSig string, args gjson.Result) ([]byte, bool) { contents := util.GetGJSONBytesNoCopy(payload, "request.contents") if !contents.IsArray() { return payload, false } arr := contents.Array() if beforeIndex < 0 || beforeIndex > len(arr) { return payload, false } fc := map[string]any{"name": name} if callID != "" { fc["id"] = callID } if args.Exists() { fc["args"] = args.Value() } part := map[string]any{"functionCall": fc} if thoughtSig == "" { thoughtSig = "skip_thought_signature_validator" } part["thoughtSignature"] = thoughtSig newContent := map[string]any{ "role": "model", "parts": []any{part}, } newArr := make([]any, 0, len(arr)+1) for i := 0; i < beforeIndex; i++ { newArr = append(newArr, arr[i].Value()) } newArr = append(newArr, newContent) for i := beforeIndex; i < len(arr); i++ { newArr = append(newArr, arr[i].Value()) } updated, err := sjson.SetBytes(payload, "request.contents", newArr) if err != nil { return payload, false } return updated, true } func appendAntigravityFunctionCallToModelContent(payload []byte, contentIndex int, name, callID, thoughtSig string, args gjson.Result) ([]byte, bool) { contentPath := fmt.Sprintf("request.contents.%d", contentIndex) if !strings.EqualFold(strings.TrimSpace(gjson.GetBytes(payload, contentPath+".role").String()), "model") || !gjson.GetBytes(payload, contentPath+".parts").IsArray() { return payload, false } fc := map[string]any{"name": name} if callID != "" { fc["id"] = callID } if args.Exists() { fc["args"] = args.Value() } part := map[string]any{"functionCall": fc} if thoughtSig == "" { hasFunctionCall := false gjson.GetBytes(payload, contentPath+".parts").ForEach(func(_, existingPart gjson.Result) bool { hasFunctionCall = existingPart.Get("functionCall").Exists() return !hasFunctionCall }) if !hasFunctionCall { thoughtSig = "skip_thought_signature_validator" } } if thoughtSig != "" { part["thoughtSignature"] = thoughtSig } updated, errSet := sjson.SetBytes(payload, contentPath+".parts.-1", part) if errSet != nil { return payload, false } return updated, true } func antigravityRemoveThoughtSignatureFromOtherParts(payload []byte, contentIndex int, signature, keepPartPath string) []byte { signature = strings.TrimSpace(signature) partsPath := fmt.Sprintf("request.contents.%d.parts", contentIndex) parts := gjson.GetBytes(payload, partsPath) if signature == "" || !parts.IsArray() { return payload } out := payload for partIndex, part := range parts.Array() { partPath := fmt.Sprintf("%s.%d", partsPath, partIndex) if partPath == keepPartPath || antigravityNativePartThoughtSignature(part) != signature { continue } for _, field := range []string{"thoughtSignature", "thought_signature", "extra_content.google.thought_signature"} { out, _ = sjson.DeleteBytes(out, partPath+"."+field) } } return out } func antigravityHasNativeThoughtSignature(signature string) bool { signature = strings.TrimSpace(signature) return signature != "" && signature != "skip_thought_signature_validator" } func antigravityReplayPartFingerprint(part gjson.Result) (kind, fingerprint string) { if part.Get("functionCall").Exists() || part.Get("functionResponse").Exists() { return "", "" } text := part.Get("text") if !text.Exists() { return "", "" } kind = "text" if part.Get("thought").Bool() { kind = "thought" } sum := sha256.Sum256([]byte(kind + "\x00" + text.String())) return kind, fmt.Sprintf("%x", sum[:]) } func antigravityReplayPartOccurrence(parts []gjson.Result, targetPartIndex int, targetKind, targetHash string) int { occurrence := 0 for partIndex := 0; partIndex < targetPartIndex && partIndex < len(parts); partIndex++ { kind, fingerprint := antigravityReplayPartFingerprint(parts[partIndex]) if kind == targetKind && fingerprint == targetHash { occurrence++ } } return occurrence } type antigravityReplayIndexedPart struct { contentIndex int partIndex int part gjson.Result functionCall gjson.Result } type antigravityReplayIndexedContent struct { content gjson.Result parts []gjson.Result } // antigravityReplayRequestIndex is an immutable, request-scoped view over one // exact revision of a replay payload. It retains no-copy GJSON results that // alias the payload bytes and memoizes context fingerprints lazily, so it must // be discarded and rebuilt as soon as the payload changes, and it must never be // shared across goroutines. type antigravityReplayRequestIndex struct { validContents bool contents []antigravityReplayIndexedContent functionCallsByID map[string]antigravityReplayIndexedPart functionResponseContentByID map[string]int contextFingerprints *antigravityReplayContextFingerprints } func newAntigravityReplayRequestIndex(payload []byte) *antigravityReplayRequestIndex { index := &antigravityReplayRequestIndex{ functionCallsByID: make(map[string]antigravityReplayIndexedPart), functionResponseContentByID: make(map[string]int), } contentsResult := util.GetGJSONBytesNoCopy(payload, "request.contents") index.validContents = contentsResult.IsArray() if index.validContents { contents := contentsResult.Array() index.contents = make([]antigravityReplayIndexedContent, len(contents)) for contentIndex, content := range contents { indexedContent := antigravityReplayIndexedContent{content: content} partsResult := content.Get("parts") if partsResult.IsArray() { indexedContent.parts = partsResult.Array() } index.contents[contentIndex] = indexedContent for partIndex, part := range indexedContent.parts { if functionCall := part.Get("functionCall"); functionCall.Exists() { callID := strings.TrimSpace(functionCall.Get("id").String()) if _, exists := index.functionCallsByID[callID]; callID != "" && !exists { index.functionCallsByID[callID] = antigravityReplayIndexedPart{ contentIndex: contentIndex, partIndex: partIndex, part: part, functionCall: functionCall, } } } if functionResponse := part.Get("functionResponse"); functionResponse.Exists() { callID := strings.TrimSpace(functionResponse.Get("id").String()) if _, exists := index.functionResponseContentByID[callID]; callID != "" && !exists { index.functionResponseContentByID[callID] = contentIndex } } } } } index.contextFingerprints = newAntigravityReplayContextFingerprints(payload, index.contents, index.validContents) return index } func (i *antigravityReplayRequestIndex) functionCallPartLocation(callID string) (antigravityReplayIndexedPart, bool) { if i == nil { return antigravityReplayIndexedPart{}, false } location, ok := i.functionCallsByID[strings.TrimSpace(callID)] return location, ok } func (i *antigravityReplayRequestIndex) functionResponseContentIndex(callID string) (int, bool) { if i == nil { return -1, false } contentIndex, ok := i.functionResponseContentByID[strings.TrimSpace(callID)] return contentIndex, ok } func (i *antigravityReplayRequestIndex) contextFingerprint(beforeContentIndex int) string { if i == nil || i.contextFingerprints == nil { return "" } return i.contextFingerprints.at(beforeContentIndex) } func (i *antigravityReplayRequestIndex) contextMatches(itemResult gjson.Result, contentIndex int) bool { expected := strings.TrimSpace(itemResult.Get("contextHash").String()) return expected == "" || expected == i.contextFingerprint(contentIndex) } func (i *antigravityReplayRequestIndex) pendingModelContentIndex() (contentIndex int, basePartIndex int) { if i == nil || len(i.contents) == 0 { return 0, 0 } lastIndex := len(i.contents) - 1 last := i.contents[lastIndex] if strings.EqualFold(strings.TrimSpace(last.content.Get("role").String()), "model") { hasFunctionResponse := false for _, part := range last.parts { if part.Get("functionResponse").Exists() { hasFunctionResponse = true break } } if !hasFunctionResponse { return lastIndex, len(last.parts) } } return len(i.contents), 0 } // antigravityReplayContextFingerprints hashes the replay context incrementally, // snapshotting the running SHA-256 after every content boundary so that a // prefix lookup is O(1). Prefix sums are appended in content order on first // use, so at() mutates the running hasher and is not safe for concurrent use. type antigravityReplayContextFingerprints struct { valid bool contents []antigravityReplayIndexedContent hasher hash.Hash sums []string wroteBytes bool } func newAntigravityReplayContextFingerprints( payload []byte, contents []antigravityReplayIndexedContent, valid bool, ) *antigravityReplayContextFingerprints { fingerprints := &antigravityReplayContextFingerprints{ valid: valid, contents: contents, hasher: sha256.New(), } if !valid { fingerprints.sums = []string{""} return fingerprints } for _, path := range []string{"request.systemInstruction", "request.tools", "request.toolConfig"} { if value := util.GetGJSONBytesNoCopy(payload, path); value.Exists() { fingerprints.writeString(path) fingerprints.writeByte(0) fingerprints.write(antigravityCanonicalReplayJSON([]byte(value.Raw))) fingerprints.writeByte(0) } } fingerprints.sums = []string{fingerprints.sum()} return fingerprints } func (f *antigravityReplayContextFingerprints) write(data []byte) { if len(data) == 0 { return } _, _ = f.hasher.Write(data) f.wroteBytes = true } func (f *antigravityReplayContextFingerprints) writeString(value string) { if value == "" { return } _, _ = io.WriteString(f.hasher, value) f.wroteBytes = true } func (f *antigravityReplayContextFingerprints) writeByte(value byte) { f.write([]byte{value}) } // sum reports the empty fingerprint until at least one byte has been hashed, // which keeps an all-empty context indistinguishable from a missing one. func (f *antigravityReplayContextFingerprints) sum() string { if !f.wroteBytes { return "" } return hex.EncodeToString(f.hasher.Sum(nil)) } func (f *antigravityReplayContextFingerprints) at(beforeContentIndex int) string { if f == nil || !f.valid || beforeContentIndex < 0 || beforeContentIndex > len(f.contents) { return "" } for len(f.sums) <= beforeContentIndex { contentIndex := len(f.sums) - 1 content := f.contents[contentIndex] f.writeString(strings.ToLower(strings.TrimSpace(content.content.Get("role").String()))) f.writeByte(0) for _, part := range content.parts { normalized := []byte(part.Raw) for _, signaturePath := range []string{"thoughtSignature", "thought_signature", "extra_content.google.thought_signature"} { normalized, _ = sjson.DeleteBytes(normalized, signaturePath) } f.write(antigravityCanonicalReplayJSON(normalized)) f.writeByte(0) } f.sums = append(f.sums, f.sum()) } return f.sums[beforeContentIndex] } func antigravityReplayToolSchemasFromRequests(rawRequests ...[]byte) map[string]any { toolSchemas := make(map[string]any) for _, raw := range rawRequests { if len(raw) == 0 { continue } nameMap := util.SanitizedFunctionNameMap(raw) tools := util.GetGJSONBytesNoCopy(raw, "tools") if !tools.IsArray() { continue } for _, tool := range tools.Array() { candidates := []gjson.Result{tool} if function := tool.Get("function"); function.Exists() { candidates = append(candidates, function) } for _, candidate := range candidates { name := strings.TrimSpace(candidate.Get("name").String()) if name == "" { continue } var schema gjson.Result for _, path := range []string{"input_schema", "parameters", "parametersJsonSchema"} { if value := candidate.Get(path); value.Exists() && value.IsObject() { schema = value break } } if !schema.Exists() { continue } var schemaValue any if json.Unmarshal([]byte(schema.Raw), &schemaValue) != nil { continue } for _, schemaName := range []string{name, util.MapSanitizedFunctionName(nameMap, name)} { if schemaName == "" { continue } if _, exists := toolSchemas[schemaName]; !exists { toolSchemas[schemaName] = schemaValue } } } } } return toolSchemas } func antigravityReplayJSONValue(result gjson.Result) (any, bool) { raw := result.Raw if result.Type == gjson.String { raw = result.String() } var value any if strings.TrimSpace(raw) == "" || json.Unmarshal([]byte(raw), &value) != nil { return nil, false } return value, true } func antigravityNormalizeReplayToolValue(value, schema any) any { schemaObject, _ := schema.(map[string]any) switch typed := value.(type) { case map[string]any: normalized := make(map[string]any, len(typed)) properties, _ := schemaObject["properties"].(map[string]any) for key, child := range typed { childSchema := properties[key] normalizedChild := antigravityNormalizeReplayToolValue(child, childSchema) if propertySchema, ok := childSchema.(map[string]any); ok { if defaultValue, hasDefault := propertySchema["default"]; hasDefault && reflect.DeepEqual(normalizedChild, antigravityNormalizeReplayToolValue(defaultValue, childSchema)) { continue } } normalized[key] = normalizedChild } return normalized case []any: itemSchema := schemaObject["items"] normalized := make([]any, len(typed)) for index, child := range typed { normalized[index] = antigravityNormalizeReplayToolValue(child, itemSchema) } return normalized default: return value } } func antigravityFunctionCallMatchesReplayItem(functionCall, itemResult gjson.Result, toolSchemas map[string]any) bool { name := strings.TrimSpace(itemResult.Get("name").String()) if name == "" || strings.TrimSpace(functionCall.Get("name").String()) != name { return false } currentArgs := functionCall.Get("args") nativeArgs := itemResult.Get("args") if !currentArgs.Exists() || !nativeArgs.Exists() { return false } if bytes.Equal(antigravityCanonicalReplayJSON([]byte(currentArgs.Raw)), antigravityCanonicalReplayJSON([]byte(nativeArgs.Raw))) { return true } schema, okSchema := toolSchemas[name] if !okSchema { return false } currentValue, okCurrent := antigravityReplayJSONValue(currentArgs) nativeValue, okNative := antigravityReplayJSONValue(nativeArgs) if !okCurrent || !okNative { return false } return reflect.DeepEqual(antigravityNormalizeReplayToolValue(currentValue, schema), antigravityNormalizeReplayToolValue(nativeValue, schema)) } func antigravityPayloadHasClaudeToolProvenanceID(payload []byte) bool { contents := util.GetGJSONBytesNoCopy(payload, "request.contents") if !contents.IsArray() { return false } found := false contents.ForEach(func(_, content gjson.Result) bool { parts := content.Get("parts") hasReservedID := func(part gjson.Result) bool { for _, path := range []string{"functionCall.id", "functionResponse.id"} { if util.IsGeminiClaudeToolUseID(part.Get(path).String()) { return true } } return false } if parts.IsArray() { parts.ForEach(func(_, part gjson.Result) bool { found = hasReservedID(part) return !found }) } else if parts.Type != gjson.Null { // Result.Array returns a non-array JSON value as one item. found = hasReservedID(parts) } return !found }) return found } // antigravitySyntheticToolCallID derives a deterministic neutral call ID for a // reserved Claude-facing provenance ID that could not be resolved back to its // provider-native call. It is stable across turns and never lands in the reserved // namespace, so call/response pairs stay consistent without impersonating a // provider-issued ID. func antigravitySyntheticToolCallID(reservedID string) string { sum := sha256.Sum256([]byte("antigravity-degraded-tool-call\x00" + reservedID)) return fmt.Sprintf("call_%x", sum[:6]) } // degradeAntigravityClaudeToolProvenanceIDs rewrites unresolved reserved tool // provenance IDs to neutral synthetic IDs so a conversation survives a replay // ledger miss instead of failing closed forever. // // The same reserved ID always maps to the same synthetic ID, so functionCall and // functionResponse stay paired. Whatever signature the client carried in-band is // kept: Gemini validates a thought signature's own integrity, not its binding to // the call ID or the surrounding history, so rewriting the ID does not invalidate // it. Calls left with no signature at all get the leading bypass sentinel from // antigravityRepairUnsignedFirstFunctionCalls. Every other part is left alone, // preserving the native "1 signed + N unsigned" parallel-call shape. func degradeAntigravityClaudeToolProvenanceIDs(payload []byte) ([]byte, int) { contents := util.GetGJSONBytesNoCopy(payload, "request.contents") if !contents.IsArray() { return payload, 0 } out := payload degraded := 0 for ci, content := range contents.Array() { parts := content.Get("parts") if !parts.IsArray() { continue } for pi, part := range parts.Array() { partPath := fmt.Sprintf("request.contents.%d.parts.%d", ci, pi) if fc := part.Get("functionCall"); fc.Exists() { id := strings.TrimSpace(fc.Get("id").String()) if !util.IsGeminiClaudeToolUseID(id) { continue } out, _ = sjson.SetBytes(out, partPath+".functionCall.id", antigravitySyntheticToolCallID(id)) degraded++ continue } if fr := part.Get("functionResponse"); fr.Exists() { id := strings.TrimSpace(fr.Get("id").String()) if !util.IsGeminiClaudeToolUseID(id) { continue } out, _ = sjson.SetBytes(out, partPath+".functionResponse.id", antigravitySyntheticToolCallID(id)) degraded++ } } } return out, degraded } // antigravityRepairUnsignedFirstFunctionCalls restores Gemini's bypass sentinel on // the first function call of any model turn that replay left completely unsigned. // // Gemini rejects a model turn whose leading functionCall carries no // thoughtSignature. The request-level sanitizer enforces that invariant, but it // runs before reasoning replay, and replay can legitimately drop a signature // afterwards: a degraded call loses one, and an identity-only restore on drifted // context deliberately declines to replay one. Only a missing signature is filled // in here, so native signatures are never touched. func antigravityRepairUnsignedFirstFunctionCalls(payload []byte) []byte { contents := util.GetGJSONBytesNoCopy(payload, "request.contents") if !contents.IsArray() { return payload } out := payload contents.ForEach(func(contentIndex, content gjson.Result) bool { if !strings.EqualFold(strings.TrimSpace(content.Get("role").String()), "model") { return true } parts := content.Get("parts") if !parts.IsArray() { return true } parts.ForEach(func(partIndex, part gjson.Result) bool { if !part.Get("functionCall").Exists() { return true } if antigravityNativePartThoughtSignature(part) == "" { path := fmt.Sprintf( "request.contents.%d.parts.%d.thoughtSignature", contentIndex.Int(), partIndex.Int(), ) out, _ = sjson.SetBytes(out, path, internalsignature.GeminiSkipThoughtSignatureValidator) } // Only the first function call of a turn needs a signature; siblings stay // unsigned to preserve the native parallel-call shape. return false }) return true }) return out } func antigravityCanonicalReplayJSON(raw []byte) []byte { var value any if json.Unmarshal(raw, &value) != nil { return bytes.TrimSpace(raw) } canonical, errMarshal := json.Marshal(value) if errMarshal != nil { return bytes.TrimSpace(raw) } return canonical } func antigravitySetReplayItemContextHashValue(item []byte, contextHash string) []byte { if contextHash != "" { item, _ = sjson.SetBytes(item, "contextHash", contextHash) } return item } func antigravityExistingReplayPartPath(payload []byte, contentIndex int, partIndex int) (string, bool) { if contentIndex < 0 || partIndex < 0 { return "", false } partsPath := fmt.Sprintf("request.contents.%d.parts", contentIndex) parts := gjson.GetBytes(payload, partsPath) if !parts.IsArray() { return "", false } arr := parts.Array() if partIndex >= len(arr) || arr[partIndex].Type == gjson.Null { return "", false } return fmt.Sprintf("%s.%d", partsPath, partIndex), true } func antigravityReplayPartWritePath(payload []byte, contentIndex int, partIndex int) string { if path, ok := antigravityExistingReplayPartPath(payload, contentIndex, partIndex); ok { return path } partsPath := fmt.Sprintf("request.contents.%d.parts", contentIndex) if gjson.GetBytes(payload, partsPath).IsArray() { return partsPath + ".-1" } return partsPath + ".0" } // insertAntigravityReasoningReplayItemsWithSchemas applies items sequentially. // index must describe payload on entry and is rebuilt after any mutation so each // item observes exactly the payload the previous item produced. func insertAntigravityReasoningReplayItemsWithSchemas(index *antigravityReplayRequestIndex, payload []byte, items [][]byte, toolSchemas map[string]any) ([]byte, bool) { out := payload changed := false // The index only exists to serve later items in this loop, so it is refreshed // after a mutation exclusively when a successor still has to read it. Callers // receive no index back and must rebuild their own if they keep using one. for itemIndex, item := range items { hasSuccessor := itemIndex+1 < len(items) itemResult := gjson.ParseBytes(item) switch strings.TrimSpace(itemResult.Get("type").String()) { case "thought_signature": sig := strings.TrimSpace(itemResult.Get("thoughtSignature").String()) if sig == "" { continue } partPath, exists := index.thoughtSignatureReplayPartPath(itemResult) if !exists { continue } path := partPath + ".thoughtSignature" if antigravityHasNativeThoughtSignature(gjson.GetBytes(out, path).String()) { continue } ci := int(itemResult.Get("contentIndex").Int()) out = antigravityRemoveThoughtSignatureFromOtherParts(out, ci, sig, partPath) updated, err := sjson.SetBytes(out, path, sig) if err != nil { // antigravityRemoveThoughtSignatureFromOtherParts may already have // rewritten out, so the index has to be refreshed regardless. if hasSuccessor { index = newAntigravityReplayRequestIndex(out) } continue } out = updated changed = true if hasSuccessor { index = newAntigravityReplayRequestIndex(out) } case "function_call_part": updated, ok := mergeAntigravityFunctionCallPartReplayWithSchemas(index, out, itemResult, toolSchemas) if ok { out = updated changed = true if hasSuccessor { index = newAntigravityReplayRequestIndex(out) } } } } return out, changed } func antigravityNativeFunctionCallJSON(itemResult gjson.Result, fallbackID string) ([]byte, bool) { name := strings.TrimSpace(itemResult.Get("name").String()) args := itemResult.Get("args") if name == "" || !args.Exists() { return nil, false } functionCall := []byte(`{"name":""}`) functionCall, _ = sjson.SetBytes(functionCall, "name", name) callID := strings.TrimSpace(itemResult.Get("call_id").String()) if callID == "" { callID = fallbackID } if callID != "" { functionCall, _ = sjson.SetBytes(functionCall, "id", callID) } if args.Type == gjson.String { if parsed := gjson.Parse(args.String()); parsed.Exists() { functionCall, _ = sjson.SetRawBytes(functionCall, "args", []byte(parsed.Raw)) } else { functionCall, _ = sjson.SetBytes(functionCall, "args", args.String()) } } else { functionCall, _ = sjson.SetRawBytes(functionCall, "args", []byte(args.Raw)) } return functionCall, true } func antigravityFunctionResponsesCanRestoreID(payload []byte, currentID, nativeName string) bool { if currentID == "" { return true } contents := util.GetGJSONBytesNoCopy(payload, "request.contents") if !contents.IsArray() { return false } valid := true contents.ForEach(func(_, content gjson.Result) bool { content.Get("parts").ForEach(func(_, part gjson.Result) bool { response := part.Get("functionResponse") if !response.Exists() || strings.TrimSpace(response.Get("id").String()) != currentID { return true } name := strings.TrimSpace(response.Get("name").String()) valid = name == "" || name == "unknown" || name == nativeName return valid }) return valid }) return valid } // restoreAntigravityNativeFunctionCallReplay rewrites one function call part back // to its provider-native identity. allowSignature reports whether the cached // thoughtSignature may be replayed as well; identity-only restores pass false // because the surrounding context no longer matches the one the signature was // issued for. func restoreAntigravityNativeFunctionCallReplay(payload []byte, contentIndex, partIndex int, itemResult gjson.Result, allowLegacyIDRestore, allowSignature bool) ([]byte, bool) { partPath := fmt.Sprintf("request.contents.%d.parts.%d", contentIndex, partIndex) currentCall := gjson.GetBytes(payload, partPath+".functionCall") if !currentCall.Exists() { return payload, false } currentID := strings.TrimSpace(currentCall.Get("id").String()) nativeID := strings.TrimSpace(itemResult.Get("call_id").String()) nativeName := strings.TrimSpace(itemResult.Get("name").String()) restoreIdentity := currentID == nativeID || util.IsGeminiClaudeToolUseID(currentID) || allowLegacyIDRestore if !restoreIdentity { signature := strings.TrimSpace(itemResult.Get("thoughtSignature").String()) if !allowSignature || signature == "" || antigravityHasNativeThoughtSignature(gjson.GetBytes(payload, partPath+".thoughtSignature").String()) { return payload, false } payload = antigravityRemoveThoughtSignatureFromOtherParts(payload, contentIndex, signature, partPath) updated, errSet := sjson.SetBytes(payload, partPath+".thoughtSignature", signature) return updated, errSet == nil } if currentID != nativeID && !antigravityFunctionResponsesCanRestoreID(payload, currentID, nativeName) { return payload, false } nativeCall, okCall := antigravityNativeFunctionCallJSON(itemResult, currentID) if !okCall { return payload, false } out, errSet := sjson.SetRawBytes(payload, partPath+".functionCall", nativeCall) if errSet != nil { return payload, false } for _, field := range []string{"thoughtSignature", "thought_signature", "extra_content.google.thought_signature"} { out, _ = sjson.DeleteBytes(out, partPath+"."+field) } if signature := strings.TrimSpace(itemResult.Get("thoughtSignature").String()); allowSignature && signature != "" { out = antigravityRemoveThoughtSignatureFromOtherParts(out, contentIndex, signature, partPath) out, _ = sjson.SetBytes(out, partPath+".thoughtSignature", signature) } if currentID != "" && nativeID != "" && currentID != nativeID { contents := util.GetGJSONBytesNoCopy(out, "request.contents") contents.ForEach(func(contentKey, content gjson.Result) bool { content.Get("parts").ForEach(func(partKey, part gjson.Result) bool { response := part.Get("functionResponse") if !response.Exists() || strings.TrimSpace(response.Get("id").String()) != currentID { return true } responsePath := fmt.Sprintf("request.contents.%d.parts.%d.functionResponse", contentKey.Int(), partKey.Int()) out, _ = sjson.SetBytes(out, responsePath+".id", nativeID) out, _ = sjson.SetBytes(out, responsePath+".name", nativeName) return true }) return true }) } return out, !bytes.Equal(out, payload) } // mergeAntigravityFunctionCallPartReplayWithSchemas locates the target call via // index, which must describe exactly the payload passed alongside it. Every // lookup happens before the first mutation, so one index is valid for the whole // call. func mergeAntigravityFunctionCallPartReplayWithSchemas(index *antigravityReplayRequestIndex, payload []byte, itemResult gjson.Result, toolSchemas map[string]any) ([]byte, bool) { name := strings.TrimSpace(itemResult.Get("name").String()) args := itemResult.Get("args") callID := strings.TrimSpace(itemResult.Get("call_id").String()) sig := strings.TrimSpace(itemResult.Get("thoughtSignature").String()) if name == "" || !args.Exists() { return payload, false } if location, exists := index.functionCallPartLocationForReplayWithSchemas(itemResult, toolSchemas); exists { _, allowLegacyIDRestore := toolSchemas[name] return restoreAntigravityNativeFunctionCallReplay(payload, location.contentIndex, location.partIndex, itemResult, allowLegacyIDRestore, true) } // The context drifted, but an exact opaque ID match still proves this call's // identity. Gemini validates a thought signature's own integrity and nothing // about the history around it, so the drift costs the signature nothing: restore // the native call and its signature rather than making the model re-reason. if location, exists := index.functionCallProvenanceLocation(itemResult, toolSchemas); exists { return restoreAntigravityNativeFunctionCallReplay(payload, location.contentIndex, location.partIndex, itemResult, false, true) } if callID != "" { stableID := util.GeminiClaudeToolUseID(callID, name, args.Raw) _, hasNativeID := index.functionCallPartLocation(callID) hasStableID := false if stableID != "" { _, hasStableID = index.functionCallPartLocation(stableID) } if hasNativeID || hasStableID { // The call is already in the history under its native or Claude-facing // ID, and neither lookup above accepted it, so the client changed it. // Never replay an opaque signature onto that changed call, and never // insert a second copy of it further down. return payload, false } if frIndex, currentResponseID, ok := index.functionResponseContentIndexForReplay(itemResult); ok { parallelModelIndex := frIndex - 1 if parallelModelIndex >= 0 && strings.EqualFold(strings.TrimSpace(index.contents[parallelModelIndex].content.Get("role").String()), "model") && index.contextMatches(itemResult, parallelModelIndex) { if updated, appended := appendAntigravityFunctionCallToModelContent(payload, parallelModelIndex, name, callID, sig, args); appended { return restoreAntigravityFunctionResponseReplayIdentity(updated, currentResponseID, callID, name), true } } if index.contextMatches(itemResult, frIndex) { if updated, inserted := insertAntigravityModelFunctionCallBeforeContent(payload, frIndex, name, callID, sig, args); inserted { return restoreAntigravityFunctionResponseReplayIdentity(updated, currentResponseID, callID, name), true } } } } else { // Without a native call ID, only an exact semantic match is safe. Never // put an opaque signature on a different call at the old numeric slot. return payload, false } ci := antigravityReasoningReplayResolveContentIndex(payload, int(itemResult.Get("contentIndex").Int())) if ci < 0 || !index.contextMatches(itemResult, ci) { return payload, false } pi := int(itemResult.Get("partIndex").Int()) out := payload changed := false partPath, exists := antigravityExistingReplayPartPath(out, ci, pi) if !exists { fc := map[string]any{"name": name} if callID != "" { fc["id"] = callID } if args.Type == gjson.String { fc["args"] = args.String() } else { var parsed any if json.Unmarshal([]byte(args.Raw), &parsed) == nil { fc["args"] = parsed } } part := map[string]any{"functionCall": fc} if sig != "" { part["thoughtSignature"] = sig } if updated, err := sjson.SetBytes(out, antigravityReplayPartWritePath(out, ci, pi), part); err == nil { return updated, true } return payload, false } pathSig := partPath + ".thoughtSignature" if sig != "" && !antigravityHasNativeThoughtSignature(gjson.GetBytes(out, pathSig).String()) { out = antigravityRemoveThoughtSignatureFromOtherParts(out, ci, sig, partPath) if updated, err := sjson.SetBytes(out, pathSig, sig); err == nil { out = updated changed = true } } pathFC := partPath + ".functionCall" if !gjson.GetBytes(out, pathFC).Exists() { fc := map[string]any{"name": name} if callID != "" { fc["id"] = callID } if args.Type == gjson.String { fc["args"] = args.String() } else { var parsed any if json.Unmarshal([]byte(args.Raw), &parsed) == nil { fc["args"] = parsed } } if updated, err := sjson.SetBytes(out, pathFC, fc); err == nil { out = updated changed = true } } return out, changed } type antigravityPendingThoughtSignature struct { signature string targetKind string } type antigravityReasoningReplayAccumulator struct { scope antigravityReasoningReplayScope responseContextHash string items [][]byte seenFC map[string]bool seenSignatures map[string]bool segmentOccurrences map[string]int functionCallOccurrences map[string]int contentIndex int nextPartIndex int visibleText strings.Builder thoughtText strings.Builder visiblePartIndex int thoughtPartIndex int lastResponseKind string pendingSignatures []antigravityPendingThoughtSignature itemBytes int overflow bool terminal bool } func newAntigravityReasoningReplayAccumulator(scope antigravityReasoningReplayScope, requestPayload []byte) *antigravityReasoningReplayAccumulator { if !scope.valid() { return nil } index := newAntigravityReplayRequestIndex(requestPayload) contentIndex, basePartIndex := index.pendingModelContentIndex() items := index.reasoningReplayItemsFromRequest() seenSignatures := make(map[string]bool, len(items)) for _, item := range items { itemResult := gjson.ParseBytes(item) if signature := strings.TrimSpace(itemResult.Get("thoughtSignature").String()); signature != "" { seenSignatures[signature] = true } } itemBytes := 0 for _, item := range items { itemBytes += len(item) } segmentOccurrences := make(map[string]int) functionCallOccurrences := make(map[string]int) if contentIndex >= 0 && contentIndex < len(index.contents) { for _, part := range index.contents[contentIndex].parts { if fc := part.Get("functionCall"); fc.Exists() { key := antigravityFunctionCallKey(fc.Get("name").String(), fc.Get("args").Raw, "") if key != "" { functionCallOccurrences[key]++ } continue } if kind, fingerprint := antigravityReplayPartFingerprint(part); fingerprint != "" { segmentOccurrences[kind+"\x00"+fingerprint]++ } } } return &antigravityReasoningReplayAccumulator{ scope: scope, responseContextHash: index.contextFingerprint(contentIndex), items: items, seenFC: make(map[string]bool), seenSignatures: seenSignatures, segmentOccurrences: segmentOccurrences, functionCallOccurrences: functionCallOccurrences, contentIndex: contentIndex, nextPartIndex: basePartIndex, visiblePartIndex: -1, thoughtPartIndex: -1, itemBytes: itemBytes, overflow: len(items) > internalcache.AntigravityReasoningReplayCacheMaxItemsPerEntry || itemBytes > internalcache.AntigravityReasoningReplayCacheMaxBytesPerEntry, } } func antigravityReasoningReplayItemsFromRequest(payload []byte) [][]byte { return newAntigravityReplayRequestIndex(payload).reasoningReplayItemsFromRequest() } func (i *antigravityReplayRequestIndex) reasoningReplayItemsFromRequest() [][]byte { // Invalid contents yield a nil slice while a valid but empty array yields an // empty non-nil slice, matching the pre-index behavior exactly. if i == nil || !i.validContents { return nil } items := make([][]byte, 0) for contentIndex, content := range i.contents { if !strings.EqualFold(strings.TrimSpace(content.content.Get("role").String()), "model") || len(content.parts) == 0 { continue } functionCallOccurrences := make(map[string]int) for partIndex, part := range content.parts { signature := antigravityNativePartThoughtSignature(part) if !antigravityHasNativeThoughtSignature(signature) { signature = "" } if functionCall := part.Get("functionCall"); functionCall.Exists() { key := antigravityFunctionCallKey(functionCall.Get("name").String(), functionCall.Get("args").Raw, "") occurrence := functionCallOccurrences[key] if key != "" { functionCallOccurrences[key] = occurrence + 1 } if item := buildAntigravityFunctionCallPartItem(contentIndex, partIndex, occurrence, functionCall, signature); len(item) > 0 { items = append(items, antigravitySetReplayItemContextHashValue(item, i.contextFingerprint(contentIndex))) } continue } if signature == "" { continue } targetPart := part targetPartIndex := partIndex kind, fingerprint := antigravityReplayPartFingerprint(targetPart) if fingerprint == "" && partIndex > 0 { targetPartIndex = partIndex - 1 targetPart = content.parts[targetPartIndex] kind, fingerprint = antigravityReplayPartFingerprint(targetPart) } if fingerprint == "" { continue } item := buildAntigravityThoughtSignatureItem(contentIndex, targetPartIndex, signature, kind, fingerprint) item, _ = sjson.SetBytes(item, "targetOccurrence", antigravityReplayPartOccurrence(content.parts, targetPartIndex, kind, fingerprint)) items = append(items, antigravitySetReplayItemContextHashValue(item, i.contextFingerprint(contentIndex))) } } return items } func (a *antigravityReasoningReplayAccumulator) appendItem(item []byte) { if a == nil || len(item) == 0 || a.overflow { return } if len(a.items)+1 > internalcache.AntigravityReasoningReplayCacheMaxItemsPerEntry || a.itemBytes+len(item) > internalcache.AntigravityReasoningReplayCacheMaxBytesPerEntry { a.overflow = true return } a.items = append(a.items, item) a.itemBytes += len(item) } func (a *antigravityReasoningReplayAccumulator) attachDetachedSignatureToLastFunctionCall(signature string) { if a == nil || signature == "" { return } for itemIndex := len(a.items) - 1; itemIndex >= 0; itemIndex-- { item := gjson.ParseBytes(a.items[itemIndex]) if item.Get("type").String() != "function_call_part" { continue } if strings.TrimSpace(item.Get("thoughtSignature").String()) != "" { return } updated, errSet := sjson.SetBytes(a.items[itemIndex], "thoughtSignature", signature) if errSet != nil { return } delta := len(updated) - len(a.items[itemIndex]) if a.itemBytes+delta > internalcache.AntigravityReasoningReplayCacheMaxBytesPerEntry { a.overflow = true return } a.items[itemIndex] = updated a.itemBytes += delta return } } func (a *antigravityReasoningReplayAccumulator) ObserveSSELine(line []byte) { if a == nil { return } payload := helps.JSONPayload(line) if payload == nil { return } a.observeResponsePayload(payload) } func (a *antigravityReasoningReplayAccumulator) observeResponsePayload(payload []byte) { if finishReason := strings.TrimSpace(gjson.GetBytes(payload, "response.candidates.0.finishReason").String()); finishReason != "" { a.terminal = true } parts := gjson.GetBytes(payload, "response.candidates.0.content.parts") if !parts.IsArray() { return } parts.ForEach(func(_, part gjson.Result) bool { pi := a.nextPartIndex a.nextPartIndex++ signature := antigravityNativePartThoughtSignature(part) if !antigravityHasNativeThoughtSignature(signature) { signature = "" } if fc := part.Get("functionCall"); fc.Exists() { if a.lastResponseKind == "text" || a.lastResponseKind == "thought" { a.flushPendingThoughtSignaturesForKind(a.lastResponseKind) } if signature != "" { remainingPending := a.pendingSignatures[:0] for _, pending := range a.pendingSignatures { if pending.targetKind != "" { remainingPending = append(remainingPending, pending) } } a.pendingSignatures = remainingPending } if signature == "" { for pendingIndex := len(a.pendingSignatures) - 1; pendingIndex >= 0; pendingIndex-- { if a.pendingSignatures[pendingIndex].targetKind == "" { signature = a.pendingSignatures[pendingIndex].signature a.pendingSignatures = append(a.pendingSignatures[:pendingIndex], a.pendingSignatures[pendingIndex+1:]...) break } } } keys := antigravityReplayToolCallKeysFromPart(fc) for _, key := range keys { dedupeKey := key + "\x00" + signature if signature == "" { dedupeKey = fmt.Sprintf("%s\x00part:%d", key, pi) } if a.seenFC[dedupeKey] { return true } a.seenFC[dedupeKey] = true } occurrenceKey := antigravityFunctionCallKey(fc.Get("name").String(), fc.Get("args").Raw, "") occurrence := a.functionCallOccurrences[occurrenceKey] if occurrenceKey != "" { a.functionCallOccurrences[occurrenceKey] = occurrence + 1 } item := buildAntigravityFunctionCallPartItem(a.contentIndex, pi, occurrence, fc, signature) if len(item) > 0 { a.appendItem(antigravitySetReplayItemContextHashValue(item, a.responseContextHash)) if signature != "" { a.seenSignatures[signature] = true } } a.lastResponseKind = "function_call" return true } targetKind := "" if part.Get("thought").Bool() { targetKind = "thought" } text := part.Get("text") hasSemanticText := text.Exists() && text.String() != "" signatureOnly := signature != "" && !hasSemanticText if signatureOnly && a.lastResponseKind == "function_call" { if !a.seenSignatures[signature] { a.attachDetachedSignatureToLastFunctionCall(signature) a.seenSignatures[signature] = true } return true } if hasSemanticText { if targetKind != "thought" { targetKind = "text" } if signature != "" { remainingPending := a.pendingSignatures[:0] for _, pending := range a.pendingSignatures { unboundPrefix := pending.targetKind == "" if pending.targetKind == targetKind { unboundPrefix = (targetKind == "text" && a.visibleText.Len() == 0) || (targetKind == "thought" && a.thoughtText.Len() == 0) } if unboundPrefix { if pending.signature == signature { delete(a.seenSignatures, signature) } continue } remainingPending = append(remainingPending, pending) } a.pendingSignatures = remainingPending for _, pending := range a.pendingSignatures { if pending.targetKind == targetKind && pending.signature != signature { a.flushPendingThoughtSignaturesForKind(targetKind) break } } } if a.lastResponseKind != "" && a.lastResponseKind != targetKind && (a.lastResponseKind == "text" || a.lastResponseKind == "thought") { a.flushPendingThoughtSignaturesForKind(a.lastResponseKind) } if targetKind == "thought" { if a.thoughtText.Len() == 0 { a.thoughtPartIndex = pi } a.thoughtText.WriteString(text.String()) } else { if a.visibleText.Len() == 0 { a.visiblePartIndex = pi } a.visibleText.WriteString(text.String()) } a.lastResponseKind = targetKind } acceptedSignature := false if signature != "" && !a.seenSignatures[signature] { if targetKind == "" { targetKind = a.lastResponseKind } unmatchedDetachedCarrier := signatureOnly && a.lastResponseKind == targetKind && ((targetKind == "text" && a.visibleText.Len() == 0) || (targetKind == "thought" && a.thoughtText.Len() == 0)) if unmatchedDetachedCarrier { a.seenSignatures[signature] = true } else if len(a.pendingSignatures)+len(a.items)+1 > internalcache.AntigravityReasoningReplayCacheMaxItemsPerEntry || a.itemBytes+len(signature) > internalcache.AntigravityReasoningReplayCacheMaxBytesPerEntry { a.overflow = true a.seenSignatures[signature] = true } else { a.pendingSignatures = append(a.pendingSignatures, antigravityPendingThoughtSignature{signature: signature, targetKind: targetKind}) a.seenSignatures[signature] = true acceptedSignature = true } } if acceptedSignature && (signatureOnly || hasSemanticText) { switch targetKind { case "text": if a.visibleText.Len() > 0 { a.flushPendingThoughtSignaturesForKind("text") } case "thought": if a.thoughtText.Len() > 0 { a.flushPendingThoughtSignaturesForKind("thought") } } } return true }) } func buildAntigravityThoughtSignatureItem(contentIndex, partIndex int, signature, targetKind, targetHash string) []byte { item := []byte(fmt.Sprintf(`{"type":"thought_signature","thoughtSignature":%q,"contentIndex":%d,"partIndex":%d}`, signature, contentIndex, partIndex)) if targetKind != "" { item, _ = sjson.SetBytes(item, "targetKind", targetKind) } if targetHash != "" { item, _ = sjson.SetBytes(item, "targetHash", targetHash) } return item } func buildAntigravityFunctionCallPartItem(contentIndex, partIndex, targetOccurrence int, fc gjson.Result, signature string) []byte { item := map[string]any{ "type": "function_call_part", "contentIndex": contentIndex, "partIndex": partIndex, "targetOccurrence": targetOccurrence, "name": fc.Get("name").String(), } if id := strings.TrimSpace(fc.Get("id").String()); id != "" { item["call_id"] = id } if args := fc.Get("args"); args.Exists() { if args.Type == gjson.String { item["args"] = args.String() } else { item["args"] = json.RawMessage(args.Raw) } } if signature != "" { item["thoughtSignature"] = signature } raw, err := json.Marshal(item) if err != nil { return nil } return raw } func (a *antigravityReasoningReplayAccumulator) flushPendingThoughtSignaturesForKind(targetKind string) { if a == nil || (targetKind != "text" && targetKind != "thought") { return } text := a.visibleText.String() partIndex := a.visiblePartIndex if targetKind == "thought" { text = a.thoughtText.String() partIndex = a.thoughtPartIndex } targetHash := "" targetOccurrence := 0 if text != "" { sum := sha256.Sum256([]byte(targetKind + "\x00" + text)) targetHash = fmt.Sprintf("%x", sum[:]) occurrenceKey := targetKind + "\x00" + targetHash targetOccurrence = a.segmentOccurrences[occurrenceKey] a.segmentOccurrences[occurrenceKey] = targetOccurrence + 1 } remaining := a.pendingSignatures[:0] for _, pending := range a.pendingSignatures { if pending.targetKind != targetKind || targetHash == "" { remaining = append(remaining, pending) continue } item := buildAntigravityThoughtSignatureItem(a.contentIndex, partIndex, pending.signature, targetKind, targetHash) item, _ = sjson.SetBytes(item, "targetOccurrence", targetOccurrence) a.appendItem(antigravitySetReplayItemContextHashValue(item, a.responseContextHash)) } a.pendingSignatures = remaining if targetKind == "thought" { a.thoughtText.Reset() a.thoughtPartIndex = -1 } else { a.visibleText.Reset() a.visiblePartIndex = -1 } } func (a *antigravityReasoningReplayAccumulator) appendPendingThoughtSignatures() { if a == nil { return } for index := range a.pendingSignatures { if a.pendingSignatures[index].targetKind != "" { continue } switch { case a.lastResponseKind == "text" && a.visibleText.Len() > 0: a.pendingSignatures[index].targetKind = "text" case a.lastResponseKind == "thought" && a.thoughtText.Len() > 0: a.pendingSignatures[index].targetKind = "thought" case a.visibleText.Len() > 0: a.pendingSignatures[index].targetKind = "text" case a.thoughtText.Len() > 0: a.pendingSignatures[index].targetKind = "thought" } } a.flushPendingThoughtSignaturesForKind("thought") a.flushPendingThoughtSignaturesForKind("text") a.pendingSignatures = nil } func (a *antigravityReasoningReplayAccumulator) Commit(ctx context.Context) { if a == nil || !a.scope.valid() { return } log.Debugf("antigravity replay: accumulator commit terminal=%t overflow=%t items=%d (session=%s)", a.terminal, a.overflow, len(a.items), antigravityReplayLogKey(a.scope.sessionKey)) if !a.terminal { // No terminal finishReason means the stream never completed, so this turn // contributes nothing to the ledger and its tool IDs become unresolvable. return } if a.overflow { _, _ = internalcache.DeleteAntigravityReasoningReplayItemsIfUnchanged(ctx, a.scope.modelName, a.scope.sessionKey, a.scope.cacheSnapshot) return } a.appendPendingThoughtSignatures() if a.overflow { _, _ = internalcache.DeleteAntigravityReasoningReplayItemsIfUnchanged(ctx, a.scope.modelName, a.scope.sessionKey, a.scope.cacheSnapshot) return } if len(a.items) == 0 { _, _ = internalcache.DeleteAntigravityReasoningReplayItemsIfUnchanged(ctx, a.scope.modelName, a.scope.sessionKey, a.scope.cacheSnapshot) return } if _, errReplace := internalcache.ReplaceAntigravityReasoningReplayItemsIfUnchanged(ctx, a.scope.modelName, a.scope.sessionKey, a.scope.cacheSnapshot, a.items); errReplace != nil { _, _ = internalcache.DeleteAntigravityReasoningReplayItemsIfUnchanged(ctx, a.scope.modelName, a.scope.sessionKey, a.scope.cacheSnapshot) } } func cacheAntigravityReasoningReplayFromResponse(ctx context.Context, scope antigravityReasoningReplayScope, requestPayload, body []byte) { if !scope.valid() || len(body) == 0 { return } acc := newAntigravityReasoningReplayAccumulator(scope, requestPayload) acc.observeResponsePayload(body) acc.Commit(ctx) } func applyAntigravityNativeSignatureReplayIfNeeded(modelName string, payload []byte) []byte { if antigravityUsesReasoningReplayCache(modelName) { return payload } // Native per-part signature replay is not on upstream/dev; Gemini uses HOME replay only. return payload } func antigravityUsesReasoningReplayCache(modelName string) bool { modelName = strings.ToLower(modelName) if strings.Contains(modelName, "claude") { return false } return strings.Contains(modelName, "gemini") || strings.Contains(modelName, "flash") || strings.Contains(modelName, "agent") } func antigravityNativePartThoughtSignature(part gjson.Result) string { for _, path := range []string{"thoughtSignature", "thought_signature", "extra_content.google.thought_signature"} { if signature := strings.TrimSpace(part.Get(path).String()); signature != "" { return signature } } return "" }