agent-orchestrator-mcp-server 0.7.6 → 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.6",
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",
@@ -29,7 +29,7 @@
29
29
  "stage-publish": "npm version"
30
30
  },
31
31
  "dependencies": {
32
- "@modelcontextprotocol/sdk": "^1.26.0",
32
+ "@modelcontextprotocol/sdk": "^1.29.0",
33
33
  "zod": "^3.24.1"
34
34
  },
35
35
  "devDependencies": {
@@ -241,8 +241,8 @@ export function createIntegrationMockOrchestratorClient(initialMockData) {
241
241
  if (!session) {
242
242
  throw new Error(`API Error (404): Session not found`);
243
243
  }
244
- if (session.status !== 'needs_input') {
245
- throw new Error(`API Error (422): Session must be in needs_input state to sleep`);
244
+ if (session.status !== 'needs_input' && session.status !== 'running') {
245
+ throw new Error(`API Error (422): Session must be in needs_input or running state to sleep (current: ${session.status})`);
246
246
  }
247
247
  session.status = 'waiting';
248
248
  session.updated_at = new Date().toISOString();
@@ -633,7 +633,7 @@ export function createIntegrationMockOrchestratorClient(initialMockData) {
633
633
  return {
634
634
  id: 1,
635
635
  name: data.name,
636
- trigger_type: data.trigger_type,
636
+ trigger_type: data.trigger_type || 'schedule',
637
637
  status: data.status || 'enabled',
638
638
  agent_root_name: data.agent_root_name,
639
639
  prompt_template: data.prompt_template,
@@ -642,7 +642,7 @@ export function createIntegrationMockOrchestratorClient(initialMockData) {
642
642
  mcp_servers: data.mcp_servers || [],
643
643
  configuration: data.configuration || {},
644
644
  schedule_description: null,
645
- last_session_id: null,
645
+ last_session_id: data.last_session_id ?? null,
646
646
  last_triggered_at: null,
647
647
  last_polled_at: null,
648
648
  sessions_created_count: 0,
@@ -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');
@@ -16,7 +80,7 @@ function buildToolDescription() {
16
80
  - **Claude Code \`ScheduleWakeup\` tool**: Does not integrate with AO's session lifecycle — it won't transition the session to sleeping/waiting state or create an AO trigger, so AO cannot track or manage the wake-up.
17
81
  - **Claude Code \`Monitor\` tool**: Same problem — it operates outside AO's session state management.
18
82
 
19
- This tool does both: it properly transitions the session to sleeping (waiting) state so AO can reclaim resources, AND creates a one-time AO trigger to resume the session at the specified time.
83
+ This tool creates a one-time AO wake-up trigger bound to the target session. The AO API atomically transitions the session to sleeping (waiting) state as part of creating the trigger, so AO can reclaim resources and the trigger is guaranteed to resume the correct session at the specified time.
20
84
 
21
85
  **Current server time:** ${utcNow} (UTC). Use this as your reference point when calculating wake-up times.
22
86
 
@@ -28,16 +92,15 @@ This tool does both: it properly transitions the session to sleeping (waiting) s
28
92
  - If you omit timezone, wake_at is treated as UTC.
29
93
 
30
94
  **Parameters:**
31
- - **session_id**: The session to wake up (must be in needs_input state)
95
+ - **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
96
  - **wake_at**: ISO 8601 datetime without offset for when to wake up (e.g., "2026-04-15T14:30:00")
33
97
  - **timezone**: IANA timezone for interpreting wake_at (default: "UTC", e.g., "America/New_York")
34
98
  - **prompt**: The prompt to send when waking up the session
35
99
 
36
100
  **What happens:**
37
- 1. Validates the session is in needs_input state
38
- 2. Puts the session to sleep (waiting status)
39
- 3. Creates a one-time schedule trigger that fires at the specified time
40
- 4. The trigger resumes the session with the provided prompt`;
101
+ 1. Creates a one-time schedule trigger bound to this session that fires at the specified time.
102
+ 2. As a side effect of creating the trigger, the AO API transitions the session to sleeping (waiting) status — immediately if currently \`needs_input\`, or after the current turn ends if currently \`running\`.
103
+ 3. At the scheduled time, the trigger resumes the session with the provided prompt.`;
41
104
  }
42
105
  export function wakeMeUpLaterTool(_server, clientFactory) {
43
106
  return {
@@ -48,7 +111,7 @@ export function wakeMeUpLaterTool(_server, clientFactory) {
48
111
  properties: {
49
112
  session_id: {
50
113
  oneOf: [{ type: 'string' }, { type: 'number' }],
51
- description: 'Session ID (numeric) or slug (string). Must be in needs_input state.',
114
+ description: 'Session ID (numeric) or slug (string). Accepts sessions in needs_input or running state — from a running session, the sleep takes effect after the current turn ends.',
52
115
  },
53
116
  wake_at: {
54
117
  type: 'string',
@@ -68,9 +131,42 @@ export function wakeMeUpLaterTool(_server, clientFactory) {
68
131
  handler: async (args) => {
69
132
  try {
70
133
  const validated = WakeMeUpLaterSchema.parse(args);
71
- const client = clientFactory();
72
134
  const { session_id, wake_at, prompt } = validated;
73
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();
74
170
  if (parseAllowedAgentRoots() !== null) {
75
171
  return {
76
172
  content: [
@@ -83,30 +179,49 @@ export function wakeMeUpLaterTool(_server, clientFactory) {
83
179
  };
84
180
  }
85
181
  const session = await client.getSession(session_id);
86
- if (session.status !== 'needs_input') {
182
+ // Reject states the Rails API can't auto-sleep from. `needs_input`
183
+ // immediate sleep; `running` → deferred sleep via pending_sleep metadata;
184
+ // `waiting` → already dormant, trigger fires normally. Anything else
185
+ // (failed, archived) would silently no-op the auto-sleep and leave the
186
+ // caller with a trigger targeting a session that can't be woken.
187
+ const WAKEABLE_STATUSES = ['needs_input', 'running', 'waiting'];
188
+ if (!WAKEABLE_STATUSES.includes(session.status)) {
87
189
  return {
88
190
  content: [
89
191
  {
90
192
  type: 'text',
91
- text: `Error: Session must be in "needs_input" state to sleep (current: "${session.status}"). Only idle sessions can be scheduled for a delayed wake-up.`,
193
+ text: `Error: Session ${session.id} is in "${session.status}" state and cannot be scheduled for wake-up. Only sessions in ${WAKEABLE_STATUSES.join(', ')} can be woken up.`,
92
194
  },
93
195
  ],
94
196
  isError: true,
95
197
  };
96
198
  }
97
- const sleepingSession = await client.sleepSession(session_id);
199
+ // The Rails Trigger model requires agent_root_name, but for per-session
200
+ // wake-up triggers (reuse_session + last_session_id + one-time schedule)
201
+ // the value is never used to spawn a session — the target session is
202
+ // always reused. Prefer the canonical metadata value. The agent_type
203
+ // fallback is a best-effort for pre-migration sessions without an
204
+ // agent_root_key; if agent_type isn't a registered agent root, the
205
+ // createTrigger call will fail loudly with a 422 rather than proceed
206
+ // with a bad value — which is what we want.
207
+ const agentRootName = session.metadata?.agent_root_key || session.agent_type;
98
208
  let trigger;
99
209
  try {
100
210
  trigger = await client.createTrigger({
101
- name: `Wake session #${sleepingSession.id} at ${wake_at}`,
102
- trigger_type: 'schedule',
103
- agent_root_name: session.agent_type,
211
+ name: `Wake session #${session.id} at ${wake_at}`,
212
+ agent_root_name: agentRootName,
104
213
  prompt_template: prompt,
105
214
  reuse_session: true,
106
- configuration: {
107
- scheduled_at: wake_at,
108
- timezone,
109
- },
215
+ last_session_id: session.id,
216
+ trigger_conditions_attributes: [
217
+ {
218
+ condition_type: 'schedule',
219
+ configuration: {
220
+ scheduled_at: wake_at,
221
+ timezone,
222
+ },
223
+ },
224
+ ],
110
225
  });
111
226
  }
112
227
  catch (triggerError) {
@@ -114,28 +229,21 @@ export function wakeMeUpLaterTool(_server, clientFactory) {
114
229
  content: [
115
230
  {
116
231
  type: 'text',
117
- text: `Error: Session ${sleepingSession.id} was put to sleep (waiting status) but trigger creation failed: ${triggerError instanceof Error ? triggerError.message : 'Unknown error'}. The session is now in "waiting" state and needs manual intervention (use action_session with "restart" or "follow_up" to recover).`,
232
+ text: `Error: Trigger creation failed: ${triggerError instanceof Error ? triggerError.message : 'Unknown error'}. The session is still in its original state no changes were made.`,
118
233
  },
119
234
  ],
120
235
  isError: true,
121
236
  };
122
237
  }
123
- await client.updateSession(session_id, {
124
- custom_metadata: {
125
- ...session.custom_metadata,
126
- wake_trigger_id: trigger.id,
127
- },
128
- });
129
238
  const lines = [
130
239
  '## Wake-Up Scheduled Successfully',
131
240
  '',
132
- `- **Session ID:** ${sleepingSession.id}`,
133
- `- **Session Status:** ${sleepingSession.status}`,
241
+ `- **Session ID:** ${session.id}`,
134
242
  `- **Wake At:** ${wake_at} (${timezone})`,
135
243
  `- **Trigger ID:** ${trigger.id}`,
136
244
  `- **Trigger Name:** ${trigger.name}`,
137
245
  '',
138
- '**You must end your conversation turn now.** The session is sleeping and will be automatically resumed at the scheduled time with the provided prompt.',
246
+ '**You must end your conversation turn now.** The session will be automatically transitioned to waiting (immediately if currently needs_input; after the current turn ends if currently running) and resumed at the scheduled time with the provided prompt.',
139
247
  '',
140
248
  '⚠️ **Warning:** If you do not end your conversation turn, the session may still be running when the scheduled wake-up fires. A wake-up cannot be delivered to a session that is not in a wakeable (sleeping/waiting) state — it will be silently dropped, and you will never receive it.',
141
249
  ];
package/shared/types.d.ts CHANGED
@@ -251,9 +251,13 @@ export interface TriggerChannelsResponse {
251
251
  num_members: number;
252
252
  }>;
253
253
  }
254
+ export interface TriggerConditionAttributes {
255
+ condition_type: 'slack' | 'schedule' | 'ao_event';
256
+ configuration: Record<string, unknown>;
257
+ }
254
258
  export interface CreateTriggerRequest {
255
259
  name: string;
256
- trigger_type: TriggerType;
260
+ trigger_type?: TriggerType;
257
261
  agent_root_name: string;
258
262
  prompt_template: string;
259
263
  status?: TriggerStatus;
@@ -261,6 +265,8 @@ export interface CreateTriggerRequest {
261
265
  reuse_session?: boolean;
262
266
  mcp_servers?: string[];
263
267
  configuration?: Record<string, unknown>;
268
+ last_session_id?: number;
269
+ trigger_conditions_attributes?: TriggerConditionAttributes[];
264
270
  }
265
271
  export interface UpdateTriggerRequest {
266
272
  name?: string;