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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-orchestrator-mcp-server",
3
- "version": "0.7.9",
3
+ "version": "0.7.10",
4
4
  "description": "Local implementation of agent-orchestrator MCP server",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
@@ -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: [