@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
@@ -0,0 +1,18 @@
1
+ import type { ClockConfigSnapshot, ClockNtpState } from './types.js';
2
+ export declare function resolveServerTimezone(): string;
3
+ export declare function formatLocalTime(ms: number): string;
4
+ export declare function buildTimeTagLine(snapshot: ClockTimeSnapshot): string;
5
+ export type ClockTimeSnapshot = {
6
+ active: boolean;
7
+ nowMs: number;
8
+ utc: string;
9
+ local: string;
10
+ timezone: string;
11
+ ntp: ClockNtpState;
12
+ };
13
+ export declare function syncClockWithNtpOnce(): Promise<void>;
14
+ export declare function startClockNtpSyncIfNeeded(_config?: ClockConfigSnapshot): Promise<void>;
15
+ export declare function getClockNtpState(): Promise<ClockNtpState>;
16
+ export declare function getClockTimeSnapshot(): Promise<ClockTimeSnapshot>;
17
+ export declare function getCurrentClockOffsetMs(): number;
18
+ export declare function buildStableToolCallId(prefix?: string): string;
@@ -0,0 +1,318 @@
1
+ import dgram from 'node:dgram';
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { ensureDir, readSessionDirEnv, resolveClockNtpStateFile } from './paths.js';
6
+ import { readJsonFile, writeJsonFileAtomic } from './io.js';
7
+ import { getClockOffsetMs, setClockOffsetMs, nowMs as correctedNowMs } from './state.js';
8
+ const DEFAULT_NTP_SERVERS = ['time.google.com', 'time.cloudflare.com', 'pool.ntp.org'];
9
+ function isNtpDisabledByEnv() {
10
+ const raw = String(process.env.ROUTECODEX_CLOCK_NTP || '').trim().toLowerCase();
11
+ if (!raw)
12
+ return false;
13
+ return raw === '0' || raw === 'false' || raw === 'off' || raw === 'disable' || raw === 'disabled';
14
+ }
15
+ function clampNumber(value, min, max) {
16
+ if (!Number.isFinite(value))
17
+ return min;
18
+ return Math.max(min, Math.min(max, value));
19
+ }
20
+ function safeErrorMessage(err) {
21
+ try {
22
+ if (err instanceof Error)
23
+ return err.message || err.name;
24
+ return String(err ?? 'unknown');
25
+ }
26
+ catch {
27
+ return 'unknown';
28
+ }
29
+ }
30
+ function pad2(n) {
31
+ return n < 10 ? `0${n}` : String(n);
32
+ }
33
+ function pad3(n) {
34
+ if (n < 10)
35
+ return `00${n}`;
36
+ if (n < 100)
37
+ return `0${n}`;
38
+ return String(n);
39
+ }
40
+ function formatOffset(minutesEast) {
41
+ const sign = minutesEast >= 0 ? '+' : '-';
42
+ const abs = Math.abs(minutesEast);
43
+ const hh = Math.floor(abs / 60);
44
+ const mm = abs % 60;
45
+ return `${sign}${pad2(hh)}:${pad2(mm)}`;
46
+ }
47
+ export function resolveServerTimezone() {
48
+ try {
49
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
50
+ return typeof tz === 'string' && tz.trim().length ? tz.trim() : 'unknown';
51
+ }
52
+ catch {
53
+ return 'unknown';
54
+ }
55
+ }
56
+ export function formatLocalTime(ms) {
57
+ const d = new Date(ms);
58
+ const y = d.getFullYear();
59
+ const mo = pad2(d.getMonth() + 1);
60
+ const da = pad2(d.getDate());
61
+ const hh = pad2(d.getHours());
62
+ const mi = pad2(d.getMinutes());
63
+ const ss = pad2(d.getSeconds());
64
+ const mmm = pad3(d.getMilliseconds());
65
+ const minutesEast = -d.getTimezoneOffset();
66
+ return `${y}-${mo}-${da} ${hh}:${mi}:${ss}.${mmm} ${formatOffset(minutesEast)}`;
67
+ }
68
+ export function buildTimeTagLine(snapshot) {
69
+ // Markdown inline code blocks to reduce the chance of models "roleplaying" XML-like tags.
70
+ return `[Time/Date]: utc=\`${snapshot.utc}\` local=\`${snapshot.local}\` tz=\`${snapshot.timezone}\` nowMs=\`${snapshot.nowMs}\` ntpOffsetMs=\`${snapshot.ntp.offsetMs}\``;
71
+ }
72
+ const EMPTY_NTP_STATE = {
73
+ version: 1,
74
+ offsetMs: 0,
75
+ updatedAtMs: 0,
76
+ status: 'stale'
77
+ };
78
+ let loaded = false;
79
+ let state = { ...EMPTY_NTP_STATE };
80
+ let syncing = null;
81
+ function coerceNtpState(raw) {
82
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
83
+ return { ...EMPTY_NTP_STATE };
84
+ }
85
+ const r = raw;
86
+ const offsetMs = typeof r.offsetMs === 'number' && Number.isFinite(r.offsetMs) ? Math.floor(r.offsetMs) : 0;
87
+ const updatedAtMs = typeof r.updatedAtMs === 'number' && Number.isFinite(r.updatedAtMs) ? Math.floor(r.updatedAtMs) : 0;
88
+ const statusRaw = typeof r.status === 'string' ? r.status.trim() : '';
89
+ const status = statusRaw === 'synced' || statusRaw === 'stale' || statusRaw === 'error' || statusRaw === 'disabled'
90
+ ? statusRaw
91
+ : updatedAtMs > 0
92
+ ? 'stale'
93
+ : 'stale';
94
+ const source = typeof r.source === 'string' && r.source.trim().length ? r.source.trim() : undefined;
95
+ const rttMs = typeof r.rttMs === 'number' && Number.isFinite(r.rttMs) ? Math.max(0, Math.floor(r.rttMs)) : undefined;
96
+ const lastError = typeof r.lastError === 'string' && r.lastError.trim().length ? r.lastError.trim() : undefined;
97
+ return { version: 1, offsetMs, updatedAtMs, status, ...(source ? { source } : {}), ...(rttMs !== undefined ? { rttMs } : {}), ...(lastError ? { lastError } : {}) };
98
+ }
99
+ async function loadStateOnce() {
100
+ if (loaded)
101
+ return;
102
+ loaded = true;
103
+ if (isNtpDisabledByEnv()) {
104
+ state = { ...EMPTY_NTP_STATE, status: 'disabled' };
105
+ setClockOffsetMs(0);
106
+ return;
107
+ }
108
+ const sessionDir = readSessionDirEnv();
109
+ if (!sessionDir) {
110
+ state = { ...EMPTY_NTP_STATE, status: 'stale' };
111
+ setClockOffsetMs(0);
112
+ return;
113
+ }
114
+ const filePath = resolveClockNtpStateFile(sessionDir);
115
+ try {
116
+ const raw = await readJsonFile(filePath);
117
+ state = coerceNtpState(raw);
118
+ setClockOffsetMs(state.offsetMs);
119
+ }
120
+ catch {
121
+ // missing/unreadable file: keep defaults
122
+ state = { ...EMPTY_NTP_STATE, status: 'stale' };
123
+ setClockOffsetMs(0);
124
+ }
125
+ }
126
+ async function persistState(next) {
127
+ const sessionDir = readSessionDirEnv();
128
+ if (!sessionDir)
129
+ return;
130
+ const filePath = resolveClockNtpStateFile(sessionDir);
131
+ await ensureDir(path.dirname(filePath));
132
+ try {
133
+ await fs.chmod(path.dirname(filePath), 0o700);
134
+ }
135
+ catch {
136
+ // best-effort
137
+ }
138
+ await writeJsonFileAtomic(filePath, next);
139
+ }
140
+ const NTP_EPOCH_OFFSET_SECONDS = 2208988800; // 1900-01-01 to 1970-01-01
141
+ function msToNtpTimestamp(ms) {
142
+ const seconds = Math.floor(ms / 1000) + NTP_EPOCH_OFFSET_SECONDS;
143
+ const msRemainder = ms % 1000;
144
+ const fraction = Math.floor((msRemainder / 1000) * 2 ** 32);
145
+ return { seconds, fraction };
146
+ }
147
+ function ntpTimestampToMs(seconds, fraction) {
148
+ const unixSeconds = seconds - NTP_EPOCH_OFFSET_SECONDS;
149
+ const fracMs = Math.round((fraction / 2 ** 32) * 1000);
150
+ return unixSeconds * 1000 + fracMs;
151
+ }
152
+ async function querySntpOnce(server, timeoutMs) {
153
+ const socket = dgram.createSocket('udp4');
154
+ const req = Buffer.alloc(48);
155
+ req[0] = 0x23; // LI=0, VN=4, Mode=3 (client)
156
+ const t1SystemMs = Date.now();
157
+ const t1 = msToNtpTimestamp(t1SystemMs);
158
+ req.writeUInt32BE(t1.seconds >>> 0, 40);
159
+ req.writeUInt32BE(t1.fraction >>> 0, 44);
160
+ const res = await new Promise((resolve, reject) => {
161
+ const timer = setTimeout(() => {
162
+ reject(new Error('ntp timeout'));
163
+ }, timeoutMs);
164
+ socket.once('error', (err) => {
165
+ clearTimeout(timer);
166
+ reject(err);
167
+ });
168
+ socket.once('message', (msg) => {
169
+ clearTimeout(timer);
170
+ resolve(msg);
171
+ });
172
+ socket.send(req, 123, server, (err) => {
173
+ if (err) {
174
+ clearTimeout(timer);
175
+ reject(err);
176
+ }
177
+ });
178
+ }).finally(() => {
179
+ try {
180
+ socket.close();
181
+ }
182
+ catch {
183
+ // ignore
184
+ }
185
+ });
186
+ if (!Buffer.isBuffer(res) || res.length < 48) {
187
+ throw new Error('invalid ntp response');
188
+ }
189
+ const t4SystemMs = Date.now();
190
+ const t2Seconds = res.readUInt32BE(32);
191
+ const t2Fraction = res.readUInt32BE(36);
192
+ const t3Seconds = res.readUInt32BE(40);
193
+ const t3Fraction = res.readUInt32BE(44);
194
+ const t2Ms = ntpTimestampToMs(t2Seconds, t2Fraction);
195
+ const t3Ms = ntpTimestampToMs(t3Seconds, t3Fraction);
196
+ const offsetMs = ((t2Ms - t1SystemMs) + (t3Ms - t4SystemMs)) / 2;
197
+ const rttMs = (t4SystemMs - t1SystemMs) - (t3Ms - t2Ms);
198
+ return {
199
+ offsetMs: Math.floor(clampNumber(offsetMs, -24 * 60 * 60_000, 24 * 60 * 60_000)),
200
+ rttMs: Math.max(0, Math.floor(rttMs))
201
+ };
202
+ }
203
+ function resolveNtpServers() {
204
+ const raw = String(process.env.ROUTECODEX_CLOCK_NTP_SERVERS || '').trim();
205
+ if (!raw)
206
+ return [...DEFAULT_NTP_SERVERS];
207
+ const list = raw.split(',').map((s) => s.trim()).filter(Boolean);
208
+ return list.length ? list : [...DEFAULT_NTP_SERVERS];
209
+ }
210
+ export async function syncClockWithNtpOnce() {
211
+ await loadStateOnce();
212
+ if (isNtpDisabledByEnv()) {
213
+ state = { ...state, status: 'disabled', offsetMs: 0 };
214
+ setClockOffsetMs(0);
215
+ return;
216
+ }
217
+ const servers = resolveNtpServers();
218
+ const timeoutMs = (() => {
219
+ const raw = Number(process.env.ROUTECODEX_CLOCK_NTP_TIMEOUT_MS ?? 800);
220
+ return Number.isFinite(raw) ? Math.max(100, Math.floor(raw)) : 800;
221
+ })();
222
+ let lastErr;
223
+ for (const server of servers.slice(0, 5)) {
224
+ try {
225
+ const result = await querySntpOnce(server, timeoutMs);
226
+ const updatedAtMs = Date.now();
227
+ const next = {
228
+ version: 1,
229
+ offsetMs: result.offsetMs,
230
+ updatedAtMs,
231
+ source: server,
232
+ rttMs: result.rttMs,
233
+ status: 'synced'
234
+ };
235
+ state = next;
236
+ setClockOffsetMs(result.offsetMs);
237
+ await persistState(next);
238
+ return;
239
+ }
240
+ catch (err) {
241
+ lastErr = safeErrorMessage(err);
242
+ }
243
+ }
244
+ state = {
245
+ ...state,
246
+ status: 'error',
247
+ lastError: lastErr || 'ntp failed',
248
+ updatedAtMs: state.updatedAtMs || Date.now()
249
+ };
250
+ }
251
+ export async function startClockNtpSyncIfNeeded(_config) {
252
+ await loadStateOnce();
253
+ if (isNtpDisabledByEnv())
254
+ return;
255
+ if (syncing)
256
+ return syncing;
257
+ // Best-effort background sync; do not block the request pipeline.
258
+ syncing = (async () => {
259
+ try {
260
+ await syncClockWithNtpOnce();
261
+ }
262
+ catch {
263
+ // best-effort
264
+ }
265
+ finally {
266
+ syncing = null;
267
+ }
268
+ })();
269
+ return syncing;
270
+ }
271
+ export async function getClockNtpState() {
272
+ await loadStateOnce();
273
+ const now = Date.now();
274
+ const staleAfterMs = (() => {
275
+ const raw = Number(process.env.ROUTECODEX_CLOCK_NTP_STALE_AFTER_MS ?? 6 * 60 * 60_000);
276
+ return Number.isFinite(raw) ? Math.max(60_000, Math.floor(raw)) : 6 * 60 * 60_000;
277
+ })();
278
+ const age = state.updatedAtMs > 0 ? Math.max(0, now - state.updatedAtMs) : Number.POSITIVE_INFINITY;
279
+ if (state.status === 'synced' && age > staleAfterMs) {
280
+ return { ...state, status: 'stale' };
281
+ }
282
+ return { ...state };
283
+ }
284
+ export async function getClockTimeSnapshot() {
285
+ await loadStateOnce();
286
+ const now = correctedNowMs();
287
+ const d = new Date(now);
288
+ const utc = (() => {
289
+ try {
290
+ return d.toISOString();
291
+ }
292
+ catch {
293
+ return new Date(0).toISOString();
294
+ }
295
+ })();
296
+ const timezone = resolveServerTimezone();
297
+ const local = formatLocalTime(now);
298
+ const ntp = await getClockNtpState();
299
+ return {
300
+ active: true,
301
+ nowMs: now,
302
+ utc,
303
+ local,
304
+ timezone,
305
+ ntp
306
+ };
307
+ }
308
+ export function getCurrentClockOffsetMs() {
309
+ return getClockOffsetMs();
310
+ }
311
+ export function buildStableToolCallId(prefix = 'call_clock') {
312
+ try {
313
+ return `${prefix}_${crypto.randomUUID()}`;
314
+ }
315
+ catch {
316
+ return `${prefix}_${Date.now()}_${Math.random().toString(16).slice(2)}`;
317
+ }
318
+ }
@@ -1,4 +1,5 @@
1
1
  export declare function readSessionDirEnv(): string;
