@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.
- package/dist/conversion/codecs/gemini-openai-codec.js +6 -1
- package/dist/conversion/compat/actions/anthropic-claude-code-system-prompt.d.ts +4 -6
- package/dist/conversion/compat/actions/anthropic-claude-code-system-prompt.js +179 -41
- package/dist/conversion/compat/actions/antigravity-thought-signature-cache.js +73 -14
- package/dist/conversion/compat/actions/antigravity-thought-signature-prepare.js +165 -10
- package/dist/conversion/compat/actions/gemini-cli-request.js +72 -13
- package/dist/conversion/compat/antigravity-session-signature.d.ts +68 -1
- package/dist/conversion/compat/antigravity-session-signature.js +833 -21
- package/dist/conversion/compat/profiles/anthropic-claude-code.json +17 -0
- package/dist/conversion/compat/profiles/chat-gemini-cli.json +1 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +33 -8
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +17 -1
- package/dist/conversion/hub/pipeline/compat/compat-profile-store.js +12 -3
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +1 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +24 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +20 -0
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +26 -1
- package/dist/conversion/hub/process/chat-process.js +300 -67
- package/dist/conversion/hub/response/provider-response.js +4 -3
- package/dist/conversion/shared/gemini-tool-utils.js +134 -9
- package/dist/conversion/shared/text-markup-normalizer.js +90 -1
- package/dist/conversion/shared/thought-signature-validator.d.ts +1 -1
- package/dist/conversion/shared/thought-signature-validator.js +2 -1
- package/dist/quota/apikey-reset.d.ts +17 -0
- package/dist/quota/apikey-reset.js +43 -0
- package/dist/quota/index.d.ts +2 -0
- package/dist/quota/index.js +1 -0
- package/dist/quota/quota-manager.d.ts +44 -0
- package/dist/quota/quota-manager.js +491 -0
- package/dist/quota/quota-state.d.ts +6 -0
- package/dist/quota/quota-state.js +167 -0
- package/dist/quota/types.d.ts +61 -0
- package/dist/quota/types.js +1 -0
- package/dist/router/virtual-router/bootstrap.js +103 -6
- package/dist/router/virtual-router/engine-health.js +104 -0
- package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +18 -0
- package/dist/router/virtual-router/engine-selection/tier-priority.d.ts +1 -2
- package/dist/router/virtual-router/engine-selection/tier-priority.js +2 -2
- package/dist/router/virtual-router/engine-selection/tier-selection-select.js +34 -10
- package/dist/router/virtual-router/engine-selection/tier-selection.js +250 -6
- package/dist/router/virtual-router/engine-selection.js +2 -2
- package/dist/router/virtual-router/engine.d.ts +16 -1
- package/dist/router/virtual-router/engine.js +320 -42
- package/dist/router/virtual-router/features.js +20 -2
- package/dist/router/virtual-router/success-center.d.ts +10 -0
- package/dist/router/virtual-router/success-center.js +32 -0
- package/dist/router/virtual-router/types.d.ts +48 -0
- package/dist/servertool/clock/config.d.ts +2 -0
- package/dist/servertool/clock/config.js +10 -2
- package/dist/servertool/clock/daemon.js +3 -0
- package/dist/servertool/clock/ntp.d.ts +18 -0
- package/dist/servertool/clock/ntp.js +318 -0
- package/dist/servertool/clock/paths.d.ts +1 -0
- package/dist/servertool/clock/paths.js +3 -0
- package/dist/servertool/clock/state.d.ts +2 -0
- package/dist/servertool/clock/state.js +15 -2
- package/dist/servertool/clock/tasks.d.ts +1 -0
- package/dist/servertool/clock/tasks.js +24 -1
- package/dist/servertool/clock/types.d.ts +21 -0
- package/dist/servertool/engine.js +105 -1
- package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.d.ts +1 -0
- package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.js +201 -0
- package/dist/servertool/handlers/clock-auto.js +39 -4
- package/dist/servertool/handlers/clock.js +145 -16
- package/dist/servertool/handlers/followup-request-builder.js +84 -0
- package/dist/servertool/handlers/stop-message-auto.js +1 -1
- package/dist/servertool/server-side-tools.d.ts +1 -0
- package/dist/servertool/server-side-tools.js +1 -0
- package/dist/servertool/types.d.ts +2 -0
- package/dist/tools/apply-patch/execution-capturer.js +24 -3
- 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();
|
|
@@ -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
|
|
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 {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|