antigravity-claude-proxy 2.9.2 → 2.9.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antigravity-claude-proxy",
3
- "version": "2.9.2",
3
+ "version": "2.9.3",
4
4
  "description": "Proxy server to use Antigravity's Claude models with Claude Code CLI",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -1,36 +1,92 @@
1
1
  /**
2
2
  * Session Management for Cloud Code
3
3
  *
4
- * Handles session ID derivation for prompt caching continuity.
5
- * Session IDs are derived from the first user message to ensure
6
- * the same conversation uses the same session across turns.
4
+ * Session IDs give the Cloud Code backend a cache-affinity namespace.
5
+ * A per-conversation ID (stable hash of the first user message) keeps every
6
+ * conversation in its own namespace so interleaved conversations on the same
7
+ * account stop evicting each other's implicit-cache KV blocks. Requests
8
+ * without any user text fall back to a per-account process-lifetime ID.
7
9
  */
8
10
 
9
11
  import crypto from 'crypto';
10
12
 
11
13
 
12
- // Runtime storage for session IDs (per account)
13
- // This mimics the behavior of the binary which generates a session ID at startup
14
- // and keeps it for the process lifetime.
14
+ // Runtime fallback storage for session IDs (per account)
15
15
  // Key: accountEmail, Value: sessionId
16
16
  const runtimeSessionStore = new Map();
17
17
 
18
18
  /**
19
- * Get or create a session ID for the given account.
20
- *
21
- * The binary generates a session ID once at startup: `p.sessionID = rs() + Date.now()`.
22
- * Since our proxy is long-running, we simulate this "per-launch" behavior by storing
23
- * a generated ID in memory for each account.
19
+ * Extract the first meaningful user text from an Anthropic request.
20
+ * Returns null when no user message carries text.
21
+ */
22
+ function extractFirstUserText(anthropicRequest) {
23
+ const messages = anthropicRequest?.messages;
24
+ if (!Array.isArray(messages)) return null;
25
+ for (const msg of messages) {
26
+ if (!msg || msg.role !== 'user') continue;
27
+ const content = msg.content;
28
+ let text = null;
29
+ if (typeof content === 'string') {
30
+ text = content;
31
+ } else if (Array.isArray(content)) {
32
+ const parts = [];
33
+ for (const block of content) {
34
+ if (block && block.type === 'text' && typeof block.text === 'string') {
35
+ parts.push(block.text);
36
+ }
37
+ }
38
+ if (parts.length > 0) text = parts.join('\n');
39
+ }
40
+ if (text && text.trim().length > 0) {
41
+ // Hash the full text, never a prefix: clients such as Claude Code
42
+ // open the first user message with a long shared preamble
43
+ // (<system-reminder>, CLAUDE.md), so a truncated prefix maps
44
+ // every conversation in a project to the same session ID.
45
+ return text;
46
+ }
47
+ }
48
+ return null;
49
+ }
50
+
51
+ /**
52
+ * Derive a deterministic per-conversation session ID shaped like the binary's
53
+ * `uuid + Date.now()` format. The same conversation (same first user text,
54
+ * same account) always maps to the same ID across turns and proxy restarts.
55
+ */
56
+ function deriveConversationSessionId(anthropicRequest, accountEmail) {
57
+ const firstUserText = extractFirstUserText(anthropicRequest);
58
+ if (!firstUserText) return null;
59
+ const digest = crypto
60
+ .createHash('sha256')
61
+ .update(`${accountEmail || 'anonymous'}\u0000${firstUserText}`)
62
+ .digest('hex');
63
+ const uuid =
64
+ `${digest.slice(0, 8)}-${digest.slice(8, 12)}-${digest.slice(12, 16)}-` +
65
+ `${digest.slice(16, 20)}-${digest.slice(20, 32)}`;
66
+ const tail = BigInt(`0x${digest.slice(32, 45)}`).toString();
67
+ return `${uuid}${tail}`;
68
+ }
69
+
70
+ /**
71
+ * Get or create a session ID for a request.
24
72
  *
25
- * - If the proxy restarts, the ID changes (matching binary/VS Code restart behavior).
26
- * - Within a running proxy instance, the ID is stable for that account.
27
- * - This enables prompt caching while using the EXACT random logic of the binary.
73
+ * Priority:
74
+ * 1. Per-conversation deterministic ID from the first user message —
75
+ * restores cache continuity per conversation and isolates concurrent
76
+ * conversations from each other's cache pressure.
77
+ * 2. Per-account process-lifetime ID when the request carries no user
78
+ * text at all (previous behavior).
28
79
  *
29
- * @param {Object} anthropicRequest - The Anthropic-format request (unused for ID generation now)
30
- * @param {string} accountEmail - The account email to scope the session ID
80
+ * @param {Object} anthropicRequest - The Anthropic-format request
81
+ * @param {string} accountEmail - The account email to scope fallback IDs
31
82
  * @returns {string} A stable session ID string matching binary format
32
83
  */
