agent-orchestrator-mcp-server 0.7.9 → 0.7.11

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.11",
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');
@@ -27,6 +91,14 @@ This tool creates a one-time AO wake-up trigger bound to the target session. The
27
91
  - Use IANA timezone names (e.g., "America/New_York", "Europe/London", "Asia/Tokyo"). Do NOT pass UTC offsets like "+05:00" in the timezone parameter.
28
92
  - If you omit timezone, wake_at is treated as UTC.
29
93
 
94
+ **Choosing wake_at — adaptive scheduling for unknown durations:**
95
+ When monitoring downstream work whose duration you can't predict (e.g., a subagent or pipeline phase), do NOT guess a long interval up front. Default to a short first wake (~5 minutes), observe progress, then extend if needed:
96
+ - If the work is nearly done (~80%+): schedule a short follow-up (a few more minutes).
97
+ - If barely started (<20%): schedule a longer follow-up (e.g., 30–45 minutes), since the underlying work clearly needs more time.
98
+ - In between: pick proportional to remaining work.
99
+
100
+ This keeps you from sleeping through work that finishes early. It does NOT apply when waking at a known wall-clock time (e.g., "9am tomorrow") — use the calculated time directly.
101
+
30
102
  **Parameters:**
31
103
  - **session_id**: The session to wake up. Works from either \`needs_input\` or \`running\` state — if you call this tool from within your own currently-running session, the sleep transition is recorded and takes effect after the current turn ends.
32
104
  - **wake_at**: ISO 8601 datetime without offset for when to wake up (e.g., "2026-04-15T14:30:00")
@@ -67,9 +139,42 @@ export function wakeMeUpLaterTool(_server, clientFactory) {
67
139
  handler: async (args) => {
68
140
  try {
69
141
  const validated = WakeMeUpLaterSchema.parse(args);
70
- const client = clientFactory();
71
142
  const { session_id, wake_at, prompt } = validated;
72
143
  const timezone = validated.timezone || 'UTC';
144
+ // Cheapest validation runs first (no DB/API calls). Past-dated
145
+ // wake_at values silently fire-and-drop in the scheduler and leave
146
+ // the session permanently asleep, so reject up front before any
147
+ // state change.
148
+ let wakeAtUtcMs;
149
+ try {
150
+ wakeAtUtcMs = parseWakeAtToUtcMs(wake_at, timezone);
151
+ }
152
+ catch (parseError) {
153
+ return {
154
+ content: [
155
+ {
156
+ type: 'text',
157
+ 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.`,
158
+ },
159
+ ],
160
+ isError: true,
161
+ };
162
+ }
163
+ const nowMs = Date.now();
164
+ if (wakeAtUtcMs - nowMs <= WAKE_AT_GRACE_WINDOW_MS) {
165
+ const wakeAtUtcStr = formatUtcInstant(wakeAtUtcMs);
166
+ const nowUtcStr = formatUtcInstant(nowMs);
167
+ return {
168
+ content: [
169
+ {
170
+ type: 'text',
171
+ 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.`,
172
+ },
173
+ ],
174
+ isError: true,
175
+ };
176
+ }
177
+ const client = clientFactory();
73
178
  if (parseAllowedAgentRoots() !== null) {
74
179
  return {
75
180
  content: [