2
2
  export declare function resolveClockDir(sessionDir: string): string;
3
+ export declare function resolveClockNtpStateFile(sessionDir: string): string;
3
4
  export declare function resolveClockStateFile(sessionDir: string, sessionId: string): string | null;
4
5
  export declare function ensureDir(dir: string): Promise<void>;
@@ -13,6 +13,9 @@ function sanitizeSegment(value) {
13
13
  export function resolveClockDir(sessionDir) {
14
14
  return path.join(sessionDir, 'clock');
15
15
  }
16
+ export function resolveClockNtpStateFile(sessionDir) {
17
+ return path.join(resolveClockDir(sessionDir), 'ntp-state.json');
18
+ }
16
19
  export function resolveClockStateFile(sessionDir, sessionId) {
17
20
  const safe = sanitizeSegment(sessionId);
18
21
  if (!safe) {
@@ -1,4 +1,6 @@
1
1
  import type { ClockConfigSnapshot, ClockSessionState, ClockTask } from './types.js';
2
+ export declare function setClockOffsetMs(value: number): void;
3
+ export declare function getClockOffsetMs(): number;
2
4
  export declare function nowMs(): number;
3
5
  export declare function buildEmptyState(sessionId: string): ClockSessionState;
4
6
  export declare function coerceState(raw: unknown, sessionId: string): ClockSessionState;
@@ -1,5 +1,14 @@
1
+ let clockOffsetMs = 0;
2
+ export function setClockOffsetMs(value) {
3
+ if (typeof value !== 'number' || !Number.isFinite(value))
4
+ return;
5
+ clockOffsetMs = Math.max(-24 * 60 * 60_000, Math.min(24 * 60 * 60_000, Math.floor(value)));
6
+ }
7
+ export function getClockOffsetMs() {
8
+ return clockOffsetMs;
9
+ }
1
10
  export function nowMs() {
2
- return Date.now();
11
+ return Date.now() + clockOffsetMs;
3
12
  }
4
13
  export function buildEmptyState(sessionId) {
5
14
  const t = nowMs();
@@ -29,6 +38,9 @@ export function coerceState(raw, sessionId) {
29
38
  ? e.arguments
30
39
  : undefined;
31
40
  const deliveredAtMs = typeof e.deliveredAtMs === 'number' && Number.isFinite(e.deliveredAtMs) ? Math.floor(e.deliveredAtMs) : undefined;
41
+ const notBeforeRequestId = typeof e.notBeforeRequestId === 'string' && e.notBeforeRequestId.trim().length
42
+ ? e.notBeforeRequestId.trim()
43
+ : undefined;
32
44
  const deliveryCount = typeof e.deliveryCount === 'number' && Number.isFinite(e.deliveryCount) ? Math.max(0, Math.floor(e.deliveryCount)) : 0;
33
45
  tasks.push({
34
46
  taskId,
@@ -40,7 +52,8 @@ export function coerceState(raw, sessionId) {
40
52
  ...(tool ? { tool } : {}),
41
53
  ...(args ? { arguments: args } : {}),
42
54
  ...(deliveredAtMs !== undefined ? { deliveredAtMs } : {}),
43
- deliveryCount
55
+ deliveryCount,
56
+ ...(notBeforeRequestId ? { notBeforeRequestId } : {})
44
57
  });
45
58
  }
46
59
  const updatedAtMs = typeof record.updatedAtMs === 'number' && Number.isFinite(record.updatedAtMs) ? Math.floor(record.updatedAtMs) : nowMs();
@@ -10,6 +10,7 @@ export declare function reserveDueTasksForRequest(args: {
10
10
  reservationId: string;
11
11
  sessionId: string;
12
12
  config: ClockConfigSnapshot;
13
+ requestId?: string;
13
14
  }): Promise<{
14
15
  reservation: ClockReservation | null;
15
16
  injectText?: string;
@@ -71,6 +71,7 @@ export async function scheduleClockTasks(sessionId, items, config) {
71
71
  task: text,
72
72
  ...(item.tool ? { tool: item.tool } : {}),
73
73
  ...(item.arguments ? { arguments: item.arguments } : {}),
74
+ ...(item.notBeforeRequestId ? { notBeforeRequestId: item.notBeforeRequestId } : {}),
74
75
  deliveryCount: 0
75
76
  });
76
77
  }
@@ -125,6 +126,10 @@ export function selectDueUndeliveredTasks(tasks, config, atMs) {
125
126
  continue;
126
127
  if (task.deliveredAtMs !== undefined)
127
128
  continue;
129
+ if (typeof task.notBeforeRequestId === 'string' && task.notBeforeRequestId.trim().length) {
130
+ // notBeforeRequestId is evaluated by reserveDueTasksForRequest, which passes requestId.
131
+ // Keep legacy callers working by ignoring this guard here (default behavior).
132
+ }
128
133
  if (!Number.isFinite(task.dueAtMs))
129
134
  continue;
130
135
  if (atMs < task.dueAtMs - config.dueWindowMs)
@@ -157,7 +162,25 @@ export function findNextUndeliveredDueAtMs(tasks, atMs) {
157
162
  export async function reserveDueTasksForRequest(args) {
158
163
  const state = await loadClockSessionState(args.sessionId, args.config);
159
164
  const at = nowMs();
160
- const due = selectDueUndeliveredTasks(state.tasks, args.config, at);
165
+ const dueAll = selectDueUndeliveredTasks(state.tasks, args.config, at);
166
+ const requestId = typeof args.requestId === 'string' && args.requestId.trim().length ? args.requestId.trim() : '';
167
+ const isSameRequestChain = (guardedRequestId, currentRequestId) => {
168
+ const guarded = guardedRequestId.trim();
169
+ if (!guarded)
170
+ return false;
171
+ return currentRequestId === guarded || currentRequestId.startsWith(`${guarded}:`);
172
+ };
173
+ const due = requestId
174
+ ? dueAll.filter((t) => {
175
+ const guarded = typeof t.notBeforeRequestId === 'string' ? String(t.notBeforeRequestId).trim() : '';
176
+ if (!guarded) {
177
+ return true;
178
+ }
179
+ // Avoid same-request triggers, including internal followups that suffix the requestId
180
+ // (e.g. ":clock_followup"), to prevent request-local dead loops.
181
+ return !isSameRequestChain(guarded, requestId);
182
+ })
183
+ : dueAll;
161
184
  if (!due.length) {
162
185
  return { reservation: null };
163
186
  }
@@ -9,6 +9,7 @@ export type ClockTask = {
9
9
  arguments?: Record<string, unknown>;
10
10
  deliveredAtMs?: number;
11
11
  deliveryCount: number;
12
+ notBeforeRequestId?: string;
12
13
  };
13
14
  export type ClockSessionState = {
14
15
  version: 1;
@@ -27,10 +28,30 @@ export type ClockConfigSnapshot = {
27
28
  retentionMs: number;
28
29
  dueWindowMs: number;
29
30
  tickMs: number;
31
+ /**
32
+ * Whether clock_hold_flow is allowed for non-streaming (JSON) clients.
33
+ * Default: true (best-effort long-poll hold; clients can abort the connection).
34
+ */
35
+ holdNonStreaming: boolean;
36
+ /**
37
+ * Maximum time (ms) a single request is allowed to hold before followup.
38
+ * Default: 60s. Larger values increase resource usage and risk client/proxy timeouts.
39
+ */
40
+ holdMaxMs: number;
30
41
  };
31
42
  export type ClockScheduleItem = {
32
43
  dueAtMs: number;
33
44
  task: string;
34
45
  tool?: string;
35
46
  arguments?: Record<string, unknown>;
47
+ notBeforeRequestId?: string;
48
+ };
49
+ export type ClockNtpState = {
50
+ version: 1;
51
+ offsetMs: number;
52
+ updatedAtMs: number;
53
+ source?: string;
54
+ rttMs?: number;
55
+ status: 'synced' | 'stale' | 'error' | 'disabled';
56
+ lastError?: string;
36
57
  };
@@ -5,7 +5,7 @@ import { createHash } from 'node:crypto';
5
5
  import { loadRoutingInstructionStateSync, saveRoutingInstructionStateSync } from '../router/virtual-router/sticky-session-store.js';
6
6
  import { deserializeRoutingInstructionState, serializeRoutingInstructionState } from '../router/virtual-router/routing-instructions.js';
7
7
  import { applyHubFollowupPolicyShadow } from './followup-shadow.js';
8
- import { buildServerToolFollowupChatPayloadFromInjection } from './handlers/followup-request-builder.js';
8
+ import { buildServerToolFollowupChatPayloadFromInjection, extractCapturedChatSeed } from './handlers/followup-request-builder.js';
9
9
  import { findNextUndeliveredDueAtMs, listClockTasks, resolveClockConfig } from './clock/task-store.js';
10
10
  import { savePendingServerToolInjection } from './pending-session.js';
11
11
  function parseTimeoutMs(raw, fallback) {
@@ -277,12 +277,32 @@ export async function runServerToolOrchestration(options) {
277
277
  timeoutMs: effectiveServerToolTimeoutMs || serverToolTimeoutMs
278
278
  }));
279
279
  if (engineResult.mode === 'passthrough' || !engineResult.execution) {
280
+ try {
281
+ options.stageRecorder?.record('servertool.match', {
282
+ matched: false,
283
+ mode: engineResult.mode,
284
+ reason: engineResult.mode === 'passthrough' ? 'passthrough' : 'no_execution'
285
+ });
286
+ }
287
+ catch {
288
+ // best-effort only
289
+ }
280
290
  return {
281
291
  chat: engineResult.finalChatResponse,
282
292
  executed: false
283
293
  };
284
294
  }
285
295
  const flowId = engineResult.execution.flowId ?? 'unknown';
296
+ try {
297
+ options.stageRecorder?.record('servertool.match', {
298
+ matched: true,
299
+ flowId,
300
+ hasFollowup: Boolean(engineResult.execution.followup)
301
+ });
302
+ }
303
+ catch {
304
+ // best-effort only
305
+ }
286
306
  const totalSteps = 5;
287
307
  logProgress(1, totalSteps, 'matched', { flowId });
288
308
  // Mixed tools: persist servertool outputs for next request, but return remaining tool_calls to client.
@@ -513,6 +533,90 @@ export async function runServerToolOrchestration(options) {
513
533
  wrapped.cause = lastError;
514
534
  throw wrapped;
515
535
  }
536
+ // Special case: Antigravity thoughtSignature bootstrap flow.
537
+ // - First followup performs a minimal preflight (forces clock.get) to obtain a fresh signature.
538
+ // - If preflight succeeds, immediately replay the original captured request as a second internal hop,
539
+ // so the client sees a single recovered response (transparent).
540
+ if (engineResult.execution.flowId === 'antigravity_thought_signature_bootstrap' && options.reenterPipeline) {
541
+ const preflight = followupBody;
542
+ const preflightError = preflight && typeof preflight.error === 'object' ? preflight.error : null;
543
+ const preflightStatus = (() => {
544
+ if (!preflightError || typeof preflightError !== 'object' || Array.isArray(preflightError))
545
+ return undefined;
546
+ const statusRaw = preflightError.status ?? preflightError.statusCode;
547
+ if (typeof statusRaw === 'number' && Number.isFinite(statusRaw))
548
+ return Math.floor(statusRaw);
549
+ const codeRaw = preflightError.code;
550
+ const code = typeof codeRaw === 'string' ? codeRaw.trim() : typeof codeRaw === 'number' ? String(codeRaw) : '';
551
+ if (code && /^HTTP_\d{3}$/i.test(code))
552
+ return Number(code.split('_')[1]);
553
+ if (code && /^\d{3}$/.test(code))
554
+ return Number(code);
555
+ return undefined;
556
+ })();
557
+ // One-shot guard: if preflight still looks rate-limited / invalid, stop and return the original error.
558
+ if (preflightError && (preflightStatus === 429 || preflightStatus === 400)) {
559
+ const decorated = decorateFinalChatWithServerToolContext(engineResult.finalChatResponse, engineResult.execution);
560
+ logProgress(5, totalSteps, 'completed (bootstrap preflight failed)', { flowId });
561
+ return { chat: decorated, executed: true, flowId: engineResult.execution.flowId };
562
+ }
563
+ const replaySeed = extractCapturedChatSeed(options.adapterContext?.capturedChatRequest);
564
+ if (replaySeed) {
565
+ const replayPayload = {
566
+ ...(replaySeed.model ? { model: replaySeed.model } : {}),
567
+ messages: Array.isArray(replaySeed.messages) ? replaySeed.messages : [],
568
+ ...(Array.isArray(replaySeed.tools) ? { tools: replaySeed.tools } : {}),
569
+ ...(replaySeed.parameters && typeof replaySeed.parameters === 'object' && !Array.isArray(replaySeed.parameters)
570
+ ? { parameters: replaySeed.parameters }
571
+ : {})
572
+ };
573
+ const replayLoopState = buildServerToolLoopState(options.adapterContext, engineResult.execution.flowId, replayPayload);
574
+ const replayMetadata = { stream: false };
575
+ const replayRt = ensureRuntimeMetadata(replayMetadata);
576
+ replayRt.serverToolFollowup = true;
577
+ if (replayLoopState) {
578
+ replayRt.serverToolLoopState = replayLoopState;
579
+ }
580
+ replayMetadata.__hubEntry = 'chat_process';
581
+ replayMetadata.routeHint = '';
582
+ replayRt.preserveRouteHint = false;
583
+ replayRt.disableStickyRoutes = true;
584
+ replayRt.serverToolOriginalEntryEndpoint =
585
+ (typeof options.entryEndpoint === 'string' && options.entryEndpoint.trim().length
586
+ ? options.entryEndpoint
587
+ : followupEntryEndpoint);
588
+ const forcedProviderKeyRaw = options.adapterContext?.providerKey;
589
+ const forcedProviderKey = typeof forcedProviderKeyRaw === 'string' && forcedProviderKeyRaw.trim().length ? forcedProviderKeyRaw.trim() : '';
590
+ if (forcedProviderKey) {
591
+ replayMetadata.__shadowCompareForcedProviderKey = forcedProviderKey;
592
+ }
593
+ const replayRequestId = buildFollowupRequestId(options.requestId, ':antigravity_ts_replay');
594
+ const replayPayloadFinal = applyHubFollowupPolicyShadow({
595
+ requestId: replayRequestId,
596
+ entryEndpoint: followupEntryEndpoint,
597
+ flowId: engineResult.execution.flowId,
598
+ payload: coerceFollowupPayloadStream(replayPayload, false),
599
+ stageRecorder: options.stageRecorder
600
+ });
601
+ const replayResult = await withTimeout(options.reenterPipeline({
602
+ entryEndpoint: followupEntryEndpoint,
603
+ requestId: replayRequestId,
604
+ body: replayPayloadFinal,
605
+ metadata: replayMetadata
606
+ }), followupTimeoutMs, () => createServerToolTimeoutError({
607
+ requestId: options.requestId,
608
+ phase: 'followup',
609
+ timeoutMs: followupTimeoutMs,
610
+ flowId: engineResult.execution.flowId
611
+ }));
612
+ const replayBody = replayResult && replayResult.body && typeof replayResult.body === 'object'
613
+ ? replayResult.body
614
+ : undefined;
615
+ const decorated = decorateFinalChatWithServerToolContext(replayBody ?? preflight ?? engineResult.finalChatResponse, engineResult.execution);
616
+ logProgress(5, totalSteps, 'completed (bootstrap replay)', { flowId });
617
+ return { chat: decorated, executed: true, flowId: engineResult.execution.flowId };
618
+ }
619
+ }
516
620
  const decorated = decorateFinalChatWithServerToolContext(followupBody ?? engineResult.finalChatResponse, engineResult.execution);
517
621
  logProgress(5, totalSteps, 'completed', { flowId });
518
622
  return {