@jsonstudio/llms 0.6.1462 → 0.6.1733
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 -7
- package/dist/conversion/compat/actions/anthropic-claude-code-system-prompt.js +140 -21
- package/dist/conversion/compat/actions/antigravity-thought-signature-cache.js +68 -10
- package/dist/conversion/compat/actions/antigravity-thought-signature-prepare.js +151 -23
- package/dist/conversion/compat/actions/gemini-cli-request.js +72 -13
- package/dist/conversion/compat/actions/harvest-tool-calls-from-text.d.ts +10 -0
- package/dist/conversion/compat/actions/harvest-tool-calls-from-text.js +121 -0
- package/dist/conversion/compat/actions/iflow-kimi-cli-defaults.d.ts +10 -0
- package/dist/conversion/compat/actions/iflow-kimi-cli-defaults.js +80 -0
- package/dist/conversion/compat/actions/iflow-kimi-history-media-placeholder.d.ts +7 -0
- package/dist/conversion/compat/actions/iflow-kimi-history-media-placeholder.js +161 -0
- package/dist/conversion/compat/actions/iflow-kimi-thinking-reasoning-fill.d.ts +12 -0
- package/dist/conversion/compat/actions/iflow-kimi-thinking-reasoning-fill.js +67 -0
- package/dist/conversion/compat/actions/iflow-response-body-unwrap.d.ts +9 -0
- package/dist/conversion/compat/actions/iflow-response-body-unwrap.js +140 -0
- package/dist/conversion/compat/actions/lmstudio-responses-fc-ids.d.ts +10 -0
- package/dist/conversion/compat/actions/lmstudio-responses-fc-ids.js +59 -0
- package/dist/conversion/compat/actions/lmstudio-responses-input-stringify.d.ts +14 -0
- package/dist/conversion/compat/actions/lmstudio-responses-input-stringify.js +125 -0
- package/dist/conversion/compat/actions/normalize-tool-call-ids.d.ts +11 -0
- package/dist/conversion/compat/actions/normalize-tool-call-ids.js +140 -0
- package/dist/conversion/compat/actions/strip-orphan-function-calls-tag.d.ts +2 -0
- package/dist/conversion/compat/actions/strip-orphan-function-calls-tag.js +152 -0
- package/dist/conversion/compat/antigravity-session-signature.d.ts +57 -3
- package/dist/conversion/compat/antigravity-session-signature.js +821 -27
- package/dist/conversion/compat/profiles/anthropic-claude-code.json +0 -9
- package/dist/conversion/compat/profiles/chat-gemini-cli.json +1 -0
- package/dist/conversion/compat/profiles/chat-iflow.json +6 -0
- package/dist/conversion/compat/profiles/chat-lmstudio.json +7 -1
- package/dist/conversion/hub/operation-table/operation-table-runner.js +1 -1
- package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +52 -10
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +102 -6
- package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.d.ts +2 -0
- package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.js +63 -0
- package/dist/conversion/hub/pipeline/compat/compat-profile-store.js +12 -3
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +18 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +1 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +25 -1
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +20 -0
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +8 -5
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js +5 -1
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +113 -0
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +26 -1
- package/dist/conversion/hub/pipeline/target-utils.js +3 -0
- package/dist/conversion/hub/process/chat-process.js +300 -67
- package/dist/conversion/hub/response/provider-response.js +31 -4
- package/dist/conversion/responses/responses-openai-bridge.js +32 -6
- package/dist/conversion/shared/anthropic-message-utils.js +20 -5
- package/dist/conversion/shared/bridge-id-utils.d.ts +2 -0
- package/dist/conversion/shared/bridge-id-utils.js +52 -15
- package/dist/conversion/shared/gemini-tool-utils.js +134 -9
- package/dist/conversion/shared/responses-conversation-store.js +40 -5
- package/dist/conversion/shared/responses-output-builder.js +23 -7
- package/dist/conversion/shared/responses-tool-utils.d.ts +1 -0
- package/dist/conversion/shared/responses-tool-utils.js +30 -13
- package/dist/conversion/shared/text-markup-normalizer.d.ts +1 -0
- package/dist/conversion/shared/text-markup-normalizer.js +359 -2
- 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 +134 -13
- package/dist/router/virtual-router/classifier.js +1 -1
- package/dist/router/virtual-router/engine/antigravity/alias-lease.d.ts +33 -0
- package/dist/router/virtual-router/engine/antigravity/alias-lease.js +247 -0
- package/dist/router/virtual-router/engine/health/index.d.ts +23 -0
- package/dist/router/virtual-router/engine/health/index.js +720 -0
- package/dist/router/virtual-router/engine/provider-key/parse.d.ts +6 -0
- package/dist/router/virtual-router/engine/provider-key/parse.js +43 -0
- package/dist/router/virtual-router/engine/routing-pools/index.d.ts +13 -0
- package/dist/router/virtual-router/engine/routing-pools/index.js +225 -0
- package/dist/router/virtual-router/engine/routing-state/keys.d.ts +3 -0
- package/dist/router/virtual-router/engine/routing-state/keys.js +30 -0
- package/dist/router/virtual-router/engine/routing-state/metadata.d.ts +6 -0
- package/dist/router/virtual-router/engine/routing-state/metadata.js +132 -0
- package/dist/router/virtual-router/engine/routing-state/store.d.ts +11 -0
- package/dist/router/virtual-router/engine/routing-state/store.js +107 -0
- package/dist/router/virtual-router/engine-health.d.ts +1 -23
- package/dist/router/virtual-router/engine-health.js +1 -616
- package/dist/router/virtual-router/engine-selection/route-utils.js +57 -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 +39 -55
- package/dist/router/virtual-router/engine-selection/tier-selection.js +284 -23
- package/dist/router/virtual-router/engine-selection.d.ts +1 -13
- package/dist/router/virtual-router/engine-selection.js +1 -225
- package/dist/router/virtual-router/engine.d.ts +8 -14
- package/dist/router/virtual-router/engine.js +187 -382
- package/dist/router/virtual-router/features.js +20 -2
- package/dist/router/virtual-router/message-utils.js +15 -5
- 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 +109 -5
- 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/gemini-empty-reply-continue.js +48 -47
- package/dist/servertool/handlers/stop-message-auto.js +3 -3
- package/dist/servertool/handlers/vision.js +10 -0
- 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/sse/sse-to-json/builders/response-builder.js +6 -0
- package/dist/sse/sse-to-json/chat-sse-to-json-converter.js +32 -2
- package/dist/sse/sse-to-json/parsers/sse-parser.js +34 -0
- package/dist/sse/sse-to-json/responses-sse-to-json-converter.d.ts +1 -0
- package/dist/sse/sse-to-json/responses-sse-to-json-converter.js +33 -1
- package/dist/tools/apply-patch/args-normalizer/default-actions.d.ts +2 -0
- package/dist/tools/apply-patch/args-normalizer/default-actions.js +12 -0
- package/dist/tools/apply-patch/args-normalizer/extract-patch.d.ts +2 -0
- package/dist/tools/apply-patch/args-normalizer/extract-patch.js +15 -0
- package/dist/tools/apply-patch/args-normalizer/index.d.ts +2 -0
- package/dist/tools/apply-patch/args-normalizer/index.js +164 -0
- package/dist/tools/apply-patch/args-normalizer/structured-builders.d.ts +7 -0
- package/dist/tools/apply-patch/args-normalizer/structured-builders.js +85 -0
- package/dist/tools/apply-patch/args-normalizer/types.d.ts +54 -0
- package/dist/tools/apply-patch/args-normalizer/types.js +1 -0
- package/dist/tools/apply-patch/execution-capturer.js +24 -3
- package/dist/tools/apply-patch/patch-text/looks-like-patch.js +1 -0
- package/dist/tools/apply-patch/patch-text/normalize.js +104 -5
- package/dist/tools/apply-patch/structured/coercion.js +28 -4
- package/dist/tools/apply-patch/validator.js +7 -146
- package/package.json +3 -2
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
import { detectExtendedThinkingKeyword, detectImageAttachment, detectKeyword, extractMessageText, getLatestMessageRole, getLatestUserMessage } from './message-utils.js';
|
|
2
|
+
import { extractAntigravityGeminiSessionId } from '../../conversion/compat/antigravity-session-signature.js';
|
|
2
3
|
import { detectCodingTool, detectLastAssistantToolCategory, detectVisionTool, detectWebTool, extractMeaningfulDeclaredToolNames } from './tool-signals.js';
|
|
3
4
|
import { computeRequestTokens } from './token-estimator.js';
|
|
4
5
|
const THINKING_KEYWORDS = ['let me think', 'chain of thought', 'cot', 'reason step', 'deliberate'];
|
|
5
6
|
export function buildRoutingFeatures(request, metadata) {
|
|
7
|
+
const antigravitySessionId = (() => {
|
|
8
|
+
try {
|
|
9
|
+
const messages = Array.isArray(request.messages) ? request.messages : [];
|
|
10
|
+
const contents = messages.map((msg) => {
|
|
11
|
+
const role = msg?.role === 'user' ? 'user' : 'assistant';
|
|
12
|
+
const text = msg ? extractMessageText(msg) : '';
|
|
13
|
+
return { role, parts: [{ text }] };
|
|
14
|
+
});
|
|
15
|
+
return extractAntigravityGeminiSessionId({ contents });
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
})();
|
|
6
21
|
const latestUserMessage = getLatestUserMessage(request.messages);
|
|
7
22
|
const latestMessageRole = getLatestMessageRole(request.messages);
|
|
8
23
|
const assistantMessages = request.messages.filter((msg) => msg.role === 'assistant');
|
|
@@ -14,7 +29,9 @@ export function buildRoutingFeatures(request, metadata) {
|
|
|
14
29
|
const estimatedTokens = computeRequestTokens(request, latestUserText);
|
|
15
30
|
const hasThinking = detectKeyword(normalizedUserText, THINKING_KEYWORDS);
|
|
16
31
|
const hasVisionTool = detectVisionTool(request);
|
|
17
|
-
|
|
32
|
+
// Vision routing must only trigger for the current user turn (latest message),
|
|
33
|
+
// not for historical user messages carrying images during tool/assistant followups.
|
|
34
|
+
const hasImageAttachment = latestMessageRole === 'user' && detectImageAttachment(latestUserMessage);
|
|
18
35
|
const hasCodingTool = detectCodingTool(request);
|
|
19
36
|
const hasWebTool = detectWebTool(request);
|
|
20
37
|
const hasThinkingKeyword = hasThinking || detectExtendedThinkingKeyword(normalizedUserText);
|
|
@@ -50,7 +67,8 @@ export function buildRoutingFeatures(request, metadata) {
|
|
|
50
67
|
lastAssistantToolLabel,
|
|
51
68
|
latestMessageFromUser: latestMessageRole === 'user',
|
|
52
69
|
metadata: {
|
|
53
|
-
...metadata
|
|
70
|
+
...metadata,
|
|
71
|
+
...(antigravitySessionId ? { antigravitySessionId } : {})
|
|
54
72
|
}
|
|
55
73
|
};
|
|
56
74
|
}
|
|
@@ -59,8 +59,8 @@ export function detectExtendedThinkingKeyword(text) {
|
|
|
59
59
|
export function detectImageAttachment(message) {
|
|
60
60
|
if (!message)
|
|
61
61
|
return false;
|
|
62
|
-
// 仅基于标准 Chat
|
|
63
|
-
// - content 为数组时查找 { type: 'image' | 'image_url' | 'input_image', ... } 块;
|
|
62
|
+
// 仅基于标准 Chat 语义判断是否携带视觉媒体(图片/视频):
|
|
63
|
+
// - content 为数组时查找 { type: 'image' | 'image_url' | 'input_image' | 'video' | 'video_url' | 'input_video', ... } 块;
|
|
64
64
|
// - 不再依赖 metadata.attachments,也不再用纯文本关键字或剪贴板标记作为信号。
|
|
65
65
|
if (Array.isArray(message.content)) {
|
|
66
66
|
for (const part of message.content) {
|
|
@@ -69,19 +69,29 @@ export function detectImageAttachment(message) {
|
|
|
69
69
|
}
|
|
70
70
|
const record = part;
|
|
71
71
|
const typeValue = typeof record.type === 'string' ? record.type.toLowerCase() : '';
|
|
72
|
-
// For chat/standardized content,
|
|
72
|
+
// For chat/standardized content, media may appear as:
|
|
73
73
|
// - { type: "image_url", image_url: { url } }
|
|
74
74
|
// - { type: "image", uri: "...", data: "...", url: "..." }
|
|
75
75
|
// - { type: "input_image", image_url: "data:..." }
|
|
76
|
-
//
|
|
76
|
+
// - { type: "video_url", video_url: { url } }
|
|
77
|
+
// - { type: "video", uri: "...", data: "...", url: "..." }
|
|
78
|
+
// - { type: "input_video", video_url: "data:..." }
|
|
79
|
+
// Treat any non-empty URL/URI/data on a media-* block as a signal.
|
|
77
80
|
let imageCandidate = '';
|
|
78
81
|
if (typeof record.image_url === 'string') {
|
|
79
82
|
imageCandidate = record.image_url ?? '';
|
|
80
83
|
}
|
|
84
|
+
else if (typeof record.video_url === 'string') {
|
|
85
|
+
imageCandidate = record.video_url ?? '';
|
|
86
|
+
}
|
|
81
87
|
else if (record.image_url &&
|
|
82
88
|
typeof record.image_url?.url === 'string') {
|
|
83
89
|
imageCandidate = record.image_url?.url ?? '';
|
|
84
90
|
}
|
|
91
|
+
else if (record.video_url &&
|
|
92
|
+
typeof record.video_url?.url === 'string') {
|
|
93
|
+
imageCandidate = record.video_url?.url ?? '';
|
|
94
|
+
}
|
|
85
95
|
else if (typeof record.url === 'string') {
|
|
86
96
|
imageCandidate = record.url ?? '';
|
|
87
97
|
}
|
|
@@ -91,7 +101,7 @@ export function detectImageAttachment(message) {
|
|
|
91
101
|
else if (typeof record.data === 'string') {
|
|
92
102
|
imageCandidate = record.data ?? '';
|
|
93
103
|
}
|
|
94
|
-
if (typeValue.includes('image') && imageCandidate.trim().length > 0) {
|
|
104
|
+
if ((typeValue.includes('image') || typeValue.includes('video')) && imageCandidate.trim().length > 0) {
|
|
95
105
|
return true;
|
|
96
106
|
}
|
|
97
107
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ProviderSuccessEvent } from './types.js';
|
|
2
|
+
type ProviderSuccessListener = (event: ProviderSuccessEvent) => void;
|
|
3
|
+
export declare class ProviderSuccessCenter {
|
|
4
|
+
private readonly listeners;
|
|
5
|
+
subscribe(listener: ProviderSuccessListener): () => void;
|
|
6
|
+
emit(event: ProviderSuccessEvent): ProviderSuccessEvent;
|
|
7
|
+
private normalize;
|
|
8
|
+
}
|
|
9
|
+
export declare const providerSuccessCenter: ProviderSuccessCenter;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export class ProviderSuccessCenter {
|
|
2
|
+
listeners = new Set();
|
|
3
|
+
subscribe(listener) {
|
|
4
|
+
this.listeners.add(listener);
|
|
5
|
+
return () => {
|
|
6
|
+
this.listeners.delete(listener);
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
emit(event) {
|
|
10
|
+
const enriched = this.normalize(event);
|
|
11
|
+
for (const listener of this.listeners) {
|
|
12
|
+
try {
|
|
13
|
+
listener(enriched);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// Listener failures should not break propagation
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return enriched;
|
|
20
|
+
}
|
|
21
|
+
normalize(event) {
|
|
22
|
+
const timestamp = typeof event.timestamp === 'number' ? event.timestamp : Date.now();
|
|
23
|
+
const runtime = event.runtime || {};
|
|
24
|
+
return {
|
|
25
|
+
runtime,
|
|
26
|
+
timestamp,
|
|
27
|
+
metadata: event.metadata,
|
|
28
|
+
details: event.details
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export const providerSuccessCenter = new ProviderSuccessCenter();
|
|
@@ -166,6 +166,19 @@ export interface AliasSelectionConfig {
|
|
|
166
166
|
* Per-provider overrides keyed by providerId (e.g. "antigravity").
|
|
167
167
|
*/
|
|
168
168
|
providers?: Record<string, AliasSelectionStrategy>;
|
|
169
|
+
/**
|
|
170
|
+
* Antigravity session isolation cooldown window (ms).
|
|
171
|
+
* Within this window, the same Antigravity auth alias must not be reused by a different session.
|
|
172
|
+
* Default: 300000 (5 minutes).
|
|
173
|
+
*/
|
|
174
|
+
sessionLeaseCooldownMs?: number;
|
|
175
|
+
/**
|
|
176
|
+
* Antigravity multi-alias session binding policy.
|
|
177
|
+
* - "lease" (default): prefer the session's last used alias, but can rotate to another alias when needed.
|
|
178
|
+
* - "strict": once a session binds to an alias, it will not switch to another alias; on failure it must
|
|
179
|
+
* fall back to other providers/routes rather than trying a different Antigravity alias.
|
|
180
|
+
*/
|
|
181
|
+
antigravitySessionBinding?: 'lease' | 'strict';
|
|
169
182
|
}
|
|
170
183
|
export interface ContextWeightedLoadBalancingConfig {
|
|
171
184
|
/**
|
|
@@ -238,6 +251,16 @@ export interface VirtualRouterClockConfig {
|
|
|
238
251
|
* Daemon tick interval (ms). 0 disables background cleanup tick (still cleans on load).
|
|
239
252
|
*/
|
|
240
253
|
tickMs?: number;
|
|
254
|
+
/**
|
|
255
|
+
* Allow clock hold flow for non-streaming (JSON) requests.
|
|
256
|
+
* Default: true.
|
|
257
|
+
*/
|
|
258
|
+
holdNonStreaming?: boolean;
|
|
259
|
+
/**
|
|
260
|
+
* Maximum time (ms) a request is allowed to hold waiting for due window.
|
|
261
|
+
* Default: 60s.
|
|
262
|
+
*/
|
|
263
|
+
holdMaxMs?: number;
|
|
241
264
|
}
|
|
242
265
|
export interface VirtualRouterConfig {
|
|
243
266
|
routing: RoutingPools;
|
|
@@ -284,6 +307,11 @@ export interface RouterMetadataInput {
|
|
|
284
307
|
providerProtocol?: string;
|
|
285
308
|
stage?: 'inbound' | 'outbound' | 'response';
|
|
286
309
|
routeHint?: string;
|
|
310
|
+
/**
|
|
311
|
+
* Antigravity-Manager alignment: stable sessionId derived from the first user message text.
|
|
312
|
+
* Used for Antigravity alias/session binding and thoughtSignature persistence.
|
|
313
|
+
*/
|
|
314
|
+
antigravitySessionId?: string;
|
|
287
315
|
/**
|
|
288
316
|
* Indicates that current routing decision is for a request which
|
|
289
317
|
* expects server-side tools orchestration (e.g. web_search).
|
|
@@ -481,6 +509,26 @@ export interface ProviderErrorEvent {
|
|
|
481
509
|
timestamp: number;
|
|
482
510
|
details?: Record<string, unknown>;
|
|
483
511
|
}
|
|
512
|
+
export interface ProviderSuccessRuntimeMetadata {
|
|
513
|
+
requestId: string;
|
|
514
|
+
routeName?: string;
|
|
515
|
+
providerKey?: string;
|
|
516
|
+
providerId?: string;
|
|
517
|
+
providerType?: string;
|
|
518
|
+
providerProtocol?: string;
|
|
519
|
+
pipelineId?: string;
|
|
520
|
+
target?: TargetMetadata | Record<string, unknown>;
|
|
521
|
+
}
|
|
522
|
+
export interface ProviderSuccessEvent {
|
|
523
|
+
runtime: ProviderSuccessRuntimeMetadata;
|
|
524
|
+
timestamp: number;
|
|
525
|
+
/**
|
|
526
|
+
* Optional request metadata snapshot (e.g. sessionId / conversationId).
|
|
527
|
+
* This must not contain provider-specific payload semantics.
|
|
528
|
+
*/
|
|
529
|
+
metadata?: Record<string, unknown>;
|
|
530
|
+
details?: Record<string, unknown>;
|
|
531
|
+
}
|
|
484
532
|
export interface FeatureBuilder {
|
|
485
533
|
build(request: StandardizedRequest, metadata: RouterMetadataInput): RoutingFeatures;
|
|
486
534
|
}
|
|
@@ -3,6 +3,8 @@ export declare const CLOCK_CONFIG_DEFAULTS: {
|
|
|
3
3
|
readonly retentionMs: number;
|
|
4
4
|
readonly dueWindowMs: 60000;
|
|
5
5
|
readonly tickMs: 60000;
|
|
6
|
+
readonly holdNonStreaming: true;
|
|
7
|
+
readonly holdMaxMs: 60000;
|
|
6
8
|
};
|
|
7
9
|
export declare function normalizeClockConfig(raw: unknown): ClockConfigSnapshot | null;
|
|
8
10
|
/**
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export const CLOCK_CONFIG_DEFAULTS = {
|
|
2
2
|
retentionMs: 20 * 60_000,
|
|
3
3
|
dueWindowMs: 60_000,
|
|
4
|
-
tickMs: 60_000
|
|
4
|
+
tickMs: 60_000,
|
|
5
|
+
holdNonStreaming: true,
|
|
6
|
+
holdMaxMs: 60_000
|
|
5
7
|
};
|
|
6
8
|
export function normalizeClockConfig(raw) {
|
|
7
9
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
@@ -23,7 +25,13 @@ export function normalizeClockConfig(raw) {
|
|
|
23
25
|
const tickMs = typeof record.tickMs === 'number' && Number.isFinite(record.tickMs) && record.tickMs >= 0
|
|
24
26
|
? Math.floor(record.tickMs)
|
|
25
27
|
: CLOCK_CONFIG_DEFAULTS.tickMs;
|
|
26
|
-
|
|
28
|
+
const holdNonStreaming = record.holdNonStreaming === true ||
|
|
29
|
+
(typeof record.holdNonStreaming === 'string' && record.holdNonStreaming.trim().toLowerCase() === 'true') ||
|
|
30
|
+
(typeof record.holdNonStreaming === 'number' && record.holdNonStreaming === 1);
|
|
31
|
+
const holdMaxMs = typeof record.holdMaxMs === 'number' && Number.isFinite(record.holdMaxMs) && record.holdMaxMs >= 0
|
|
32
|
+
? Math.floor(record.holdMaxMs)
|
|
33
|
+
: CLOCK_CONFIG_DEFAULTS.holdMaxMs;
|
|
34
|
+
return { enabled: true, retentionMs, dueWindowMs, tickMs, holdNonStreaming, holdMaxMs };
|
|
27
35
|
}
|
|
28
36
|
/**
|
|
29
37
|
* Resolve the effective clock config for a request/session.
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { readSessionDirEnv, resolveClockDir } from './paths.js';
|
|
4
4
|
import { cleanExpiredTasks, coerceState, nowMs } from './state.js';
|
|
5
5
|
import { readJsonFile, writeJsonFileAtomic } from './io.js';
|
|
6
|
+
import { startClockNtpSyncIfNeeded } from './ntp.js';
|
|
6
7
|
let daemonStarted = false;
|
|
7
8
|
let daemonTimer;
|
|
8
9
|
let daemonConfig;
|
|
@@ -16,6 +17,8 @@ export async function startClockDaemonIfNeeded(config) {
|
|
|
16
17
|
}
|
|
17
18
|
daemonStarted = true;
|
|
18
19
|
daemonConfig = config;
|
|
20
|
+
// Best-effort NTP sync (do not block daemon startup).
|
|
21
|
+
void startClockNtpSyncIfNeeded(config);
|
|
19
22
|
const tickOnce = async () => {
|
|
20
23
|
const effective = daemonConfig;
|
|
21
24
|
if (!effective)
|
|
@@ -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();
|