@jsonstudio/llms 0.6.1462 → 0.6.1643

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/conversion/codecs/gemini-openai-codec.js +6 -1
  2. package/dist/conversion/compat/actions/anthropic-claude-code-system-prompt.d.ts +4 -7
  3. package/dist/conversion/compat/actions/anthropic-claude-code-system-prompt.js +140 -21
  4. package/dist/conversion/compat/actions/antigravity-thought-signature-cache.js +68 -10
  5. package/dist/conversion/compat/actions/antigravity-thought-signature-prepare.js +151 -23
  6. package/dist/conversion/compat/actions/gemini-cli-request.js +72 -13
  7. package/dist/conversion/compat/antigravity-session-signature.d.ts +56 -2
  8. package/dist/conversion/compat/antigravity-session-signature.js +819 -26
  9. package/dist/conversion/compat/profiles/anthropic-claude-code.json +0 -9
  10. package/dist/conversion/compat/profiles/chat-gemini-cli.json +1 -0
  11. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +33 -8
  12. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +1 -1
  13. package/dist/conversion/hub/pipeline/compat/compat-profile-store.js +12 -3
  14. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +1 -0
  15. package/dist/conversion/hub/pipeline/hub-pipeline.js +24 -0
  16. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +20 -0
  17. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +26 -1
  18. package/dist/conversion/hub/process/chat-process.js +300 -67
  19. package/dist/conversion/hub/response/provider-response.js +4 -3
  20. package/dist/conversion/shared/gemini-tool-utils.js +134 -9
  21. package/dist/conversion/shared/text-markup-normalizer.js +90 -1
  22. package/dist/conversion/shared/thought-signature-validator.d.ts +1 -1
  23. package/dist/conversion/shared/thought-signature-validator.js +2 -1
  24. package/dist/quota/apikey-reset.d.ts +17 -0
  25. package/dist/quota/apikey-reset.js +43 -0
  26. package/dist/quota/index.d.ts +2 -0
  27. package/dist/quota/index.js +1 -0
  28. package/dist/quota/quota-manager.d.ts +44 -0
  29. package/dist/quota/quota-manager.js +491 -0
  30. package/dist/quota/quota-state.d.ts +6 -0
  31. package/dist/quota/quota-state.js +167 -0
  32. package/dist/quota/types.d.ts +61 -0
  33. package/dist/quota/types.js +1 -0
  34. package/dist/router/virtual-router/bootstrap.js +103 -6
  35. package/dist/router/virtual-router/engine-health.js +104 -0
  36. package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +18 -0
  37. package/dist/router/virtual-router/engine-selection/tier-priority.d.ts +1 -2
  38. package/dist/router/virtual-router/engine-selection/tier-priority.js +2 -2
  39. package/dist/router/virtual-router/engine-selection/tier-selection-select.js +34 -10
  40. package/dist/router/virtual-router/engine-selection/tier-selection.js +250 -6
  41. package/dist/router/virtual-router/engine-selection.js +2 -2
  42. package/dist/router/virtual-router/engine.d.ts +16 -1
  43. package/dist/router/virtual-router/engine.js +320 -42
  44. package/dist/router/virtual-router/features.js +20 -2
  45. package/dist/router/virtual-router/success-center.d.ts +10 -0
  46. package/dist/router/virtual-router/success-center.js +32 -0
  47. package/dist/router/virtual-router/types.d.ts +48 -0
  48. package/dist/servertool/clock/config.d.ts +2 -0
  49. package/dist/servertool/clock/config.js +10 -2
  50. package/dist/servertool/clock/daemon.js +3 -0
  51. package/dist/servertool/clock/ntp.d.ts +18 -0
  52. package/dist/servertool/clock/ntp.js +318 -0
  53. package/dist/servertool/clock/paths.d.ts +1 -0
  54. package/dist/servertool/clock/paths.js +3 -0
  55. package/dist/servertool/clock/state.d.ts +2 -0
  56. package/dist/servertool/clock/state.js +15 -2
  57. package/dist/servertool/clock/tasks.d.ts +1 -0
  58. package/dist/servertool/clock/tasks.js +24 -1
  59. package/dist/servertool/clock/types.d.ts +21 -0
  60. package/dist/servertool/engine.js +105 -1
  61. package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.d.ts +1 -0
  62. package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.js +201 -0
  63. package/dist/servertool/handlers/clock-auto.js +39 -4
  64. package/dist/servertool/handlers/clock.js +145 -16
  65. package/dist/servertool/handlers/followup-request-builder.js +84 -0
  66. package/dist/servertool/handlers/stop-message-auto.js +1 -1
  67. package/dist/servertool/server-side-tools.d.ts +1 -0
  68. package/dist/servertool/server-side-tools.js +1 -0
  69. package/dist/servertool/types.d.ts +2 -0
  70. package/dist/tools/apply-patch/execution-capturer.js +24 -3
  71. package/package.json +3 -2
