agent-orchestrator-mcp-server 0.7.9 → 0.7.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -17,6 +17,7 @@ export declare const WakeMeUpLaterSchema: z.ZodObject<{
|
|
|
17
17
|
wake_at: string;
|
|
18
18
|
timezone?: string | undefined;
|
|
19
19
|
}>;
|
|
20
|
+
export declare function parseWakeAtToUtcMs(wakeAt: string, timezone: string): number;
|
|
20
21
|
export declare function wakeMeUpLaterTool(_server: Server, clientFactory: () => IAgentOrchestratorClient): {
|
|
21
22
|
name: string;
|
|
22
23
|
description: string;
|
|
@@ -6,6 +6,70 @@ export const WakeMeUpLaterSchema = z.object({
|
|
|
6
6
|
timezone: z.string().optional(),
|
|
7
7
|
prompt: z.string(),
|
|
8
8
|
});
|
|
9
|
+
// Reject wake-ups that resolve to ≤30 seconds in the future. Anything inside
|
|
10
|
+
// this window is effectively "now" given network latency between this tool
|
|
11
|
+
// call and the trigger being scheduled — and the past-dated case (the bug
|
|
12
|
+
// this guards against) silently fires-and-drops, leaving the session
|
|
13
|
+
// permanently asleep.
|
|
14
|
+
const WAKE_AT_GRACE_WINDOW_MS = 30 * 1000;
|
|
15
|
+
function getTimezoneOffsetMs(date, timeZone) {
|
|
16
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
17
|
+
timeZone,
|
|
18
|
+
year: 'numeric',
|
|
19
|
+
month: '2-digit',
|
|
20
|
+
day: '2-digit',
|
|
21
|
+
hour: '2-digit',
|
|
22
|
+
minute: '2-digit',
|
|
23
|
+
second: '2-digit',
|
|
24
|
+
hour12: false,
|
|
25
|
+
}).formatToParts(date);
|
|
26
|
+
const filled = {};
|
|
27
|
+
for (const part of parts) {
|
|
28
|
+
if (part.type !== 'literal')
|
|
29
|
+
filled[part.type] = part.value;
|
|
30
|
+
}
|
|
31
|
+
// hour12: false can emit "24" at midnight in some locales — normalize.
|
|
32
|
+
const hour = parseInt(filled.hour, 10) % 24;
|
|
33
|
+
const asIfUtc = Date.UTC(parseInt(filled.year, 10), parseInt(filled.month, 10) - 1, parseInt(filled.day, 10), hour, parseInt(filled.minute, 10), parseInt(filled.second, 10));
|
|
34
|
+
// Sub-second precision is dropped by Date.UTC, so the offset can be off by
|
|
35
|
+
// up to ~1s when the input has fractional seconds. That's well inside the
|
|
36
|
+
// 30s grace window, so it doesn't affect validation.
|
|
37
|
+
return asIfUtc - date.getTime();
|
|
38
|
+
}
|
|
39
|
+
// Reject inputs that don't look like a calendar+time: bare dates ("2026-04-15"),
|
|
40
|
+
// trailing offsets ("...+05:00"), and `Z` paired with a non-UTC IANA timezone
|
|
41
|
+
// (ambiguous — we'd have to pick one to honor and the other to ignore).
|
|
42
|
+
const NAIVE_DATETIME_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?Z?$/;
|
|
43
|
+
const EXPLICIT_OFFSET_REGEX = /[+-]\d{2}:?\d{2}$/;
|
|
44
|
+
// Convert a naive ISO-8601 wall-clock string in `timezone` to a UTC epoch ms.
|
|
45
|
+
// Iterates twice so DST-boundary inputs converge on the correct offset.
|
|
46
|
+
export function parseWakeAtToUtcMs(wakeAt, timezone) {
|
|
47
|
+
if (EXPLICIT_OFFSET_REGEX.test(wakeAt)) {
|
|
48
|
+
throw new Error(`wake_at must not include a UTC offset (e.g., "+05:00"); pass the wall-clock time and an IANA timezone name (e.g., "America/New_York")`);
|
|
49
|
+
}
|
|
50
|
+
if (!NAIVE_DATETIME_REGEX.test(wakeAt)) {
|
|
51
|
+
throw new Error(`wake_at must be an ISO-8601 datetime like "2026-04-15T14:30:00" (date-only and other formats are not accepted)`);
|
|
52
|
+
}
|
|
53
|
+
const hasZ = wakeAt.endsWith('Z');
|
|
54
|
+
const isUtc = timezone === 'UTC' || timezone === 'Etc/UTC';
|
|
55
|
+
if (hasZ && !isUtc) {
|
|
56
|
+
throw new Error(`wake_at ends with "Z" (UTC) but timezone is "${timezone}". Either drop the trailing "Z" or set timezone to "UTC"`);
|
|
57
|
+
}
|
|
58
|
+
const naive = hasZ ? wakeAt.slice(0, -1) : wakeAt;
|
|
59
|
+
const naiveAsUtc = new Date(naive + 'Z').getTime();
|
|
60
|
+
if (Number.isNaN(naiveAsUtc)) {
|
|
61
|
+
throw new Error(`Invalid wake_at value: "${wakeAt}"`);
|
|
62
|
+
}
|
|
63
|
+
if (isUtc) {
|
|
64
|
+
return naiveAsUtc;
|
|
65
|
+
}
|
|
66
|
+
let utcGuess = naiveAsUtc - getTimezoneOffsetMs(new Date(naiveAsUtc), timezone);
|
|
67
|
+
utcGuess = naiveAsUtc - getTimezoneOffsetMs(new Date(utcGuess), timezone);
|
|
68
|
+
return utcGuess;
|
|
69
|
+
}
|
|
70
|
+
function formatUtcInstant(ms) {
|
|
71
|
+
return new Date(ms).toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
72
|
+
}
|
|
9
73
|
function buildToolDescription() {
|
|
10
74
|
const now = new Date();
|
|
11
75
|
const utcNow = now.toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
@@ -67,9 +131,42 @@ export function wakeMeUpLaterTool(_server, clientFactory) {
|
|
|
67
131
|
handler: async (args) => {
|
|
68
132
|
try {
|
|
69
133
|
const validated = WakeMeUpLaterSchema.parse(args);
|
|
70
|
-
const client = clientFactory();
|
|
71
134
|
const { session_id, wake_at, prompt } = validated;
|
|
72
135
|
const timezone = validated.timezone || 'UTC';
|
|
136
|
+
// Cheapest validation runs first (no DB/API calls). Past-dated
|
|
137
|
+
// wake_at values silently fire-and-drop in the scheduler and leave
|
|
138
|
+
// the session permanently asleep, so reject up front before any
|
|
139
|
+
// state change.
|
|
140
|
+
let wakeAtUtcMs;
|
|
141
|
+
try {
|
|
142
|
+
wakeAtUtcMs = parseWakeAtToUtcMs(wake_at, timezone);
|
|
143
|
+
}
|
|
144
|
+
catch (parseError) {
|
|
145
|
+
return {
|
|
146
|
+
content: [
|
|
147
|
+
{
|
|
148
|
+
type: 'text',
|
|
149
|
+
text: `Error: Could not parse wake_at "${wake_at}" with timezone "${timezone}": ${parseError instanceof Error ? parseError.message : 'Unknown error'}. No trigger was created and no session state was changed.`,
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
isError: true,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const nowMs = Date.now();
|
|
156
|
+
if (wakeAtUtcMs - nowMs <= WAKE_AT_GRACE_WINDOW_MS) {
|
|
157
|
+
const wakeAtUtcStr = formatUtcInstant(wakeAtUtcMs);
|
|
158
|
+
const nowUtcStr = formatUtcInstant(nowMs);
|
|
159
|
+
return {
|
|
160
|
+
content: [
|
|
161
|
+
{
|
|
162
|
+
type: 'text',
|
|
163
|
+
text: `Error: wake_at "${wake_at}" (timezone: ${timezone}) resolves to ${wakeAtUtcStr} UTC, which is in the past or within 30 seconds of the current server time (${nowUtcStr} UTC). No trigger was created and no session state was changed. Recompute relative to the current server time shown in the tool description and call again — wake_at must be more than 30 seconds in the future.`,
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
isError: true,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const client = clientFactory();
|
|
73
170
|
if (parseAllowedAgentRoots() !== null) {
|
|
74
171
|
return {
|
|
75
172
|
content: [
|