@jsonstudio/llms 0.6.1449 → 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 -6
  3. package/dist/conversion/compat/actions/anthropic-claude-code-system-prompt.js +179 -41
  4. package/dist/conversion/compat/actions/antigravity-thought-signature-cache.js +73 -14
  5. package/dist/conversion/compat/actions/antigravity-thought-signature-prepare.js +165 -10
  6. package/dist/conversion/compat/actions/gemini-cli-request.js +72 -13
  7. package/dist/conversion/compat/antigravity-session-signature.d.ts +68 -1
  8. package/dist/conversion/compat/antigravity-session-signature.js +833 -21
  9. package/dist/conversion/compat/profiles/anthropic-claude-code.json +17 -0
  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 +17 -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,10 +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 forces the Anthropic `system` prompt to Claude Code's official string.
8
- * Optionally preserves the previous system prompt by moving it into the user message stream.
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.
9
7
  */
10
- export declare function applyAnthropicClaudeCodeSystemPromptCompat(payload: JsonObject, config?: AnthropicClaudeCodeSystemPromptConfig): JsonObject;
8
+ export declare function applyAnthropicClaudeCodeSystemPromptCompat(payload: JsonObject, config?: AnthropicClaudeCodeSystemPromptConfig, adapterContext?: unknown): JsonObject;
@@ -1,62 +1,185 @@
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
  }
5
- function extractSystemText(system) {
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
+ }
95
+ function normalizeSystemBlocks(system) {
96
+ const blocks = [];
97
+ const pushText = (text, extra) => {
98
+ const trimmed = text.trim();
99
+ if (!trimmed) {
100
+ return;
101
+ }
102
+ blocks.push({
103
+ ...(extra ?? {}),
104
+ type: 'text',
105
+ text: trimmed
106
+ });
107
+ };
6
108
  if (typeof system === 'string') {
7
- return system.trim();
109
+ pushText(system);
110
+ return blocks;
8
111
  }
9
112
  if (Array.isArray(system)) {
10
- const parts = [];
11
113
  for (const entry of system) {
12
114
  if (typeof entry === 'string') {
13
- if (entry.trim())
14
- parts.push(entry.trim());
115
+ pushText(entry);
15
116
  continue;
16
117
  }
17
- if (!isRecord(entry))
118
+ if (!isRecord(entry)) {
18
119
  continue;
19
- const text = typeof entry.text === 'string' ? entry.text.trim() : '';
20
- if (text)
21
- parts.push(text);
120
+ }
121
+ const text = typeof entry.text === 'string' ? entry.text : '';
122
+ if (text) {
123
+ const extra = { ...entry };
124
+ delete extra.type;
125
+ delete extra.text;
126
+ pushText(text, extra);
127
+ }
22
128
  }
23
- return parts.join('\n').trim();
129
+ return blocks;
24
130
  }
25
131
  if (isRecord(system)) {
26
- const text = typeof system.text === 'string' ? system.text.trim() : '';
27
- return text;
132
+ const text = typeof system.text === 'string' ? system.text : '';
133
+ if (text) {
134
+ const extra = { ...system };
135
+ delete extra.type;
136
+ delete extra.text;
137
+ pushText(text, extra);
138
+ }
28
139
  }
29
- return '';
140
+ return blocks;
30
141
  }
