@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
@@ -1,11 +1,85 @@
1
1
  import { createHash } from 'node:crypto';
2
- export const DUMMY_THOUGHT_SIGNATURE = 'skip_thought_signature_validator';
3
- // Antigravity-Manager alignment: Node proxy uses 2 hours TTL.
4
- const SIGNATURE_TTL_MS = 2 * 60 * 60 * 1000;
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ const DUMMY_THOUGHT_SIGNATURE_SENTINEL = 'skip_thought_signature_validator';
5
+ // Antigravity-Manager alignment:
6
+ // - session_id has no time limit (continuity across long conversations / restarts)
7
+ // - signature cache is bounded by size, not by time
8
+ // Time-based expiry is disabled when SIGNATURE_TTL_MS <= 0.
9
+ const SIGNATURE_TTL_MS = 0;
10
+ // Still "touch" active sessions to refresh persisted timestamps / LRU ordering,
11
+ // but avoid excessive disk churn.
12
+ const SIGNATURE_TOUCH_INTERVAL_MS = 5 * 60 * 1000;
5
13
  const MIN_SIGNATURE_LENGTH = 50;
6
14
  const SESSION_CACHE_LIMIT = 1000;
15
+ // Keep rewind guard finite to avoid reusing stale signatures after a rewind.
16
+ const REWIND_BLOCK_MS = 2 * 60 * 60 * 1000;
17
+ const GLOBAL_PERSISTENCE_KEY = '__LLMSWITCH_ANTIGRAVITY_SESSION_SIGNATURE_PERSISTENCE__';
18
+ function getPersistenceState() {
19
+ const g = globalThis;
20
+ const existing = g[GLOBAL_PERSISTENCE_KEY];
21
+ if (existing && typeof existing === 'object') {
22
+ return existing;
23
+ }
24
+ return null;
25
+ }
26
+ function setPersistenceState(state) {
27
+ const g = globalThis;
28
+ if (!state) {
29
+ delete g[GLOBAL_PERSISTENCE_KEY];
30
+ return;
31
+ }
32
+ g[GLOBAL_PERSISTENCE_KEY] = state;
33
+ }
34
+ export function configureAntigravitySessionSignaturePersistence(input) {
35
+ if (!input) {
36
+ const prior = getPersistenceState();
37
+ if (prior?.flushTimer) {
38
+ clearTimeout(prior.flushTimer);
39
+ }
40
+ setPersistenceState(null);
41
+ return;
42
+ }
43
+ const stateDir = typeof input.stateDir === 'string' ? input.stateDir.trim() : '';
44
+ if (!stateDir) {
45
+ setPersistenceState(null);
46
+ return;
47
+ }
48
+ const fileName = typeof input.fileName === 'string' && input.fileName.trim() ? input.fileName.trim() : 'antigravity-session-signatures.json';
49
+ const prior = getPersistenceState();
50
+ if (prior?.flushTimer) {
51
+ clearTimeout(prior.flushTimer);
52
+ }
53
+ setPersistenceState({
54
+ config: { stateDir, fileName },
55
+ loadedMtimeMs: null,
56
+ loadedOnce: false,
57
+ flushTimer: null
58
+ });
59
+ // Ensure we hydrate immediately so a short-lived process (or a server restart)
60
+ // never flushes an empty in-memory cache over an existing persisted file.
61
+ try {
62
+ hydrateSignaturesFromDiskIfNeeded(true);
63
+ }
64
+ catch {
65
+ // best-effort only
66
+ }
67
+ }
68
+ export function flushAntigravitySessionSignaturePersistenceSync() {
69
+ const state = getPersistenceState();
70
+ if (!state) {
71
+ return;
72
+ }
73
+ if (state.flushTimer) {
74
+ clearTimeout(state.flushTimer);
75
+ state.flushTimer = null;
76
+ }
77
+ flushPersistedSignaturesSync(state);
78
+ }
7
79
  const GLOBAL_SIGNATURE_CACHE_KEY = '__LLMSWITCH_ANTIGRAVITY_SESSION_SIGNATURE_CACHE__';
8
80
  const GLOBAL_REQUEST_SESSION_CACHE_KEY = '__LLMSWITCH_ANTIGRAVITY_REQUEST_SESSION_ID_CACHE__';