33
84
  export function deriveSessionId(anthropicRequest, accountEmail) {
85
+ const conversationId = deriveConversationSessionId(anthropicRequest, accountEmail);
86
+ if (conversationId) {
87
+ return conversationId;
88
+ }
89
+
34
90
  if (!accountEmail) {
35
91
  // Fallback for requests without an account (should differ every time)
36
92
  return generateBinaryStyleId();
@@ -55,7 +55,11 @@ export async function* streamSSEResponse(response, originalModel) {
55
55
  const usage = innerResponse.usageMetadata;
56
56
  if (usage) {
57
57
  inputTokens = usage.promptTokenCount || inputTokens;
58
- outputTokens = usage.candidatesTokenCount || outputTokens;
58
+ // Anthropic semantics: output_tokens covers ALL generated
59
+ // tokens, thinking included (thoughtsTokenCount is billed as
60
+ // output by the backend). candidatesTokenCount alone silently
61
+ // undercounts thinking-heavy responses.
62
+ outputTokens = ((usage.candidatesTokenCount || 0) + (usage.thoughtsTokenCount || 0)) || outputTokens;
59
63
  cacheReadTokens = usage.cachedContentTokenCount || cacheReadTokens;
60
64
  }
61
65
 
@@ -279,10 +283,20 @@ export async function* streamSSEResponse(response, originalModel) {
279
283
  }
280
284
 
281
285
  // Emit message_delta and message_stop
286
+ //
287
+ // Usage reporting: Google streams usageMetadata progressively; the
288
+ // early chunks carry an interim promptTokenCount and no
289
+ // cachedContentTokenCount yet. message_start therefore latches an
290
+ // inflated input_tokens (the nearly-full prompt minus zero cache) whenever
291
+ // the request is served mostly from cache. The final usageMetadata is
292
+ // authoritative, so correct input_tokens here: clients that take the last
293
+ // non-zero value (or read message_delta.usage.input_tokens) then see the
294
+ // true uncached input instead of a phantom near-100%-of-prompt figure.
282
295
  yield {
283
296
  type: 'message_delta',
284
297
  delta: { stop_reason: stopReason || 'end_turn', stop_sequence: null },
285
298
  usage: {
299
+ input_tokens: Math.max(0, inputTokens - cacheReadTokens),
286
300
  output_tokens: outputTokens,
287
301
  cache_read_input_tokens: cacheReadTokens,
288
302
  cache_creation_input_tokens: 0
@@ -112,7 +112,8 @@ export function convertGoogleToAnthropic(googleResponse, model) {
112
112
  stop_sequence: null,
113
113
  usage: {
114
114
  input_tokens: promptTokens - cachedTokens,
115
- output_tokens: usageMetadata.candidatesTokenCount || 0,
115
+ // Thinking tokens are generated (and billed) output — include them.
116
+ output_tokens: (usageMetadata.candidatesTokenCount || 0) + (usageMetadata.thoughtsTokenCount || 0),
116
117
  cache_read_input_tokens: cachedTokens,
117
118
  cache_creation_input_tokens: 0
118
119
  }
@@ -1,82 +1,270 @@
1
1
  /**
2
- * Signature Cache
3
- * In-memory cache for Gemini thoughtSignatures
2
+ * Signature Cache — persistent, LRU-bounded
4
3
  *
5
- * Gemini models require thoughtSignature on tool calls, but Claude Code
6
- * strips non-standard fields. This cache stores signatures by tool_use_id
7
- * so they can be restored in subsequent requests.
4
+ * Gemini models require thoughtSignature on tool calls and thinking blocks,
5
+ * but Anthropic-protocol clients strip non-standard fields. This cache stores
6
+ * signatures by tool_use_id so they can be restored in subsequent requests.
7
+ *
8
+ * Signatures have no fixed time-based expiry — they stay valid for the lifetime
9
+ * of active conversations, and dropping them mid-conversation silently rewrites
10
+ * conversation history on replay, which busts the upstream implicit-cache prefix
11
+ * and drops thinking blocks. The store is persisted to disk and reloaded at boot,
12
+ * bounded by an LRU cap so memory and on-disk size stay bounded over months of use.
8
13
  *
9
14
  * Also caches thinking block signatures with model family for cross-model
10
15
  * compatibility checking.
16
+ *
17
+ * Persistence: JSON file, atomic tmp+rename writes, mode 0600, debounced
18
+ * async writes (plus a periodic safety flush and synchronous flush on SIGTERM/SIGINT/
19
+ * exit). All persistence failures fail open: the in-memory cache keeps
20
+ * working and the proxy never errors because of store I/O.
11
21
  */
12
22
 
13
- import { GEMINI_SIGNATURE_CACHE_TTL_MS, MIN_SIGNATURE_LENGTH } from '../constants.js';
23
+ import fs from 'node:fs';
24
+ import os from 'node:os';
25
+ import path from 'node:path';
26
+ import { MIN_SIGNATURE_LENGTH } from '../constants.js';
27
+
28
+ const toolSignatureCache = new Map(); // toolUseId -> signature (insertion-ordered LRU)
29
+ const thinkingSignatureCache = new Map(); // signature -> modelFamily (insertion-ordered LRU)
30
+
31
+ const STORE_VERSION = 1;
32
+ const DEFAULT_MAX_TOOL_SIGNATURES = 20_000;
33
+ const DEFAULT_MAX_THINKING_SIGNATURES = 10_000;
34
+ const FLUSH_DEBOUNCE_MS = 5000;
35
+ const PERIODIC_FLUSH_MS = 30_000;
36
+ const MAX_SILENT_STORE_ERRORS = 20;
37
+
38
+ function parsePositiveIntEnv(name, fallback) {
39
+ const raw = process.env[name];
40
+ if (!raw) return fallback;
41
+ const n = Number.parseInt(raw, 10);
42
+ return Number.isFinite(n) && n > 0 ? n : fallback;
43
+ }
44
+
45
+ const maxToolSignatures = parsePositiveIntEnv('ANTIGRAVITY_SIGNATURE_CACHE_MAX_TOOL', DEFAULT_MAX_TOOL_SIGNATURES);
46
+ const maxThinkingSignatures = parsePositiveIntEnv('ANTIGRAVITY_SIGNATURE_CACHE_MAX_THINK', DEFAULT_MAX_THINKING_SIGNATURES);
47
+
48
+ function lruSet(map, key, value, maxEntries) {
49
+ if (map.has(key)) {
50
+ map.delete(key);
51
+ }
52
+ map.set(key, value);
53
+ while (map.size > maxEntries) {
54
+ const oldestKey = map.keys().next().value;
55
+ if (oldestKey === undefined) break;
56
+ map.delete(oldestKey);
57
+ }
58
+ }
59
+
60
+ function lruGet(map, key) {
61
+ if (!map.has(key)) return null;
62
+ const value = map.get(key);
63
+ map.delete(key);
64
+ map.set(key, value);
65
+ return value;
66
+ }
67
+
68
+ function resolveStorePath() {
69
+ const fromEnv = process.env.ANTIGRAVITY_SIGNATURE_STORE;
70
+ if (fromEnv && fromEnv.trim()) return fromEnv;
71
+ return path.join(os.homedir(), '.config', 'antigravity-proxy', 'signature-store.json');
72
+ }
73
+
74
+ let storePath = resolveStorePath();
75
+ let flushTimer = null;
76
+ let periodicTimer = null;
77
+ let dirty = false;
78
+ let storeErrorCount = 0;
79
+ let writeInFlight = null;
80
+ let storeGeneration = 0;
14
81
 
15
- const signatureCache = new Map();
16
- const thinkingSignatureCache = new Map();
82
+ function reportStoreError(context, error) {
83
+ if (storeErrorCount < MAX_SILENT_STORE_ERRORS) {
84
+ storeErrorCount++;
85
+ // Console only: this module is imported before the logger is configured.
86
+ console.warn(`[SignatureCache] ${context} failed: ${error?.message ?? error}`);
87
+ }
88
+ }
89
+
90
+ function loadFromDisk() {
91
+ try {
92
+ const raw = fs.readFileSync(storePath, 'utf8');
93
+ if (!raw.trim()) return;
94
+ const parsed = JSON.parse(raw);
95
+ if (parsed?.version !== STORE_VERSION) return;
96
+ let prunedOnLoad = false;
97
+ if (parsed.tool && typeof parsed.tool === 'object') {
98
+ const entries = Object.entries(parsed.tool);
99
+ if (entries.length > maxToolSignatures) prunedOnLoad = true;
100
+ for (const [id, sig] of entries) {
101
+ if (typeof id === 'string' && typeof sig === 'string') {
102
+ lruSet(toolSignatureCache, id, sig, maxToolSignatures);
103
+ }
104
+ }
105
+ }
106
+ if (parsed.think && typeof parsed.think === 'object') {
107
+ const entries = Object.entries(parsed.think);
108
+ if (entries.length > maxThinkingSignatures) prunedOnLoad = true;
109
+ for (const [sig, family] of entries) {
110
+ if (typeof sig === 'string' && typeof family === 'string') {
111
+ lruSet(thinkingSignatureCache, sig, family, maxThinkingSignatures);
112
+ }
113
+ }
114
+ }
115
+ if (prunedOnLoad) {
116
+ scheduleFlush();
117
+ }
118
+ } catch (error) {
119
+ if (error?.code !== 'ENOENT') {
120
+ reportStoreError('load', error);
121
+ }
122
+ // Corrupt or unreadable store: start empty rather than failing boot.
123
+ }
124
+ }
125
+
126
+ function serializeStore() {
127
+ return JSON.stringify({
128
+ version: STORE_VERSION,
129
+ tool: Object.fromEntries(toolSignatureCache),
130
+ think: Object.fromEntries(thinkingSignatureCache),
131
+ });
132
+ }
17
133
 
18
134
  /**
19
- * Store a signature for a tool_use_id
135
+ * Synchronous flush, used only on process exit/signals where async I/O
136
+ * would never complete. Bumps the store generation so any async write still
137
+ * in flight discards its (older) snapshot instead of renaming over this one.
138
+ */
139
+ function writeStoreSync() {
140
+ if (!dirty) return;
141
+ storeGeneration++;
142
+ try {
143
+ const data = serializeStore();
144
+ fs.mkdirSync(path.dirname(storePath), { recursive: true });
145
+ const tmp = `${storePath}.tmp-sync-${process.pid}`;
146
+ fs.writeFileSync(tmp, data, { mode: 0o600 });
147
+ fs.renameSync(tmp, storePath);
148
+ dirty = false;
149
+ } catch (error) {
150
+ reportStoreError('flush', error);
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Normal-operation flush. File I/O runs asynchronously so a store near the
156
+ * LRU cap (tens of MB) never blocks the event loop on disk writes; only the
157
+ * JSON serialization stays synchronous. At most one write is in flight.
158
+ */
159
+ function writeStoreAsync() {
160
+ if (!dirty || writeInFlight) return;
161
+ let data;
162
+ try {
163
+ data = serializeStore();
164
+ } catch (error) {
165
+ reportStoreError('flush', error);
166
+ return;
167
+ }
168
+ dirty = false;
169
+ const generation = storeGeneration;
170
+ const targetPath = storePath;
171
+ const tmp = `${targetPath}.tmp-${process.pid}`;
172
+ let failed = false;
173
+ writeInFlight = (async () => {
174
+ try {
175
+ await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });
176
+ await fs.promises.writeFile(tmp, data, { mode: 0o600 });
177
+ if (generation !== storeGeneration) {
178
+ // Cache was cleared or flushed synchronously meanwhile.
179
+ await fs.promises.rm(tmp, { force: true });
180
+ return;
181
+ }
182
+ await fs.promises.rename(tmp, targetPath);
183
+ } catch (error) {
184
+ failed = true;
185
+ dirty = true;
186
+ reportStoreError('flush', error);
187
+ }
188
+ })().finally(() => {
189
+ writeInFlight = null;
190
+ // Changes made during the write get flushed promptly; after a failed
191
+ // write, leave the retry to the periodic flush instead of spinning.
192
+ if (dirty && !failed) scheduleFlush();
193
+ });
194
+ }
195
+
196
+ function scheduleFlush() {
197
+ dirty = true;
198
+ if (flushTimer) return;
199
+ flushTimer = setTimeout(() => {
200
+ flushTimer = null;
201
+ writeStoreAsync();
202
+ }, FLUSH_DEBOUNCE_MS);
203
+ if (typeof flushTimer.unref === 'function') flushTimer.unref();
204
+ }
205
+
206
+ function removeStoreFile() {
207
+ try {
208
+ fs.rmSync(storePath, { force: true });
209
+ } catch (error) {
210
+ reportStoreError('remove', error);
211
+ }
212
+ }
213
+
214
+ loadFromDisk();
215
+
216
+ periodicTimer = setInterval(() => writeStoreAsync(), PERIODIC_FLUSH_MS);
217
+ if (typeof periodicTimer.unref === 'function') periodicTimer.unref();
218
+
219
+ for (const signal of ['SIGTERM', 'SIGINT', 'exit']) {
220
+ process.on(signal, () => writeStoreSync());
221
+ }
222
+
223
+ /**
224
+ * Store a signature for a tool_use_id. Bounded by an LRU cap (no fixed TTL):
225
+ * replayed signatures stay byte-stable for active conversations.
20
226
  * @param {string} toolUseId - The tool use ID
21
227
  * @param {string} signature - The thoughtSignature to cache
22
228
  */
23
229
  export function cacheSignature(toolUseId, signature) {
24
230
  if (!toolUseId || !signature) return;
25
- signatureCache.set(toolUseId, {
26
- signature,
27
- timestamp: Date.now()
28
- });
231
+ const prev = toolSignatureCache.get(toolUseId);
232
+ lruSet(toolSignatureCache, toolUseId, signature, maxToolSignatures);
233
+ if (prev === signature) return;
234
+ scheduleFlush();
29
235
  }
30
236
 
31
237
  /**
32
- * Get a cached signature for a tool_use_id
238
+ * Get a cached signature for a tool_use_id.
33
239
  * @param {string} toolUseId - The tool use ID
34
- * @returns {string|null} The cached signature or null if not found/expired
240
+ * @returns {string|null} The cached signature or null if never seen
35
241
  */
36
242
  export function getCachedSignature(toolUseId) {
37
243
  if (!toolUseId) return null;
38
- const entry = signatureCache.get(toolUseId);
39
- if (!entry) return null;
40
-
41
- // Check TTL
42
- if (Date.now() - entry.timestamp > GEMINI_SIGNATURE_CACHE_TTL_MS) {
43
- signatureCache.delete(toolUseId);
44
- return null;
45
- }
46
-
47
- return entry.signature;
244
+ return lruGet(toolSignatureCache, toolUseId);
48
245
  }
49
246
 
50
247
  /**
51
- * Cache a thinking block signature with its model family
248
+ * Cache a thinking block signature with its model family. Bounded by an LRU cap.
52
249
  * @param {string} signature - The thinking signature to cache
53
250
  * @param {string} modelFamily - The model family ('claude' or 'gemini')
54
251
  */
55
252
  export function cacheThinkingSignature(signature, modelFamily) {
56
253
  if (!signature || signature.length < MIN_SIGNATURE_LENGTH) return;
57
- thinkingSignatureCache.set(signature, {
58
- modelFamily,
59
- timestamp: Date.now()
60
- });
254
+ const prev = thinkingSignatureCache.get(signature);
255
+ lruSet(thinkingSignatureCache, signature, modelFamily, maxThinkingSignatures);
256
+ if (prev === modelFamily) return;
257
+ scheduleFlush();
61
258
  }
62
259
 
63
260
  /**
64
- * Get the cached model family for a thinking signature
261
+ * Get the cached model family for a thinking signature.
65
262
  * @param {string} signature - The signature to look up
66
- * @returns {string|null} 'claude', 'gemini', or null if not found/expired
263
+ * @returns {string|null} 'claude', 'gemini', or null if never seen
67
264
  */
68
265
  export function getCachedSignatureFamily(signature) {
69
266
  if (!signature) return null;
70
- const entry = thinkingSignatureCache.get(signature);
71
- if (!entry) return null;
72
-
73
- // Check TTL
74
- if (Date.now() - entry.timestamp > GEMINI_SIGNATURE_CACHE_TTL_MS) {
75
- thinkingSignatureCache.delete(signature);
76
- return null;
77
- }
78
-
79
- return entry.modelFamily;
267
+ return lruGet(thinkingSignatureCache, signature);
80
268
  }
81
269
 
82
270
  /**
@@ -85,4 +273,36 @@ export function getCachedSignatureFamily(signature) {
85
273
  */
86
274
  export function clearThinkingSignatureCache() {
87
275
  thinkingSignatureCache.clear();
276
+ scheduleFlush();
277
+ }
278
+
279
+ /**
280
+ * Flush pending changes to disk now and wait for the write to finish.
281
+ * The debounced flush normally does this; exported for tests and callers
282
+ * that need the store on disk at a known point.
283
+ * @returns {Promise<void>}
284
+ */
285
+ export async function flushSignatureStore() {
286
+ if (flushTimer) {
287
+ clearTimeout(flushTimer);
288
+ flushTimer = null;
289
+ }
290
+ if (writeInFlight) await writeInFlight;
291
+ writeStoreAsync();
292
+ if (writeInFlight) await writeInFlight;
293
+ }
294
+
295
+ /**
296
+ * Clear both signature caches and delete the persisted store.
297
+ */
298
+ export function clearAllSignatureCaches() {
299
+ toolSignatureCache.clear();
300
+ thinkingSignatureCache.clear();
301
+ if (flushTimer) {
302
+ clearTimeout(flushTimer);
303
+ flushTimer = null;
304
+ }
305
+ dirty = false;
306
+ storeGeneration++; // any in-flight async write must not resurrect the store
307
+ removeStoreFile();
88
308
  }
@@ -656,6 +656,21 @@ function stripInvalidThinkingBlocks(messages, targetFamily = null) {
656
656
  * @returns {Array<Object>} Modified messages with synthetic messages injected
657
657
  */
658
658
  export function closeToolLoopForThinking(messages, targetFamily = null) {
659
+ // Escape hatch (Gemini only): skip the synthetic-message injection.
660
+ // Google's backend accepts raw mid-tool-loop and interrupted-tool
661
+ // histories natively (verified 2026-09-03: identical answers with and
662
+ // without injection), so for Gemini targets the fabricated messages are
663
+ // pure downside — a fake "[Continue]" becomes the model's latest user
664
+ // instruction and the count-bearing text is unstable across requests.
665
+ // Claude targets keep the recovery: the Anthropic API hard-rejects
666
+ // unclosed tool_use ids, so the interrupted-tool synthesis stays needed.
667
+ // Set ANTIGRAVITY_DISABLE_THINKING_RECOVERY=1 to activate.
668
+ if (
669
+ process.env.ANTIGRAVITY_DISABLE_THINKING_RECOVERY === '1' &&
670
+ targetFamily === 'gemini'
671
+ ) {
672
+ return messages;
673
+ }
659
674
  const state = analyzeConversationState(messages);
660
675
 
661
676
  // Handle neither tool loop nor interrupted tool
package/src/server.js CHANGED
@@ -17,7 +17,7 @@ const __dirname = path.dirname(__filename);
17
17
  import { forceRefresh } from './auth/token-extractor.js';
18
18
  import { REQUEST_BODY_LIMIT, BUILTIN_MODEL_MAPPINGS } from './constants.js';
19
19
  import { AccountManager } from './account-manager/index.js';
20
- import { clearThinkingSignatureCache } from './format/signature-cache.js';
20
+ import { clearAllSignatureCaches } from './format/signature-cache.js';
21
21
  import { formatDuration } from './utils/helpers.js';
22
22
  import { logger } from './utils/logger.js';
23
23
  import usageStats from './modules/usage-stats.js';
@@ -217,13 +217,13 @@ app.post('/', (req, res) => {
217
217
  });
218
218
 
219
219
  /**
220
- * Test endpoint - Clear thinking signature cache
220
+ * Test endpoint - Clear all signature caches (in-memory + persisted store)
221
221
  * Used for testing cold cache scenarios in cross-model tests
222
222
  */
223
223
  app.post('/test/clear-signature-cache', (req, res) => {
224
- clearThinkingSignatureCache();
225
- logger.debug('[Test] Cleared thinking signature cache');
226
- res.json({ success: true, message: 'Thinking signature cache cleared' });
224
+ clearAllSignatureCaches();
225
+ logger.debug('[Test] Cleared all signature caches and removed persisted store');
226
+ res.json({ success: true, message: 'All signature caches cleared' });
227
227
  });
228
228
 
229
229
  /**