31
- function ensureUserMessage(messages) {
32
- const first = messages[0];
33
- if (isRecord(first) && typeof first.role === 'string' && first.role.toLowerCase() === 'user') {
34
- return first;
142
+ function dedupeSystemBlocksByText(blocks) {
143
+ const seen = new Set();
144
+ const result = [];
145
+ for (const block of blocks) {
146
+ const text = typeof block.text === 'string' ? block.text.trim() : '';
147
+ if (!text)
148
+ continue;
149
+ if (seen.has(text))
150
+ continue;
151
+ seen.add(text);
152
+ result.push({ ...block, type: 'text', text });
35
153
  }
36
- const created = { role: 'user', content: '' };
37
- messages.unshift(created);
38
- return created;
154
+ return result;
39
155
  }
40
- function appendUserText(message, text) {
41
- if (!text.trim()) {
156
+ function prependUserContent(messages, blocks) {
157
+ if (!Array.isArray(messages) || blocks.length === 0) {
42
158
  return;
43
159
  }
44
- const existing = message.content;
45
- if (typeof existing === 'string') {
46
- message.content = existing.trim() ? `${text}\n\n${existing}` : text;
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];
47
173
  return;
48
174
  }
49
- // Anthropic supports content blocks, but many upstreams accept string; fall back to string.
50
- message.content = text;
175
+ messages.unshift({ role: 'user', content: [...blocks] });
51
176
  }
52
177
  /**
53
- * tabglm (Anthropic-compatible) strict-gates requests to Claude Code official client.
54
- * It checks the system prompt format, and rejects mismatches with HTTP 403.
55
- *
56
- * This compat action forces the Anthropic `system` prompt to Claude Code's official string.
57
- * Optionally preserves the previous system prompt by moving it into the user message stream.
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.
58
181
  */
59
- export function applyAnthropicClaudeCodeSystemPromptCompat(payload, config) {
182
+ export function applyAnthropicClaudeCodeSystemPromptCompat(payload, config, adapterContext) {
60
183
  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
61
184
  return payload;
62
185
  }
@@ -64,17 +187,32 @@ export function applyAnthropicClaudeCodeSystemPromptCompat(payload, config) {
64
187
  const systemText = (typeof config?.systemText === 'string' && config.systemText.trim())
65
188
  ? config.systemText.trim()
66
189
  : DEFAULT_SYSTEM_TEXT;
67
- const existingSystemText = extractSystemText(root.system);
68
- const already = existingSystemText === systemText;
69
- if (!already) {
70
- root.system = systemText;
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 = {};
194
+ }
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
+ }
203
+ }
204
+ catch {
205
+ // best-effort: never block compat due to metadata shaping failures
71
206
  }
72
- const preserve = config?.preserveExistingSystemAsUserMessage !== false;
73
- if (!already && preserve && existingSystemText && existingSystemText !== 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) {
74
213
  const messages = Array.isArray(root.messages) ? root.messages : [];
75
214
  if (messages.length || root.messages !== undefined) {
76
- const msg = ensureUserMessage(messages);
77
- appendUserText(msg, existingSystemText);
215
+ prependUserContent(messages, existingBlocks);
78
216
  root.messages = messages;
79
217
  }
80
218
  }
@@ -1,7 +1,42 @@
1
- import { cacheAntigravitySessionSignature, getAntigravityRequestSessionId } 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,18 +72,28 @@ 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 = '';
77
+ let messageCount = 1;
31
78
  for (const key of keyCandidates) {
32
- const resolved = getAntigravityRequestSessionId(key);
33
- if (resolved && resolved.trim().length) {
34
- sessionId = resolved.trim();
79
+ const resolved = getAntigravityRequestSessionMeta(key);
80
+ if (resolved && resolved.sessionId.trim().length) {
81
+ aliasKey = typeof resolved.aliasKey === 'string' && resolved.aliasKey.trim().length ? resolved.aliasKey.trim() : fallbackAliasKey;
82
+ sessionId = resolved.sessionId.trim();
83
+ messageCount = resolved.messageCount;
35
84
  break;
36
85
  }
37
86
  }
87
+ if (!sessionId) {
88
+ const stable = resolveStableSessionId(adapterContext);
89
+ if (stable) {
90
+ sessionId = stable;
91
+ messageCount = 1;
92
+ }
93
+ }
38
94
  if (!sessionId) {
39
95
  return payload;
40
96
  }
41
- const messageCount = 1;
42
97
  const candidatesRaw = payload.candidates;
43
98
  const candidates = Array.isArray(candidatesRaw) ? candidatesRaw : [];
44
99
  for (const candidate of candidates) {
@@ -46,14 +101,18 @@ export function cacheAntigravityThoughtSignatureFromGeminiResponse(payload, adap
46
101
  const partsRaw = content?.parts;
47
102
  const parts = Array.isArray(partsRaw) ? partsRaw : [];
48
103
  for (const part of parts) {
49
- if (!isRecord(part.functionCall)) {
50
- continue;
51
- }
52
- const sig = typeof part.thoughtSignature === 'string' ? String(part.thoughtSignature) : '';
53
- if (!sig.trim().length) {
54
- 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
+ }
55
115
  }
56
- cacheAntigravitySessionSignature(sessionId, sig.trim(), messageCount);
57
116
  }
58
117
  }
59
118
  return payload;
@@ -1,4 +1,4 @@
1
- import { cacheAntigravityRequestSessionId, extractAntigravityGeminiSessionId, getAntigravitySessionSignature, shouldTreatAsMissingThoughtSignature } from '../antigravity-session-signature.js';
1
+ import { ANTIGRAVITY_GLOBAL_ALIAS_KEY, cacheAntigravityRequestSessionMeta, clearAntigravitySessionSignature, extractAntigravityGeminiSessionId, getAntigravityLatestSignatureSessionIdForAlias, lookupAntigravitySessionSignatureEntry, markAntigravitySessionSignatureRewind, shouldTreatAsMissingThoughtSignature } from '../antigravity-session-signature.js';
2
2
  function isRecord(value) {
3
3
  return typeof value === 'object' && value !== null && !Array.isArray(value);
4
4
  }
@@ -10,9 +10,31 @@ function shouldEnableForAdapter(adapterContext) {
10
10
  if (protocol !== 'gemini-chat') {
11
11
  return false;
12
12
  }
13
- 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();
14
22
  const effectiveProviderId = providerIdOrKey.split('.')[0] ?? '';
15
- 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;
16
38
  }
17
39
  function locateGeminiContentsNode(root) {
18
40
  if (Array.isArray(root.contents)) {
@@ -28,6 +50,80 @@ function locateGeminiContentsNode(root) {
28
50
  }
29
51
  return undefined;
30
52
  }
53
+ function resolveAntigravityAliasKey(adapterContext) {
54
+ if (!adapterContext) {
55
+ return 'antigravity.unknown';
56
+ }
57
+ const ctxAny = adapterContext;
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;
71
+ }
72
+ return 'antigravity.unknown';
73
+ }
74
+ function stripThoughtSignatures(contentsNode) {
75
+ const contentsRaw = contentsNode.contents;
76
+ if (!Array.isArray(contentsRaw)) {
77
+ return;
78
+ }
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;
126
+ }
31
127
  function injectThoughtSignatureIntoFunctionCalls(contentsNode, signature) {
32
128
  const contentsRaw = contentsNode.contents;
33
129
  if (!Array.isArray(contentsRaw)) {
@@ -58,25 +154,84 @@ export function prepareAntigravityThoughtSignatureForGeminiRequest(payload, adap
58
154
  if (!shouldEnableForAdapter(adapterContext)) {
59
155
  return payload;
60
156
  }
61
- const sessionId = extractAntigravityGeminiSessionId(payload);
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;
62
163
  const ctxAny = adapterContext;
63
164
  const keys = [
64
165
  adapterContext.requestId,
65
166
  typeof ctxAny.clientRequestId === 'string' ? String(ctxAny.clientRequestId) : '',
66
167
  typeof ctxAny.groupRequestId === 'string' ? String(ctxAny.groupRequestId) : ''
67
168
  ].filter((k) => typeof k === 'string' && k.trim().length);
169
+ const root = payload;
170
+ const contentsNode = locateGeminiContentsNode(root);
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;
68
212
  for (const key of keys) {
69
- cacheAntigravityRequestSessionId(key, sessionId);
213
+ cacheAntigravityRequestSessionMeta(key, { aliasKey, sessionId: effectiveSessionId, messageCount });
70
214
  }
71
- const signature = getAntigravitySessionSignature(sessionId);
72
- if (!signature) {
215
+ const cached = typeof lookup.signature === 'string' && typeof lookup.messageCount === 'number'
216
+ ? { signature: lookup.signature, messageCount: lookup.messageCount }
217
+ : undefined;
218
+ if (!cached) {
219
+ return payload;
220
+ }
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) {
224
+ // Rewind detected: do not inject a "future" signature; clear and wait for a fresh signature from upstream.
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);
73
230
  return payload;
74
231
  }
75
- const root = payload;
76
- const contentsNode = locateGeminiContentsNode(root);
77
232
  if (!contentsNode) {
78
233
  return payload;
79
234
  }
80
- injectThoughtSignatureIntoFunctionCalls(contentsNode, signature);
235
+ injectThoughtSignatureIntoFunctionCalls(contentsNode, cached.signature);
81
236
  return payload;
82
237
  }