81
+ const GLOBAL_PINNED_ALIAS_BY_SESSION_KEY = '__LLMSWITCH_ANTIGRAVITY_PINNED_ALIAS_BY_SESSION__';
82
+ const GLOBAL_PINNED_SESSION_BY_ALIAS_KEY = '__LLMSWITCH_ANTIGRAVITY_PINNED_SESSION_BY_ALIAS__';
9
83
  function getGlobalSignatureCache() {
10
84
  const g = globalThis;
11
85
  const existing = g[GLOBAL_SIGNATURE_CACHE_KEY];
@@ -28,9 +102,152 @@ function getGlobalRequestSessionCache() {
28
102
  return created;
29
103
  }
30
104
  const requestSessionIds = getGlobalRequestSessionCache();
105
+ const GLOBAL_REWIND_BLOCK_CACHE_KEY = '__LLMSWITCH_ANTIGRAVITY_SIGNATURE_REWIND_BLOCK_CACHE__';
106
+ function getGlobalRewindBlockCache() {
107
+ const g = globalThis;
108
+ const existing = g[GLOBAL_REWIND_BLOCK_CACHE_KEY];
109
+ if (existing instanceof Map) {
110
+ return existing;
111
+ }
112
+ const created = new Map();
113
+ g[GLOBAL_REWIND_BLOCK_CACHE_KEY] = created;
114
+ return created;
115
+ }
116
+ const rewindBlocks = getGlobalRewindBlockCache();
117
+ const GLOBAL_LATEST_SIGNATURE_BY_ALIAS_KEY = '__LLMSWITCH_ANTIGRAVITY_LATEST_SIGNATURE_BY_ALIAS__';
118
+ // Antigravity-Manager alignment: global thoughtSignature store (v2).
119
+ // - Shared across all Antigravity/GeminiCLI accounts for the SAME derived sessionId.
120
+ // - Still keyed by sessionId to avoid cross-session leakage.
121
+ export const ANTIGRAVITY_GLOBAL_ALIAS_KEY = 'antigravity.global';
122
+ function normalizeAliasKey(value) {
123
+ if (typeof value !== 'string') {
124
+ return 'antigravity.unknown';
125
+ }
126
+ const trimmed = value.trim();
127
+ if (!trimmed.length) {
128
+ return 'antigravity.unknown';
129
+ }
130
+ const lowered = trimmed.toLowerCase();
131
+ if (lowered === 'antigravity') {
132
+ return 'antigravity.unknown';
133
+ }
134
+ return lowered;
135
+ }
136
+ function normalizeSessionId(value) {
137
+ if (typeof value !== 'string') {
138
+ return '';
139
+ }
140
+ return value.trim();
141
+ }
142
+ function buildSignatureCacheKey(aliasKey, sessionId) {
143
+ const alias = normalizeAliasKey(aliasKey);
144
+ const sid = normalizeSessionId(sessionId);
145
+ if (!sid) {
146
+ return '';
147
+ }
148
+ return `${alias}|${sid}`;
149
+ }
150
+ function getLatestSignatureMap() {
151
+ const g = globalThis;
152
+ const existing = g[GLOBAL_LATEST_SIGNATURE_BY_ALIAS_KEY];
153
+ if (existing instanceof Map) {
154
+ return existing;
155
+ }
156
+ const created = new Map();
157
+ g[GLOBAL_LATEST_SIGNATURE_BY_ALIAS_KEY] = created;
158
+ return created;
159
+ }
160
+ const latestSignaturesByAlias = getLatestSignatureMap();
161
+ function getPinnedAliasBySessionMap() {
162
+ const g = globalThis;
163
+ const existing = g[GLOBAL_PINNED_ALIAS_BY_SESSION_KEY];
164
+ if (existing instanceof Map) {
165
+ return existing;
166
+ }
167
+ const created = new Map();
168
+ g[GLOBAL_PINNED_ALIAS_BY_SESSION_KEY] = created;
169
+ return created;
170
+ }
171
+ function getPinnedSessionByAliasMap() {
172
+ const g = globalThis;
173
+ const existing = g[GLOBAL_PINNED_SESSION_BY_ALIAS_KEY];
174
+ if (existing instanceof Map) {
175
+ return existing;
176
+ }
177
+ const created = new Map();
178
+ g[GLOBAL_PINNED_SESSION_BY_ALIAS_KEY] = created;
179
+ return created;
180
+ }
181
+ const pinnedAliasBySession = getPinnedAliasBySessionMap();
182
+ const pinnedSessionByAlias = getPinnedSessionByAliasMap();
183
+ export function getAntigravityThoughtSignatureSentinel() {
184
+ return DUMMY_THOUGHT_SIGNATURE_SENTINEL;
185
+ }
186
+ function shouldAllowAliasLatestFallback(aliasKey) {
187
+ const normalized = normalizeAliasKey(aliasKey);
188
+ // Never fall back when alias is unknown: avoids cross-alias mixing and enforces isolation.
189
+ return normalized !== 'antigravity.unknown';
190
+ }
191
+ function getLatestSignatureEntry(aliasKey) {
192
+ const key = normalizeAliasKey(aliasKey);
193
+ const existing = latestSignaturesByAlias.get(key);
194
+ if (existing && typeof existing === 'object') {
195
+ const signature = typeof existing.signature === 'string' ? existing.signature : '';
196
+ const messageCount = typeof existing.messageCount === 'number' ? existing.messageCount : 1;
197
+ const timestamp = typeof existing.timestamp === 'number' ? existing.timestamp : 0;
198
+ const sessionId = typeof existing.sessionId === 'string'
199
+ ? String(existing.sessionId)
200
+ : '';
201
+ if (signature && timestamp > 0) {
202
+ return { signature, messageCount, timestamp, ...(sessionId.trim().length ? { sessionId: sessionId.trim() } : {}) };
203
+ }
204
+ }
205
+ return null;
206
+ }
207
+ function setLatestSignatureEntry(aliasKey, entry) {
208
+ const key = normalizeAliasKey(aliasKey);
209
+ if (!entry) {
210
+ latestSignaturesByAlias.delete(key);
211
+ return;
212
+ }
213
+ latestSignaturesByAlias.set(key, entry);
214
+ }
31
215
  function nowMs() {
32
216
  return Date.now();
33
217
  }
218
+ function isTimeExpiryEnabled() {
219
+ return typeof SIGNATURE_TTL_MS === 'number' && Number.isFinite(SIGNATURE_TTL_MS) && SIGNATURE_TTL_MS > 0;
220
+ }
221
+ export function getAntigravityLatestSignatureSessionIdForAlias(aliasKeyInput, options) {
222
+ const allowHydrate = options?.hydrate !== false;
223
+ const aliasKey = normalizeAliasKey(aliasKeyInput);
224
+ if (!shouldAllowAliasLatestFallback(aliasKey)) {
225
+ return undefined;
226
+ }
227
+ if (allowHydrate) {
228
+ hydrateSignaturesFromDiskIfNeeded(true);
229
+ }
230
+ const latest = getLatestSignatureEntry(aliasKey);
231
+ if (!latest) {
232
+ return undefined;
233
+ }
234
+ if (isTimeExpiryEnabled()) {
235
+ const ts = nowMs();
236
+ if (ts - latest.timestamp > SIGNATURE_TTL_MS) {
237
+ return undefined;
238
+ }
239
+ }
240
+ const sessionId = typeof latest.sessionId === 'string' ? latest.sessionId.trim() : '';
241
+ return sessionId || undefined;
242
+ }
243
+ function extractSessionIdFromCacheKey(key) {
244
+ if (typeof key !== 'string' || !key)
245
+ return '';
246
+ const idx = key.indexOf('|');
247
+ if (idx < 0)
248
+ return '';
249
+ return key.slice(idx + 1).trim();
250
+ }
34
251
  function isRecord(value) {
35
252
  return typeof value === 'object' && value !== null && !Array.isArray(value);
36
253
  }
@@ -63,18 +280,74 @@ function sha256Hex(value) {
63
280
  return createHash('sha256').update(value).digest('hex');
64
281
  }
65
282
  function isExpired(entry, ts) {
283
+ if (!isTimeExpiryEnabled()) {
284
+ return false;
285
+ }
286
+ return ts - entry.timestamp > SIGNATURE_TTL_MS;
287
+ }
288
+ function isPinnedExpired(entry, ts) {
289
+ if (!isTimeExpiryEnabled()) {
290
+ return false;
291
+ }
66
292
  return ts - entry.timestamp > SIGNATURE_TTL_MS;
67
293
  }
68
294
  function isRequestSessionExpired(entry, ts) {
295
+ if (!isTimeExpiryEnabled()) {
296
+ return false;
297
+ }
69
298
  return ts - entry.timestamp > SIGNATURE_TTL_MS;
70
299
  }
300
+ function touchSessionSignature(aliasKey, cacheKey, entry, ts) {
301
+ if (ts - entry.timestamp < SIGNATURE_TOUCH_INTERVAL_MS) {
302
+ return;
303
+ }
304
+ sessionSignatures.set(cacheKey, { signature: entry.signature, messageCount: entry.messageCount, timestamp: ts });
305
+ const latest = getLatestSignatureEntry(aliasKey);
306
+ if (!latest || ts >= latest.timestamp) {
307
+ const sessionId = extractSessionIdFromCacheKey(cacheKey);
308
+ setLatestSignatureEntry(aliasKey, {
309
+ signature: entry.signature,
310
+ messageCount: entry.messageCount,
311
+ timestamp: ts,
312
+ ...(sessionId ? { sessionId } : {})
313
+ });
314
+ }
315
+ schedulePersistenceFlush();
316
+ }
71
317
  function pruneExpired() {
318
+ if (!isTimeExpiryEnabled()) {
319
+ return;
320
+ }
72
321
  const ts = nowMs();
73
322
  for (const [key, entry] of sessionSignatures.entries()) {
74
323
  if (isExpired(entry, ts)) {
75
324
  sessionSignatures.delete(key);
76
325
  }
77
326
  }
327
+ for (const [sessionId, entry] of pinnedAliasBySession.entries()) {
328
+ if (isPinnedExpired(entry, ts)) {
329
+ pinnedAliasBySession.delete(sessionId);
330
+ const aliasKey = typeof entry.aliasKey === 'string' ? entry.aliasKey : '';
331
+ if (aliasKey) {
332
+ const backref = pinnedSessionByAlias.get(aliasKey);
333
+ if (backref?.sessionId === sessionId) {
334
+ pinnedSessionByAlias.delete(aliasKey);
335
+ }
336
+ }
337
+ }
338
+ }
339
+ for (const [aliasKey, entry] of pinnedSessionByAlias.entries()) {
340
+ if (isPinnedExpired(entry, ts)) {
341
+ pinnedSessionByAlias.delete(aliasKey);
342
+ const sid = typeof entry.sessionId === 'string' ? entry.sessionId : '';
343
+ if (sid) {
344
+ const backref = pinnedAliasBySession.get(sid);
345
+ if (backref?.aliasKey === aliasKey) {
346
+ pinnedAliasBySession.delete(sid);
347
+ }
348
+ }
349
+ }
350
+ }
78
351
  }
79
352
  function ensureCacheLimit() {
80
353
  if (sessionSignatures.size <= SESSION_CACHE_LIMIT) {
@@ -94,14 +367,308 @@ function ensureCacheLimit() {
94
367
  }
95
368
  }
96
369
  }
97
- export function cacheAntigravityRequestSessionId(requestId, sessionId) {
370
+ function resolvePersistFilePath(config) {
371
+ return path.join(config.stateDir, config.fileName);
372
+ }
373
+ function isValidPersistedEntry(value) {
374
+ if (!isRecord(value))
375
+ return false;
376
+ const sig = typeof value.signature === 'string' ? value.signature.trim() : '';
377
+ if (!sig || sig.length < MIN_SIGNATURE_LENGTH)
378
+ return false;
379
+ if (sig === DUMMY_THOUGHT_SIGNATURE_SENTINEL)
380
+ return false;
381
+ const messageCount = typeof value.messageCount === 'number' && Number.isFinite(value.messageCount) && value.messageCount > 0
382
+ ? Math.floor(value.messageCount)
383
+ : 1;
384
+ const timestamp = typeof value.timestamp === 'number' && Number.isFinite(value.timestamp) && value.timestamp > 0
385
+ ? Math.floor(value.timestamp)
386
+ : 0;
387
+ if (!timestamp)
388
+ return false;
389
+ value.signature = sig;
390
+ value.messageCount = messageCount;
391
+ value.timestamp = timestamp;
392
+ return true;
393
+ }
394
+ function hydrateSignaturesFromDiskIfNeeded(force = false) {
395
+ const state = getPersistenceState();
396
+ if (!state) {
397
+ return;
398
+ }
399
+ const filePath = resolvePersistFilePath(state.config);
400
+ let stat = null;
401
+ try {
402
+ stat = fs.statSync(filePath);
403
+ }
404
+ catch {
405
+ state.loadedOnce = true;
406
+ state.loadedMtimeMs = null;
407
+ return;
408
+ }
409
+ const mtimeMs = typeof stat.mtimeMs === 'number' && Number.isFinite(stat.mtimeMs) ? stat.mtimeMs : stat.mtime.getTime();
410
+ if (!force && state.loadedOnce && state.loadedMtimeMs === mtimeMs) {
411
+ return;
412
+ }
413
+ let parsed;
414
+ try {
415
+ parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
416
+ }
417
+ catch {
418
+ state.loadedOnce = true;
419
+ state.loadedMtimeMs = mtimeMs;
420
+ return;
421
+ }
422
+ const latestRaw = isRecord(parsed) ? parsed.latest : undefined;
423
+ const latestPersisted = isValidPersistedEntry(latestRaw)
424
+ ? { ...latestRaw }
425
+ : null;
426
+ const latestByAliasRaw = isRecord(parsed) ? parsed.latestByAlias : undefined;
427
+ const latestByAlias = isRecord(latestByAliasRaw) ? latestByAliasRaw : undefined;
428
+ const sessionsRaw = isRecord(parsed) ? parsed.sessions : undefined;
429
+ const sessions = isRecord(sessionsRaw) ? sessionsRaw : undefined;
430
+ if (!sessions) {
431
+ state.loadedOnce = true;
432
+ state.loadedMtimeMs = mtimeMs;
433
+ return;
434
+ }
435
+ const tsNow = nowMs();
436
+ const pinnedBySessionRaw = isRecord(parsed) ? parsed.pinnedBySession : undefined;
437
+ const pinnedBySession = isRecord(pinnedBySessionRaw) ? pinnedBySessionRaw : undefined;
438
+ if (pinnedBySession) {
439
+ for (const [sessionIdRaw, entryRaw] of Object.entries(pinnedBySession)) {
440
+ const sessionId = typeof sessionIdRaw === 'string' ? sessionIdRaw.trim() : '';
441
+ if (!sessionId)
442
+ continue;
443
+ if (!isRecord(entryRaw))
444
+ continue;
445
+ const aliasKeyRaw = entryRaw.aliasKey;
446
+ const aliasKey = typeof aliasKeyRaw === 'string' ? aliasKeyRaw.trim() : '';
447
+ const timestampRaw = entryRaw.timestamp;
448
+ const timestamp = typeof timestampRaw === 'number' && Number.isFinite(timestampRaw) ? Math.floor(timestampRaw) : 0;
449
+ if (!aliasKey || !timestamp)
450
+ continue;
451
+ if (isPinnedExpired({ timestamp }, tsNow))
452
+ continue;
453
+ const normalizedAlias = normalizeAliasKey(aliasKey);
454
+ const existing = pinnedAliasBySession.get(sessionId);
455
+ if (existing && !isPinnedExpired(existing, tsNow) && existing.timestamp >= timestamp) {
456
+ continue;
457
+ }
458
+ pinnedAliasBySession.set(sessionId, { aliasKey: normalizedAlias, timestamp });
459
+ }
460
+ }
461
+ const pinnedByAliasRaw = isRecord(parsed) ? parsed.pinnedByAlias : undefined;
462
+ const pinnedByAlias = isRecord(pinnedByAliasRaw) ? pinnedByAliasRaw : undefined;
463
+ if (pinnedByAlias) {
464
+ for (const [aliasKeyRaw, entryRaw] of Object.entries(pinnedByAlias)) {
465
+ const aliasKey = typeof aliasKeyRaw === 'string' ? aliasKeyRaw.trim() : '';
466
+ if (!aliasKey)
467
+ continue;
468
+ if (!isRecord(entryRaw))
469
+ continue;
470
+ const sessionIdRaw = entryRaw.sessionId;
471
+ const sessionId = typeof sessionIdRaw === 'string' ? sessionIdRaw.trim() : '';
472
+ const timestampRaw = entryRaw.timestamp;
473
+ const timestamp = typeof timestampRaw === 'number' && Number.isFinite(timestampRaw) ? Math.floor(timestampRaw) : 0;
474
+ if (!sessionId || !timestamp)
475
+ continue;
476
+ if (isPinnedExpired({ timestamp }, tsNow))
477
+ continue;
478
+ const normalizedAlias = normalizeAliasKey(aliasKey);
479
+ const existing = pinnedSessionByAlias.get(normalizedAlias);
480
+ if (existing && !isPinnedExpired(existing, tsNow) && existing.timestamp >= timestamp) {
481
+ continue;
482
+ }
483
+ pinnedSessionByAlias.set(normalizedAlias, { sessionId, timestamp });
484
+ }
485
+ }
486
+ for (const [persistedKey, entry] of Object.entries(sessions)) {
487
+ if (typeof persistedKey !== 'string' || !persistedKey.trim())
488
+ continue;
489
+ if (!isValidPersistedEntry(entry))
490
+ continue;
491
+ if (isExpired(entry, tsNow))
492
+ continue;
493
+ const keyTrimmed = persistedKey.trim();
494
+ const isScopedKey = keyTrimmed.includes('|');
495
+ const normalizedKeys = isScopedKey
496
+ ? [keyTrimmed]
497
+ : [
498
+ // Legacy v1 persistence stored signatures keyed only by sid-*.
499
+ // Treat them as session-global so they can be used for any alias (v2 global store).
500
+ buildSignatureCacheKey(ANTIGRAVITY_GLOBAL_ALIAS_KEY, keyTrimmed),
501
+ buildSignatureCacheKey('antigravity.unknown', keyTrimmed)
502
+ ].filter(Boolean);
503
+ for (const normalizedKey of normalizedKeys) {
504
+ if (!normalizedKey)
505
+ continue;
506
+ const existing = sessionSignatures.get(normalizedKey);
507
+ if (existing && !isExpired(existing, tsNow) && existing.timestamp >= entry.timestamp) {
508
+ continue;
509
+ }
510
+ sessionSignatures.set(normalizedKey, entry);
511
+ }
512
+ }
513
+ latestSignaturesByAlias.clear();
514
+ if (latestByAlias) {
515
+ for (const [aliasKeyRaw, entryRaw] of Object.entries(latestByAlias)) {
516
+ if (typeof aliasKeyRaw !== 'string' || !aliasKeyRaw.trim())
517
+ continue;
518
+ if (!isValidPersistedEntry(entryRaw))
519
+ continue;
520
+ if (isExpired(entryRaw, tsNow))
521
+ continue;
522
+ const sessionIdCandidate = isRecord(entryRaw) && typeof entryRaw.sessionId === 'string'
523
+ ? String(entryRaw.sessionId).trim()
524
+ : '';
525
+ setLatestSignatureEntry(aliasKeyRaw, {
526
+ signature: entryRaw.signature,
527
+ messageCount: entryRaw.messageCount,
528
+ timestamp: entryRaw.timestamp,
529
+ ...(sessionIdCandidate ? { sessionId: sessionIdCandidate } : {})
530
+ });
531
+ }
532
+ }
533
+ // Legacy v1 persistence ("latest" without alias) is kept only under "unknown" to avoid cross-alias mixing.
534
+ if (latestSignaturesByAlias.size === 0 && latestPersisted && !isExpired(latestPersisted, tsNow)) {
535
+ setLatestSignatureEntry('antigravity.unknown', {
536
+ signature: latestPersisted.signature,
537
+ messageCount: latestPersisted.messageCount,
538
+ timestamp: latestPersisted.timestamp
539
+ });
540
+ }
541
+ // Recompute per-alias latest values from session cache as a final best-effort pass.
542
+ for (const [key, entry] of sessionSignatures.entries()) {
543
+ if (isExpired(entry, tsNow))
544
+ continue;
545
+ const alias = key.split('|')[0] ?? 'unknown';
546
+ const existing = getLatestSignatureEntry(alias);
547
+ if (!existing || entry.timestamp > existing.timestamp) {
548
+ const sessionId = extractSessionIdFromCacheKey(key);
549
+ setLatestSignatureEntry(alias, {
550
+ signature: entry.signature,
551
+ messageCount: entry.messageCount,
552
+ timestamp: entry.timestamp,
553
+ ...(sessionId ? { sessionId } : {})
554
+ });
555
+ }
556
+ }
557
+ ensureCacheLimit();
558
+ state.loadedOnce = true;
559
+ state.loadedMtimeMs = mtimeMs;
560
+ }
561
+ function flushPersistedSignaturesSync(state) {
562
+ const filePath = resolvePersistFilePath(state.config);
563
+ // Avoid wiping existing persisted signatures when this process hasn't cached anything yet.
564
+ // Always re-hydrate before writing.
565
+ hydrateSignaturesFromDiskIfNeeded(true);
566
+ const tsNow = nowMs();
567
+ pruneExpired();
568
+ ensureCacheLimit();
569
+ const sessions = {};
570
+ for (const [sid, entry] of sessionSignatures.entries()) {
571
+ if (isExpired(entry, tsNow))
572
+ continue;
573
+ if (!entry.signature || entry.signature.trim().length < MIN_SIGNATURE_LENGTH)
574
+ continue;
575
+ if (entry.signature.trim() === DUMMY_THOUGHT_SIGNATURE_SENTINEL)
576
+ continue;
577
+ sessions[sid] = entry;
578
+ }
579
+ const latestByAliasPersist = {};
580
+ for (const [aliasKey, entry] of latestSignaturesByAlias.entries()) {
581
+ if (!entry.signature || entry.signature.trim().length < MIN_SIGNATURE_LENGTH)
582
+ continue;
583
+ if (entry.signature.trim() === DUMMY_THOUGHT_SIGNATURE_SENTINEL)
584
+ continue;
585
+ latestByAliasPersist[aliasKey] = entry;
586
+ }
587
+ const pinnedBySessionPersist = {};
588
+ for (const [sessionId, entry] of pinnedAliasBySession.entries()) {
589
+ const sid = typeof sessionId === 'string' ? sessionId.trim() : '';
590
+ if (!sid)
591
+ continue;
592
+ const aliasKey = typeof entry?.aliasKey === 'string' ? entry.aliasKey.trim() : '';
593
+ const timestamp = typeof entry?.timestamp === 'number' && Number.isFinite(entry.timestamp) ? Math.floor(entry.timestamp) : 0;
594
+ if (!aliasKey || !timestamp)
595
+ continue;
596
+ if (isPinnedExpired({ timestamp }, tsNow))
597
+ continue;
598
+ pinnedBySessionPersist[sid] = { aliasKey, timestamp };
599
+ }
600
+ const pinnedByAliasPersist = {};
601
+ for (const [aliasKey, entry] of pinnedSessionByAlias.entries()) {
602
+ const a = typeof aliasKey === 'string' ? aliasKey.trim() : '';
603
+ if (!a)
604
+ continue;
605
+ const sessionId = typeof entry?.sessionId === 'string' ? entry.sessionId.trim() : '';
606
+ const timestamp = typeof entry?.timestamp === 'number' && Number.isFinite(entry.timestamp) ? Math.floor(entry.timestamp) : 0;
607
+ if (!sessionId || !timestamp)
608
+ continue;
609
+ if (isPinnedExpired({ timestamp }, tsNow))
610
+ continue;
611
+ pinnedByAliasPersist[a] = { sessionId, timestamp };
612
+ }
613
+ try {
614
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
615
+ const tmpPath = `${filePath}.tmp.${process.pid}.${tsNow}`;
616
+ fs.writeFileSync(tmpPath, JSON.stringify({
617
+ version: 3,
618
+ updatedAt: tsNow,
619
+ sessions,
620
+ ...(Object.keys(latestByAliasPersist).length ? { latestByAlias: latestByAliasPersist } : {}),
621
+ ...(Object.keys(pinnedBySessionPersist).length ? { pinnedBySession: pinnedBySessionPersist } : {}),
622
+ ...(Object.keys(pinnedByAliasPersist).length ? { pinnedByAlias: pinnedByAliasPersist } : {})
623
+ }, null, 2), 'utf8');
624
+ fs.renameSync(tmpPath, filePath);
625
+ try {
626
+ const stat = fs.statSync(filePath);
627
+ const mtimeMs = typeof stat.mtimeMs === 'number' && Number.isFinite(stat.mtimeMs) ? stat.mtimeMs : stat.mtime.getTime();
628
+ state.loadedOnce = true;
629
+ state.loadedMtimeMs = mtimeMs;
630
+ }
631
+ catch {
632
+ // ignore
633
+ }
634
+ }
635
+ catch {
636
+ // best-effort persistence: must not affect runtime
637
+ }
638
+ }
639
+ function schedulePersistenceFlush() {
640
+ const state = getPersistenceState();
641
+ if (!state) {
642
+ return;
643
+ }
644
+ if (state.flushTimer) {
645
+ return;
646
+ }
647
+ state.flushTimer = setTimeout(() => {
648
+ state.flushTimer = null;
649
+ flushPersistedSignaturesSync(state);
650
+ }, 250);
651
+ state.flushTimer.unref?.();
652
+ }
653
+ export function cacheAntigravityRequestSessionId(requestId, a, b) {
654
+ if (typeof b === 'string') {
655
+ cacheAntigravityRequestSessionMeta(requestId, { aliasKey: a, sessionId: b });
656
+ return;
657
+ }
658
+ cacheAntigravityRequestSessionMeta(requestId, { sessionId: a });
659
+ }
660
+ export function cacheAntigravityRequestSessionMeta(requestId, meta) {
98
661
  const rid = typeof requestId === 'string' ? requestId.trim() : '';
99
- const sid = typeof sessionId === 'string' ? sessionId.trim() : '';
662
+ const sid = typeof meta?.sessionId === 'string' ? meta.sessionId.trim() : '';
100
663
  if (!rid || !sid) {
101
664
  return;
102
665
  }
666
+ const aliasKey = normalizeAliasKey(meta?.aliasKey);
667
+ const messageCount = typeof meta?.messageCount === 'number' && Number.isFinite(meta.messageCount) && meta.messageCount > 0
668
+ ? Math.floor(meta.messageCount)
669
+ : 1;
103
670
  const ts = nowMs();
104
- requestSessionIds.set(rid, { sessionId: sid, timestamp: ts });
671
+ requestSessionIds.set(rid, { aliasKey, sessionId: sid, messageCount, timestamp: ts });
105
672
  if (requestSessionIds.size <= SESSION_CACHE_LIMIT) {
106
673
  return;
107
674
  }
@@ -116,6 +683,10 @@ export function cacheAntigravityRequestSessionId(requestId, sessionId) {
116
683
  }
117
684
  }
118
685
  export function getAntigravityRequestSessionId(requestId) {
686
+ const meta = getAntigravityRequestSessionMeta(requestId);
687
+ return meta?.sessionId;
688
+ }
689
+ export function getAntigravityRequestSessionMeta(requestId) {
119
690
  const rid = typeof requestId === 'string' ? requestId.trim() : '';
120
691
  if (!rid) {
121
692
  return undefined;
@@ -129,7 +700,7 @@ export function getAntigravityRequestSessionId(requestId) {
129
700
  requestSessionIds.delete(rid);
130
701
  return undefined;
131
702
  }
132
- return entry.sessionId;
703
+ return { aliasKey: entry.aliasKey, sessionId: entry.sessionId, messageCount: entry.messageCount };
133
704
  }
134
705
  function findGeminiContentsNode(payload) {
135
706
  if (!isRecord(payload)) {
@@ -176,7 +747,9 @@ export function extractAntigravityGeminiSessionId(payload) {
176
747
  }
177
748
  }
178
749
  const combined = texts.join(' ').trim();
179
- if (combined.length > 10 && !combined.includes('<system-reminder>')) {
750
+ // Antigravity-Manager alignment: always derive a stable session_id from the FIRST user message text,
751
+ // even if the prompt is short (avoid JSON fallback instability across tool loops / followups).
752
+ if (combined.length > 0) {
180
753
  seed = combined;
181
754
  break;
182
755
  }
@@ -187,14 +760,23 @@ export function extractAntigravityGeminiSessionId(payload) {
187
760
  const hash = sha256Hex(seed);
188
761
  return `sid-${hash.slice(0, 16)}`;
189
762
  }
190
- export function cacheAntigravitySessionSignature(sessionId, signature, messageCount = 1) {
191
- if (typeof sessionId !== 'string' || !sessionId.trim()) {
763
+ export function cacheAntigravitySessionSignature(a, b, c, d = 1) {
764
+ const isNewSignature = typeof c === 'string';
765
+ const aliasKey = isNewSignature ? a : 'antigravity.unknown';
766
+ const sessionId = isNewSignature ? b : a;
767
+ const signature = isNewSignature ? c : b;
768
+ const messageCount = typeof c === 'number' ? c : typeof d === 'number' ? d : 1;
769
+ const key = buildSignatureCacheKey(aliasKey, sessionId);
770
+ if (!key) {
192
771
  return;
193
772
  }
194
- if (typeof signature !== 'string' || signature.length < MIN_SIGNATURE_LENGTH) {
773
+ if (typeof signature !== 'string') {
774
+ return;
775
+ }
776
+ const trimmedSignature = signature.trim();
777
+ if (trimmedSignature.length < MIN_SIGNATURE_LENGTH || trimmedSignature === DUMMY_THOUGHT_SIGNATURE_SENTINEL) {
195
778
  return;
196
779
  }
197
- const key = sessionId.trim();
198
780
  const ts = nowMs();
199
781
  const existing = sessionSignatures.get(key);
200
782
  let shouldStore = false;
@@ -217,29 +799,259 @@ export function cacheAntigravitySessionSignature(sessionId, signature, messageCo
217
799
  if (!shouldStore) {
218
800
  return;
219
801
  }
220
- sessionSignatures.set(key, { signature, messageCount, timestamp: ts });
802
+ sessionSignatures.set(key, { signature: trimmedSignature, messageCount, timestamp: ts });
803
+ rewindBlocks.delete(key);
804
+ const latest = getLatestSignatureEntry(aliasKey);
805
+ if (!latest || ts >= latest.timestamp) {
806
+ setLatestSignatureEntry(aliasKey, { signature: trimmedSignature, messageCount, timestamp: ts, sessionId });
807
+ }
808
+ maybePinAntigravitySessionToAlias(aliasKey, sessionId, ts);
221
809
  ensureCacheLimit();
810
+ schedulePersistenceFlush();
811
+ }
812
+ function maybePinAntigravitySessionToAlias(aliasKeyInput, sessionIdInput, ts) {
813
+ const aliasKey = normalizeAliasKey(aliasKeyInput);
814
+ const sessionId = normalizeSessionId(sessionIdInput);
815
+ if (!sessionId) {
816
+ return;
817
+ }
818
+ if (aliasKey === 'antigravity.unknown' || aliasKey === ANTIGRAVITY_GLOBAL_ALIAS_KEY) {
819
+ return;
820
+ }
821
+ const existing = pinnedAliasBySession.get(sessionId);
822
+ if (existing && !isPinnedExpired(existing, ts)) {
823
+ if (existing.aliasKey === aliasKey && ts - existing.timestamp >= SIGNATURE_TOUCH_INTERVAL_MS) {
824
+ pinnedAliasBySession.set(sessionId, { aliasKey, timestamp: ts });
825
+ pinnedSessionByAlias.set(aliasKey, { sessionId, timestamp: ts });
826
+ schedulePersistenceFlush();
827
+ }
828
+ return;
829
+ }
830
+ const existingForAlias = pinnedSessionByAlias.get(aliasKey);
831
+ if (existingForAlias && !isPinnedExpired(existingForAlias, ts) && existingForAlias.sessionId !== sessionId) {
832
+ return;
833
+ }
834
+ pinnedAliasBySession.set(sessionId, { aliasKey, timestamp: ts });
835
+ pinnedSessionByAlias.set(aliasKey, { sessionId, timestamp: ts });
836
+ schedulePersistenceFlush();
222
837
  }
223
- export function getAntigravitySessionSignature(sessionId) {
224
- if (typeof sessionId !== 'string' || !sessionId.trim()) {
838
+ export function lookupAntigravityPinnedAliasForSessionId(sessionIdInput, options) {
839
+ const sessionId = normalizeSessionId(sessionIdInput);
840
+ if (!sessionId) {
225
841
  return undefined;
226
842
  }
227
- const key = sessionId.trim();
228
- const entry = sessionSignatures.get(key);
843
+ const allowHydrate = options?.hydrate !== false;
844
+ if (allowHydrate) {
845
+ hydrateSignaturesFromDiskIfNeeded(true);
846
+ }
847
+ const entry = pinnedAliasBySession.get(sessionId);
229
848
  if (!entry) {
230
849
  return undefined;
231
850
  }
232
851
  const ts = nowMs();
233
- if (isExpired(entry, ts)) {
234
- sessionSignatures.delete(key);
852
+ if (isPinnedExpired(entry, ts)) {
853
+ pinnedAliasBySession.delete(sessionId);
235
854
  return undefined;
236
855
  }
237
- return entry.signature;
856
+ if (ts - entry.timestamp >= SIGNATURE_TOUCH_INTERVAL_MS) {
857
+ pinnedAliasBySession.set(sessionId, { aliasKey: entry.aliasKey, timestamp: ts });
858
+ pinnedSessionByAlias.set(entry.aliasKey, { sessionId, timestamp: ts });
859
+ schedulePersistenceFlush();
860
+ }
861
+ return entry.aliasKey;
862
+ }
863
+ export function unpinAntigravitySessionAliasForSessionId(sessionIdInput) {
864
+ const sessionId = normalizeSessionId(sessionIdInput);
865
+ if (!sessionId) {
866
+ return;
867
+ }
868
+ hydrateSignaturesFromDiskIfNeeded(true);
869
+ const existing = pinnedAliasBySession.get(sessionId);
870
+ if (!existing) {
871
+ return;
872
+ }
873
+ pinnedAliasBySession.delete(sessionId);
874
+ const aliasKey = typeof existing.aliasKey === 'string' ? existing.aliasKey : '';
875
+ if (aliasKey) {
876
+ const backref = pinnedSessionByAlias.get(aliasKey);
877
+ if (backref?.sessionId === sessionId) {
878
+ pinnedSessionByAlias.delete(aliasKey);
879
+ }
880
+ }
881
+ schedulePersistenceFlush();
882
+ }
883
+ export function getAntigravitySessionSignature(a, b) {
884
+ return getAntigravitySessionSignatureEntry(a, b)?.signature;
885
+ }
886
+ export function lookupAntigravitySessionSignatureEntry(aliasKeyInput, sessionIdInput, options) {
887
+ const allowHydrate = options?.hydrate !== false;
888
+ const aliasKey = normalizeAliasKey(aliasKeyInput);
889
+ const sessionId = normalizeSessionId(sessionIdInput);
890
+ const cacheKey = buildSignatureCacheKey(aliasKey, sessionId);
891
+ if (!cacheKey) {
892
+ return { aliasKey, sessionId, cacheKey, source: 'miss' };
893
+ }
894
+ let entry = sessionSignatures.get(cacheKey);
895
+ if (!entry && allowHydrate) {
896
+ hydrateSignaturesFromDiskIfNeeded(true);
897
+ entry = sessionSignatures.get(cacheKey);
898
+ }
899
+ if (entry) {
900
+ const ts = nowMs();
901
+ if (isExpired(entry, ts)) {
902
+ sessionSignatures.delete(cacheKey);
903
+ return { aliasKey, sessionId, cacheKey, source: 'expired' };
904
+ }
905
+ touchSessionSignature(aliasKey, cacheKey, entry, ts);
906
+ return {
907
+ aliasKey,
908
+ sessionId,
909
+ cacheKey,
910
+ source: 'session_cache',
911
+ signature: entry.signature,
912
+ messageCount: entry.messageCount,
913
+ sourceSessionId: sessionId,
914
+ sourceTimestamp: entry.timestamp
915
+ };
916
+ }
917
+ if (!shouldAllowAliasLatestFallback(aliasKey)) {
918
+ return { aliasKey, sessionId, cacheKey, source: 'blocked_unknown_alias' };
919
+ }
920
+ const ts = nowMs();
921
+ const rewind = rewindBlocks.get(cacheKey);
922
+ if (rewind && ts < rewind.until) {
923
+ return { aliasKey, sessionId, cacheKey, source: 'blocked_rewind' };
924
+ }
925
+ // Antigravity-Manager alignment: do NOT reuse signatures across sessions.
926
+ // If we miss the session cache, the caller must omit thoughtSignature and wait for upstream to provide a real one.
927
+ // (Persistence is still used to restore the same session across restarts, not to "heal" unrelated sessions.)
928
+ if (allowHydrate) {
929
+ hydrateSignaturesFromDiskIfNeeded(true);
930
+ const hydrated = sessionSignatures.get(cacheKey);
931
+ if (hydrated && !isExpired(hydrated, ts)) {
932
+ touchSessionSignature(aliasKey, cacheKey, hydrated, ts);
933
+ return {
934
+ aliasKey,
935
+ sessionId,
936
+ cacheKey,
937
+ source: 'session_cache',
938
+ signature: hydrated.signature,
939
+ messageCount: hydrated.messageCount,
940
+ sourceSessionId: sessionId,
941
+ sourceTimestamp: hydrated.timestamp
942
+ };
943
+ }
944
+ }
945
+ return { aliasKey, sessionId, cacheKey, source: 'miss' };
946
+ }
947
+ export function getAntigravitySessionSignatureEntry(a, b, c) {
948
+ const hasAlias = typeof b === 'string';
949
+ const options = (hasAlias ? c : b);
950
+ const aliasKey = normalizeAliasKey(hasAlias ? a : 'antigravity.unknown');
951
+ const sessionId = hasAlias ? b : a;
952
+ const lookup = lookupAntigravitySessionSignatureEntry(aliasKey, sessionId, options);
953
+ if (lookup.signature && typeof lookup.messageCount === 'number') {
954
+ return { signature: lookup.signature, messageCount: lookup.messageCount };
955
+ }
956
+ return undefined;
957
+ }
958
+ export function clearAntigravitySessionSignature(a, b) {
959
+ const hasAlias = typeof b === 'string';
960
+ const aliasKey = hasAlias ? a : 'antigravity.unknown';
961
+ const sessionId = hasAlias ? b : a;
962
+ const key = buildSignatureCacheKey(aliasKey, sessionId);
963
+ if (!key) {
964
+ return;
965
+ }
966
+ sessionSignatures.delete(key);
967
+ // Keep latest-by-alias coherent so leasing doesn't keep pointing at a cleared signature.
968
+ if (hasAlias) {
969
+ const latest = getLatestSignatureEntry(aliasKey);
970
+ if (latest?.sessionId && latest.sessionId.trim() === normalizeSessionId(sessionId)) {
971
+ setLatestSignatureEntry(aliasKey, null);
972
+ }
973
+ }
974
+ schedulePersistenceFlush();
975
+ }
976
+ export function markAntigravitySessionSignatureRewind(aliasKey, sessionId, messageCount = 1) {
977
+ const key = buildSignatureCacheKey(aliasKey, sessionId);
978
+ if (!key) {
979
+ return;
980
+ }
981
+ const ts = nowMs();
982
+ const mc = typeof messageCount === 'number' && Number.isFinite(messageCount) && messageCount > 0 ? Math.floor(messageCount) : 1;
983
+ rewindBlocks.set(key, { timestamp: ts, until: ts + REWIND_BLOCK_MS, messageCount: mc });
984
+ // Best-effort bound on memory usage.
985
+ if (rewindBlocks.size > SESSION_CACHE_LIMIT) {
986
+ const entries = Array.from(rewindBlocks.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp);
987
+ const overflow = rewindBlocks.size - SESSION_CACHE_LIMIT;
988
+ for (let i = 0; i < overflow; i++) {
989
+ const k = entries[i]?.[0];
990
+ if (k)
991
+ rewindBlocks.delete(k);
992
+ }
993
+ }
994
+ }
995
+ /**
996
+ * Clear thoughtSignature caches + pins for a specific (aliasKey, sessionId).
997
+ *
998
+ * Used for "Invalid signature / Corrupted thought signature / thinking.signature" style upstream errors,
999
+ * where keeping a persisted signature would cause repeated 400s after restart.
1000
+ */
1001
+ export function invalidateAntigravitySessionSignature(aliasKeyInput, sessionIdInput) {
1002
+ const aliasKey = normalizeAliasKey(aliasKeyInput);
1003
+ const sessionId = normalizeSessionId(sessionIdInput);
1004
+ if (!sessionId) {
1005
+ return;
1006
+ }
1007
+ hydrateSignaturesFromDiskIfNeeded(true);
1008
+ const keys = [
1009
+ buildSignatureCacheKey(aliasKey, sessionId),
1010
+ buildSignatureCacheKey(ANTIGRAVITY_GLOBAL_ALIAS_KEY, sessionId)
1011
+ ].filter((k) => typeof k === 'string' && k.trim().length > 0);
1012
+ keys.forEach((k) => sessionSignatures.delete(k));
1013
+ const latest = getLatestSignatureEntry(aliasKey);
1014
+ if (latest?.sessionId && latest.sessionId.trim() === sessionId) {
1015
+ setLatestSignatureEntry(aliasKey, null);
1016
+ }
1017
+ const globalLatest = getLatestSignatureEntry(ANTIGRAVITY_GLOBAL_ALIAS_KEY);
1018
+ if (globalLatest?.sessionId && globalLatest.sessionId.trim() === sessionId) {
1019
+ setLatestSignatureEntry(ANTIGRAVITY_GLOBAL_ALIAS_KEY, null);
1020
+ }
1021
+ // Release any pins so routing can rotate away from a broken tool loop.
1022
+ const pinnedAlias = pinnedAliasBySession.get(sessionId);
1023
+ if (pinnedAlias) {
1024
+ pinnedAliasBySession.delete(sessionId);
1025
+ if (typeof pinnedAlias.aliasKey === 'string' && pinnedAlias.aliasKey.trim()) {
1026
+ const backref = pinnedSessionByAlias.get(pinnedAlias.aliasKey);
1027
+ if (backref?.sessionId === sessionId) {
1028
+ pinnedSessionByAlias.delete(pinnedAlias.aliasKey);
1029
+ }
1030
+ }
1031
+ }
1032
+ const pinnedSession = pinnedSessionByAlias.get(aliasKey);
1033
+ if (pinnedSession?.sessionId === sessionId) {
1034
+ pinnedSessionByAlias.delete(aliasKey);
1035
+ }
1036
+ schedulePersistenceFlush();
1037
+ }
1038
+ export function resetAntigravitySessionSignatureCachesForTests() {
1039
+ sessionSignatures.clear();
1040
+ requestSessionIds.clear();
1041
+ latestSignaturesByAlias.clear();
1042
+ pinnedAliasBySession.clear();
1043
+ pinnedSessionByAlias.clear();
1044
+ rewindBlocks.clear();
1045
+ const persistence = getPersistenceState();
1046
+ if (persistence) {
1047
+ persistence.loadedOnce = false;
1048
+ persistence.loadedMtimeMs = null;
1049
+ }
238
1050
  }
239
1051
  export function shouldTreatAsMissingThoughtSignature(value) {
240
1052
  if (typeof value !== 'string') {
241
1053
  return true;
242
1054
  }
243
1055
  const trimmed = value.trim();
244
- return trimmed.length === 0 || trimmed === DUMMY_THOUGHT_SIGNATURE;
1056
+ return trimmed.length === 0 || trimmed === DUMMY_THOUGHT_SIGNATURE_SENTINEL;
245
1057
  }