@@ -176,9 +176,10 @@ export function buildOpenAIChatFromGeminiRequest(payload) {
176
176
  }
177
177
  export function buildOpenAIChatFromGeminiResponse(payload) {
178
178
  const candidates = Array.isArray(payload?.candidates) ? payload.candidates : [];
179
+ const errorNode = payload?.error;
179
180
  const primary = candidates[0] && typeof candidates[0] === 'object' ? candidates[0] : {};
180
181
  const content = primary?.content || {};
181
- const role = mapGeminiRoleToChat(content.role);
182
+ const role = candidates.length > 0 ? mapGeminiRoleToChat(content.role) : 'assistant';
182
183
  const rawFinishReason = primary?.finishReason;
183
184
  const finishReasonUpper = typeof rawFinishReason === 'string' ? rawFinishReason.trim().toUpperCase() : '';
184
185
  const parts = Array.isArray(content.parts) ? content.parts : [];
@@ -420,6 +421,10 @@ export function buildOpenAIChatFromGeminiResponse(payload) {
420
421
  }
421
422
  ]
422
423
  };
424
+ // Preserve upstream error envelope (used by servertool auto flows and host status mapping).
425
+ if (errorNode && typeof errorNode === 'object' && !Array.isArray(errorNode)) {
426
+ chatResp.error = errorNode;
427
+ }
423
428
  if (Object.keys(usage).length > 0) {
424
429
  chatResp.usage = usage;
425
430
  }
@@ -1,11 +1,8 @@
1
1
  import type { JsonObject } from '../../hub/types/json.js';
2
2
  import type { AnthropicClaudeCodeSystemPromptConfig } from '../../hub/pipeline/compat/compat-types.js';
3
3
  /**
4
- * tabglm (Anthropic-compatible) strict-gates requests to Claude Code official client.
5
- * It checks the system prompt format, and rejects mismatches with HTTP 403.
6
- *
7
- * This compat action normalizes the Anthropic `system` prompt into Claude Code official format.
8
- * It ensures the *first* `system` block is Claude Code's official string, while keeping any
9
- * existing system blocks (unless explicitly disabled).
4
+ * Canonicalizes the Anthropic `system` prompt into a single text block.
5
+ * Optionally preserves previous system blocks by prepending them to the first user message,
6
+ * so request semantics are preserved after normalization.
10
7
  */
11
- export declare function applyAnthropicClaudeCodeSystemPromptCompat(payload: JsonObject, config?: AnthropicClaudeCodeSystemPromptConfig): JsonObject;
8
+ export declare function applyAnthropicClaudeCodeSystemPromptCompat(payload: JsonObject, config?: AnthropicClaudeCodeSystemPromptConfig, adapterContext?: unknown): JsonObject;
@@ -1,7 +1,97 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
1
2
  const DEFAULT_SYSTEM_TEXT = "You are Claude Code, Anthropic's official CLI for Claude.";
3
+ const DEFAULT_USER_ID_ENV = 'ROUTECODEX_CLAUDE_CODE_USER_ID';
4
+ const DEFAULT_ACCOUNT_SEED_ENV = 'ROUTECODEX_CLAUDE_CODE_ACCOUNT_SEED';
5
+ const CLAUDE_CODE_USER_ID_REGEX = /^user_[0-9a-f]{64}_account__session_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
7
+ const HEX_32_REGEX = /^[0-9a-f]{32}$/i;
8
+ const SESSION_TOKEN_REGEX = /session[_:\-\s]?([0-9a-f]{8,}(?:-[0-9a-f]{4,}){0,5})/i;
2
9
  function isRecord(value) {
3
10
  return Boolean(value && typeof value === 'object' && !Array.isArray(value));
4
11
  }
12
+ function readTrimmedString(value) {
13
+ if (typeof value !== 'string') {
14
+ return undefined;
15
+ }
16
+ const trimmed = value.trim();
17
+ return trimmed.length ? trimmed : undefined;
18
+ }
19
+ function isClaudeCodeUserId(value) {
20
+ const trimmed = readTrimmedString(value);
21
+ if (!trimmed)
22
+ return false;
23
+ return CLAUDE_CODE_USER_ID_REGEX.test(trimmed);
24
+ }
25
+ function sha256Hex(value) {
26
+ return createHash('sha256').update(value).digest('hex');
27
+ }
28
+ function formatUuidFromHex32(hex32) {
29
+ const hex = hex32.toLowerCase();
30
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
31
+ }
32
+ function uuidFromSeed(seed) {
33
+ const hex = sha256Hex(seed);
34
+ // Make it UUIDv4-ish (version nibble "4", variant "8") so regex validators accept it.
35
+ const chars = hex.split('');
36
+ if (chars.length >= 32) {
37
+ chars[12] = '4';
38
+ chars[16] = '8';
39
+ }
40
+ return formatUuidFromHex32(chars.slice(0, 32).join(''));
41
+ }
42
+ function normalizeSessionUuid(candidate) {
43
+ const raw = readTrimmedString(candidate);
44
+ if (!raw)
45
+ return undefined;
46
+ const match = raw.match(SESSION_TOKEN_REGEX);
47
+ const trimmed = (match && match[1] ? match[1] : raw).trim();
48
+ if (UUID_REGEX.test(trimmed)) {
49
+ return trimmed.toLowerCase();
50
+ }
51
+ const compact = trimmed.replace(/-/g, '');
52
+ if (HEX_32_REGEX.test(compact)) {
53
+ return formatUuidFromHex32(compact);
54
+ }
55
+ return uuidFromSeed(trimmed);
56
+ }
57
+ function resolveClaudeCodeUserId(metadata, adapterContext) {
58
+ const existing = readTrimmedString(metadata.user_id);
59
+ if (isClaudeCodeUserId(existing)) {
60
+ return existing;
61
+ }
62
+ const envUserId = readTrimmedString(process.env[DEFAULT_USER_ID_ENV]);
63
+ if (isClaudeCodeUserId(envUserId)) {
64
+ return envUserId;
65
+ }
66
+ const clientHeaders = isRecord(metadata.clientHeaders) ? metadata.clientHeaders : undefined;
67
+ const ctx = isRecord(adapterContext) ? adapterContext : undefined;
68
+ const sessionUuid = normalizeSessionUuid(existing) ||
69
+ normalizeSessionUuid(envUserId) ||
70
+ normalizeSessionUuid(readTrimmedString(clientHeaders?.session_id)) ||
71
+ normalizeSessionUuid(readTrimmedString(clientHeaders?.['anthropic-session-id'])) ||
72
+ normalizeSessionUuid(readTrimmedString(clientHeaders?.['x-session-id'])) ||
73
+ normalizeSessionUuid(readTrimmedString(clientHeaders?.conversation_id)) ||
74
+ normalizeSessionUuid(readTrimmedString(clientHeaders?.['anthropic-conversation-id'])) ||
75
+ normalizeSessionUuid(readTrimmedString(clientHeaders?.['openai-conversation-id'])) ||
76
+ normalizeSessionUuid(readTrimmedString(metadata.sessionId)) ||
77
+ normalizeSessionUuid(readTrimmedString(metadata.conversationId)) ||
78
+ normalizeSessionUuid(readTrimmedString(ctx?.sessionId)) ||
79
+ normalizeSessionUuid(readTrimmedString(ctx?.conversationId));
80
+ const accountSeed = readTrimmedString(process.env[DEFAULT_ACCOUNT_SEED_ENV]) ||
81
+ readTrimmedString(process.env.USER) ||
82
+ 'routecodex';
83
+ try {
84
+ const accountHash = sha256Hex(accountSeed);
85
+ const session = sessionUuid ?? randomUUID();
86
+ return `user_${accountHash}_account__session_${session}`;
87
+ }
88
+ catch {
89
+ // As a last resort, keep compatibility best-effort.
90
+ const session = sessionUuid ?? randomUUID();
91
+ return `user_${'0'.repeat(64)}_account__session_${session}`;
92
+ }
93
+ // unreachable
94
+ }
5
95
  function normalizeSystemBlocks(system) {
6
96
  const blocks = [];
7
97
  const pushText = (text, extra) => {
@@ -63,15 +153,33 @@ function dedupeSystemBlocksByText(blocks) {
63
153
  }
64
154
  return result;
65
155
  }
156
+ function prependUserContent(messages, blocks) {
157
+ if (!Array.isArray(messages) || blocks.length === 0) {
158
+ return;
159
+ }
160
+ const first = messages[0];
161
+ if (isRecord(first) && typeof first.role === 'string' && first.role.toLowerCase() === 'user') {
162
+ const existing = first.content;
163
+ if (typeof existing === 'string') {
164
+ const injected = blocks.map((b) => b.text).filter(Boolean).join('\n\n');
165
+ first.content = existing.trim() ? `${injected}\n\n${existing}` : injected;
166
+ return;
167
+ }
168
+ if (Array.isArray(existing)) {
169
+ first.content = [...blocks, ...existing];
170
+ return;
171
+ }
172
+ first.content = [...blocks];
173
+ return;
174
+ }
175
+ messages.unshift({ role: 'user', content: [...blocks] });
176
+ }
66
177
  /**
67
- * tabglm (Anthropic-compatible) strict-gates requests to Claude Code official client.
68
- * It checks the system prompt format, and rejects mismatches with HTTP 403.
69
- *
70
- * This compat action normalizes the Anthropic `system` prompt into Claude Code official format.
71
- * It ensures the *first* `system` block is Claude Code's official string, while keeping any
72
- * existing system blocks (unless explicitly disabled).
178
+ * Canonicalizes the Anthropic `system` prompt into a single text block.
179
+ * Optionally preserves previous system blocks by prepending them to the first user message,
180
+ * so request semantics are preserved after normalization.
73
181
  */
74
- export function applyAnthropicClaudeCodeSystemPromptCompat(payload, config) {
182
+ export function applyAnthropicClaudeCodeSystemPromptCompat(payload, config, adapterContext) {
75
183
  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
76
184
  return payload;
77
185
  }
@@ -79,23 +187,34 @@ export function applyAnthropicClaudeCodeSystemPromptCompat(payload, config) {
79
187
  const systemText = (typeof config?.systemText === 'string' && config.systemText.trim())
80
188
  ? config.systemText.trim()
81
189
  : DEFAULT_SYSTEM_TEXT;
82
- const preserveExisting = config?.preserveExistingSystemAsUserMessage !== false;
83
- const existingBlocks = dedupeSystemBlocksByText(normalizeSystemBlocks(root.system));
84
- const official = { type: 'text', text: systemText };
85
- let nextBlocks = [];
86
- if (!preserveExisting) {
87
- nextBlocks = [official];
190
+ // Some Claude-Code-gated Anthropic proxies require the top-level `metadata` field to exist.
191
+ // We don't inspect/repair semantics: just ensure the field is present as an object.
192
+ if (!isRecord(root.metadata)) {
193
+ root.metadata = {};
88
194
  }
89
- else if (existingBlocks.length === 0) {
90
- nextBlocks = [official];
195
+ // Some proxies also require `metadata.user_id` to be present.
196
+ // Fill it with Claude Code's canonical `user_<sha256>_account__session_<uuid>` shape.
197
+ try {
198
+ const userId = resolveClaudeCodeUserId(root.metadata, adapterContext);
199
+ const current = readTrimmedString(root.metadata.user_id);
200
+ if (userId && !isClaudeCodeUserId(current)) {
201
+ root.metadata.user_id = userId;
202
+ }
91
203
  }
92
- else if (existingBlocks[0].text !== systemText) {
93
- nextBlocks = [official, ...existingBlocks.filter((b) => b.text !== systemText)];
204
+ catch {
205
+ // best-effort: never block compat due to metadata shaping failures
94
206
  }
95
- else {
96
- nextBlocks = [existingBlocks[0], ...existingBlocks.slice(1).filter((b) => b.text !== systemText)];
97
- nextBlocks[0] = { ...nextBlocks[0], type: 'text', text: systemText };
207
+ const preserveExisting = config?.preserveExistingSystemAsUserMessage !== false;
208
+ const existingBlocks = dedupeSystemBlocksByText(normalizeSystemBlocks(root.system))
209
+ .filter((b) => b.text !== systemText);
210
+ // Normalize: force system into a single text block.
211
+ root.system = [{ type: 'text', text: systemText }];
212
+ if (preserveExisting && existingBlocks.length) {
213
+ const messages = Array.isArray(root.messages) ? root.messages : [];
214
+ if (messages.length || root.messages !== undefined) {
215
+ prependUserContent(messages, existingBlocks);
216
+ root.messages = messages;
217
+ }
98
218
  }
99
- root.system = nextBlocks;
100
219
  return root;
101
220
  }
@@ -1,7 +1,42 @@
1
- import { cacheAntigravitySessionSignature, getAntigravityRequestSessionMeta } from '../antigravity-session-signature.js';
1
+ import { ANTIGRAVITY_GLOBAL_ALIAS_KEY, cacheAntigravitySessionSignature, getAntigravityRequestSessionMeta } from '../antigravity-session-signature.js';
2
2
  function isRecord(value) {
3
3
  return typeof value === 'object' && value !== null && !Array.isArray(value);
4
4
  }
5
+ function resolveStableSessionId(adapterContext) {
6
+ if (!adapterContext) {
7
+ return undefined;
8
+ }
9
+ const ctxAny = adapterContext;
10
+ const candidates = [ctxAny.sessionId, ctxAny.conversationId].filter((v) => typeof v === 'string');
11
+ const raw = candidates.map((s) => s.trim()).find((s) => s.length > 0);
12
+ if (!raw) {
13
+ return undefined;
14
+ }
15
+ // Antigravity-Manager alignment: never hash/derive session ids from external session/conversation identifiers here.
16
+ // If the caller already provides a proper fingerprint (sid-*), we can use it as a last-resort fallback.
17
+ return raw.toLowerCase().startsWith('sid-') ? raw : undefined;
18
+ }
19
+ function resolveAntigravityAliasKey(adapterContext) {
20
+ if (!adapterContext) {
21
+ return 'antigravity.unknown';
22
+ }
23
+ const ctxAny = adapterContext;
24
+ const candidates = [ctxAny.runtimeKey, ctxAny.providerKey, ctxAny.providerId].filter((v) => typeof v === 'string');
25
+ for (const value of candidates) {
26
+ const trimmed = value.trim();
27
+ if (!trimmed)
28
+ continue;
29
+ const lower = trimmed.toLowerCase();
30
+ if (lower.startsWith('antigravity.')) {
31
+ const parts = trimmed.split('.');
32
+ if (parts.length >= 2 && parts[0] && parts[1]) {
33
+ return `${parts[0].trim()}.${parts[1].trim()}`;
34
+ }
35
+ }
36
+ return trimmed;
37
+ }
38
+ return 'antigravity.unknown';
39
+ }
5
40
  function shouldEnableForAdapter(adapterContext) {
6
41
  if (!adapterContext) {
7
42
  return false;
@@ -10,14 +45,24 @@ function shouldEnableForAdapter(adapterContext) {
10
45
  if (protocol !== 'gemini-chat') {
11
46
  return false;
12
47
  }
13
- const providerIdOrKey = typeof adapterContext.providerId === 'string' ? adapterContext.providerId.trim().toLowerCase() : '';
48
+ const ctxAny = adapterContext;
49
+ const providerIdOrKeyRaw = typeof ctxAny.providerId === 'string'
50
+ ? String(ctxAny.providerId)
51
+ : typeof ctxAny.providerKey === 'string'
52
+ ? String(ctxAny.providerKey)
53
+ : typeof ctxAny.runtimeKey === 'string'
54
+ ? String(ctxAny.runtimeKey)
55
+ : '';
56
+ const providerIdOrKey = providerIdOrKeyRaw.trim().toLowerCase();
14
57
  const effectiveProviderId = providerIdOrKey.split('.')[0] ?? '';
15
- return effectiveProviderId === 'antigravity';
58
+ // Antigravity-Manager alignment: thoughtSignature compat applies to both Antigravity and Gemini CLI.
59
+ return effectiveProviderId === 'antigravity' || effectiveProviderId === 'gemini-cli';
16
60
  }
17
61
  export function cacheAntigravityThoughtSignatureFromGeminiResponse(payload, adapterContext) {
18
62
  if (!shouldEnableForAdapter(adapterContext)) {
19
63
  return payload;
20
64
  }
65
+ const fallbackAliasKey = resolveAntigravityAliasKey(adapterContext);
21
66
  const ctxAny = adapterContext;
22
67
  const payloadAny = payload;
23
68
  const keyCandidates = [
@@ -27,16 +72,25 @@ export function cacheAntigravityThoughtSignatureFromGeminiResponse(payload, adap
27
72
  typeof payloadAny.request_id === 'string' ? String(payloadAny.request_id) : '',
28
73
  typeof payloadAny.requestId === 'string' ? String(payloadAny.requestId) : ''
29
74
  ].filter((k) => typeof k === 'string' && k.trim().length);
75
+ let aliasKey = fallbackAliasKey;
30
76
  let sessionId = '';
31
77
  let messageCount = 1;
32
78
  for (const key of keyCandidates) {
33
79
  const resolved = getAntigravityRequestSessionMeta(key);
34
80
  if (resolved && resolved.sessionId.trim().length) {
81
+ aliasKey = typeof resolved.aliasKey === 'string' && resolved.aliasKey.trim().length ? resolved.aliasKey.trim() : fallbackAliasKey;
35
82
  sessionId = resolved.sessionId.trim();
36
83
  messageCount = resolved.messageCount;
37
84
  break;
38
85
  }
39
86
  }
87
+ if (!sessionId) {
88
+ const stable = resolveStableSessionId(adapterContext);
89
+ if (stable) {
90
+ sessionId = stable;
91
+ messageCount = 1;
92
+ }
93
+ }
40
94
  if (!sessionId) {
41
95
  return payload;
42
96
  }
@@ -47,14 +101,18 @@ export function cacheAntigravityThoughtSignatureFromGeminiResponse(payload, adap
47
101
  const partsRaw = content?.parts;
48
102
  const parts = Array.isArray(partsRaw) ? partsRaw : [];
49
103
  for (const part of parts) {
50
- if (!isRecord(part.functionCall)) {
51
- continue;
52
- }
53
- const sig = typeof part.thoughtSignature === 'string' ? String(part.thoughtSignature) : '';
54
- if (!sig.trim().length) {
55
- continue;
104
+ const sig = typeof part.thoughtSignature === 'string'
105
+ ? String(part.thoughtSignature)
106
+ : typeof part.thought_signature === 'string'
107
+ ? String(part.thought_signature)
108
+ : '';
109
+ if (sig.trim().length) {
110
+ cacheAntigravitySessionSignature(aliasKey, sessionId, sig.trim(), messageCount);
111
+ // Antigravity-Manager alignment: also store into the global signature store for this session.
112
+ if (aliasKey !== ANTIGRAVITY_GLOBAL_ALIAS_KEY) {
113
+ cacheAntigravitySessionSignature(ANTIGRAVITY_GLOBAL_ALIAS_KEY, sessionId, sig.trim(), messageCount);
114
+ }
56
115
  }
57
- cacheAntigravitySessionSignature(sessionId, sig.trim(), messageCount);
58
116
  }
59
117
  }
60
118
  return payload;
@@ -1,5 +1,4 @@
1
- import { createHash } from 'node:crypto';
2
- import { cacheAntigravityRequestSessionMeta, clearAntigravitySessionSignature, extractAntigravityGeminiSessionId, getAntigravitySessionSignatureEntry, shouldTreatAsMissingThoughtSignature } from '../antigravity-session-signature.js';
1
+ import { ANTIGRAVITY_GLOBAL_ALIAS_KEY, cacheAntigravityRequestSessionMeta, clearAntigravitySessionSignature, extractAntigravityGeminiSessionId, getAntigravityLatestSignatureSessionIdForAlias, lookupAntigravitySessionSignatureEntry, markAntigravitySessionSignatureRewind, shouldTreatAsMissingThoughtSignature } from '../antigravity-session-signature.js';
3
2
  function isRecord(value) {
4
3
  return typeof value === 'object' && value !== null && !Array.isArray(value);
5
4
  }
@@ -11,9 +10,31 @@ function shouldEnableForAdapter(adapterContext) {
11
10
  if (protocol !== 'gemini-chat') {
12
11
  return false;
13
12
  }
14
- const providerIdOrKey = typeof adapterContext.providerId === 'string' ? adapterContext.providerId.trim().toLowerCase() : '';
13
+ const ctxAny = adapterContext;
14
+ const providerIdOrKeyRaw = typeof ctxAny.providerId === 'string'
15
+ ? String(ctxAny.providerId)
16
+ : typeof ctxAny.providerKey === 'string'
17
+ ? String(ctxAny.providerKey)
18
+ : typeof ctxAny.runtimeKey === 'string'
19
+ ? String(ctxAny.runtimeKey)
20
+ : '';
21
+ const providerIdOrKey = providerIdOrKeyRaw.trim().toLowerCase();
15
22
  const effectiveProviderId = providerIdOrKey.split('.')[0] ?? '';
16
- return effectiveProviderId === 'antigravity';
23
+ // Antigravity-Manager alignment: thoughtSignature compat applies to both Antigravity and Gemini CLI
24
+ // (both route to Google Gemini internals that enforce thoughtSignature on tool loops).
25
+ return effectiveProviderId === 'antigravity' || effectiveProviderId === 'gemini-cli';
26
+ }
27
+ function shouldEnableSignatureRecovery(adapterContext) {
28
+ if (!adapterContext) {
29
+ return false;
30
+ }
31
+ const ctxAny = adapterContext;
32
+ const rtRaw = ctxAny.__rt;
33
+ if (!rtRaw || typeof rtRaw !== 'object' || Array.isArray(rtRaw)) {
34
+ return false;
35
+ }
36
+ const rt = rtRaw;
37
+ return rt.antigravityThoughtSignatureRecovery === true;
17
38
  }
18
39
  function locateGeminiContentsNode(root) {
19
40
  if (Array.isArray(root.contents)) {
@@ -29,23 +50,79 @@ function locateGeminiContentsNode(root) {
29
50
  }
30
51
  return undefined;
31
52
  }
32
- function sha256Hex(value) {
33
- return createHash('sha256').update(value).digest('hex');
34
- }
35
- function resolveStableSessionId(adapterContext) {
53
+ function resolveAntigravityAliasKey(adapterContext) {
36
54
  if (!adapterContext) {
37
- return undefined;
55
+ return 'antigravity.unknown';
38
56
  }
39
57
  const ctxAny = adapterContext;
40
- const candidates = [ctxAny.sessionId, ctxAny.conversationId].filter((v) => typeof v === 'string');
41
- const raw = candidates.map((s) => s.trim()).find((s) => s.length > 0);
42
- if (!raw) {
43
- return undefined;
58
+ const candidates = [ctxAny.runtimeKey, ctxAny.providerKey, ctxAny.providerId].filter((v) => typeof v === 'string');
59
+ for (const value of candidates) {
60
+ const trimmed = value.trim();
61
+ if (!trimmed)
62
+ continue;
63
+ const lower = trimmed.toLowerCase();
64
+ if (lower.startsWith('antigravity.')) {
65
+ const parts = trimmed.split('.');
66
+ if (parts.length >= 2 && parts[0] && parts[1]) {
67
+ return `${parts[0].trim()}.${parts[1].trim()}`;
68
+ }
69
+ }
70
+ return trimmed;
44
71
  }
45
- if (raw.toLowerCase().startsWith('sid-')) {
46
- return raw;
72
+ return 'antigravity.unknown';
73
+ }
74
+ function stripThoughtSignatures(contentsNode) {
75
+ const contentsRaw = contentsNode.contents;
76
+ if (!Array.isArray(contentsRaw)) {
77
+ return;
47
78
  }
48
- return `sid-${sha256Hex(raw).slice(0, 16)}`;
79
+ const contents = contentsRaw;
80
+ for (const entry of contents) {
81
+ if (!isRecord(entry))
82
+ continue;
83
+ const partsRaw = entry.parts;
84
+ if (!Array.isArray(partsRaw))
85
+ continue;
86
+ const parts = partsRaw;
87
+ for (const part of parts) {
88
+ if (!isRecord(part))
89
+ continue;
90
+ if ('thoughtSignature' in part) {
91
+ delete part.thoughtSignature;
92
+ }
93
+ if ('thought_signature' in part) {
94
+ delete part.thought_signature;
95
+ }
96
+ }
97
+ }
98
+ }
99
+ const ANTIGRAVITY_SIGNATURE_RECOVERY_PROMPT = "\n\n[System Recovery] Your previous output contained an invalid signature. Please regenerate the response without the corrupted signature block.";
100
+ function injectSignatureRecoveryPrompt(contentsNode) {
101
+ const contentsRaw = contentsNode.contents;
102
+ if (!Array.isArray(contentsRaw) || contentsRaw.length === 0) {
103
+ return;
104
+ }
105
+ const contents = contentsRaw;
106
+ const last = contents[contents.length - 1];
107
+ if (!isRecord(last)) {
108
+ return;
109
+ }
110
+ const partsRaw = last.parts;
111
+ if (!Array.isArray(partsRaw)) {
112
+ last.parts = [];
113
+ }
114
+ const parts = (Array.isArray(last.parts) ? last.parts : []);
115
+ const alreadyInjected = parts.some((part) => {
116
+ if (!isRecord(part))
117
+ return false;
118
+ const text = typeof part.text === 'string' ? String(part.text) : '';
119
+ return text.includes('[System Recovery]');
120
+ });
121
+ if (alreadyInjected) {
122
+ return;
123
+ }
124
+ parts.push({ text: ANTIGRAVITY_SIGNATURE_RECOVERY_PROMPT });
125
+ last.parts = parts;
49
126
  }
50
127
  function injectThoughtSignatureIntoFunctionCalls(contentsNode, signature) {
51
128
  const contentsRaw = contentsNode.contents;
@@ -77,9 +154,12 @@ export function prepareAntigravityThoughtSignatureForGeminiRequest(payload, adap
77
154
  if (!shouldEnableForAdapter(adapterContext)) {
78
155
  return payload;
79
156
  }
80
- const stableSessionId = resolveStableSessionId(adapterContext);
81
- const derivedSessionId = extractAntigravityGeminiSessionId(payload);
82
- const sessionId = stableSessionId || derivedSessionId;
157
+ const aliasKey = resolveAntigravityAliasKey(adapterContext);
158
+ // Antigravity-Manager alignment: sessionId is derived from the first user text (or JSON fallback),
159
+ // not from external session/conversation identifiers injected by other clients/hosts.
160
+ const originalSessionId = extractAntigravityGeminiSessionId(payload);
161
+ let sessionId = originalSessionId;
162
+ let usedLeasedSession = false;
83
163
  const ctxAny = adapterContext;
84
164
  const keys = [
85
165
  adapterContext.requestId,
@@ -89,16 +169,64 @@ export function prepareAntigravityThoughtSignatureForGeminiRequest(payload, adap
89
169
  const root = payload;
90
170
  const contentsNode = locateGeminiContentsNode(root);
91
171
  const messageCount = contentsNode && Array.isArray(contentsNode.contents) ? contentsNode.contents.length : 1;
172
+ // Recovery mode: strip any user-provided / stale signatures and do not lease/inject from cache.
173
+ if (shouldEnableSignatureRecovery(adapterContext)) {
174
+ if (contentsNode) {
175
+ stripThoughtSignatures(contentsNode);
176
+ // Antigravity-Manager alignment: append a repair hint so Gemini can regenerate without a corrupted signature.
177
+ injectSignatureRecoveryPrompt(contentsNode);
178
+ }
179
+ for (const key of keys) {
180
+ cacheAntigravityRequestSessionMeta(key, { aliasKey, sessionId: originalSessionId, messageCount });
181
+ }
182
+ return payload;
183
+ }
184
+ const resolveLookup = (sid) => {
185
+ const direct = lookupAntigravitySessionSignatureEntry(aliasKey, sid, { hydrate: true });
186
+ if (typeof direct.signature === 'string' && direct.signature.trim().length) {
187
+ return direct;
188
+ }
189
+ return lookupAntigravitySessionSignatureEntry(ANTIGRAVITY_GLOBAL_ALIAS_KEY, sid, { hydrate: true });
190
+ };
191
+ let lookup = resolveLookup(sessionId);
192
+ const hasSignature = typeof lookup.signature === 'string' && lookup.signature.trim().length && typeof lookup.messageCount === 'number';
193
+ if (!hasSignature) {
194
+ // Requested behavior:
195
+ // - Once an alias has ever obtained a signature, remember its sessionId.
196
+ // - For new sessions hitting the same alias, temporarily use that signature sessionId.
197
+ // This avoids cold-start tool-call failures when the current request's derived sessionId has no cached signature yet.
198
+ const leasedSessionId = getAntigravityLatestSignatureSessionIdForAlias(aliasKey, { hydrate: true });
199
+ if (leasedSessionId && leasedSessionId.trim().length && leasedSessionId.trim() !== sessionId) {
200
+ const leasedLookup = resolveLookup(leasedSessionId.trim());
201
+ const leasedHasSignature = typeof leasedLookup.signature === 'string' &&
202
+ leasedLookup.signature.trim().length &&
203
+ typeof leasedLookup.messageCount === 'number';
204
+ if (leasedHasSignature) {
205
+ sessionId = leasedSessionId.trim();
206
+ lookup = leasedLookup;
207
+ usedLeasedSession = true;
208
+ }
209
+ }
210
+ }
211
+ const effectiveSessionId = sessionId;
92
212
  for (const key of keys) {
93
- cacheAntigravityRequestSessionMeta(key, { sessionId, messageCount });
213
+ cacheAntigravityRequestSessionMeta(key, { aliasKey, sessionId: effectiveSessionId, messageCount });
94
214
  }
95
- const cached = getAntigravitySessionSignatureEntry(sessionId);
215
+ const cached = typeof lookup.signature === 'string' && typeof lookup.messageCount === 'number'
216
+ ? { signature: lookup.signature, messageCount: lookup.messageCount }
217
+ : undefined;
96
218
  if (!cached) {
97
219
  return payload;
98
220
  }
99
- if (typeof messageCount === 'number' && messageCount > 0 && messageCount < cached.messageCount) {
221
+ // Rewind detection is meaningful within the SAME session. When we lease a prior sessionId to reuse
222
+ // an existing signature, messageCount comparisons may be unrelated and should not invalidate the cache.
223
+ if (!usedLeasedSession && typeof messageCount === 'number' && messageCount > 0 && messageCount < cached.messageCount) {
100
224
  // Rewind detected: do not inject a "future" signature; clear and wait for a fresh signature from upstream.
101
- clearAntigravitySessionSignature(sessionId);
225
+ clearAntigravitySessionSignature(aliasKey, effectiveSessionId);
226
+ markAntigravitySessionSignatureRewind(aliasKey, effectiveSessionId, messageCount);
227
+ // Antigravity-Manager alignment: global signature store must also be blocked on rewinds.
228
+ clearAntigravitySessionSignature(ANTIGRAVITY_GLOBAL_ALIAS_KEY, effectiveSessionId);
229
+ markAntigravitySessionSignatureRewind(ANTIGRAVITY_GLOBAL_ALIAS_KEY, effectiveSessionId, messageCount);
102
230
  return payload;
103
231
  }
104
232
  if (!contentsNode) {