agent-orchestrator-mcp-server 0.8.0 → 0.8.2

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.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Local implementation of agent-orchestrator MCP server",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
@@ -1,5 +1,4 @@
1
1
  import { z } from 'zod';
2
- import { parseAllowedAgentRoots } from '../allowed-agent-roots.js';
3
2
  export const WakeMeUpLaterSchema = z.object({
4
3
  session_id: z.union([z.string(), z.number()]),
5
4
  wake_at: z.string(),
@@ -113,7 +112,13 @@ This guidance does NOT apply when waking at a known wall-clock time (e.g., "9am
113
112
  **What happens:**
114
113
  1. Creates a one-time schedule trigger bound to this session that fires at the specified time.
115
114
  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\`.
116
- 3. At the scheduled time, the trigger resumes the session with the provided prompt.`;
115
+ 3. At the scheduled time, the trigger resumes the session with the provided prompt.
116
+
117
+ **⚠️ Sibling-destroy semantics when paired with state-change wakes.** If this \`wake_me_up_later\` trigger is acting as a deadline backstop alongside \`wake_me_up_when_session_changes_state\` triggers (the recommended triple-wake + deadline pattern), the AO firing path destroys ALL of the requester's other one-time wakes whenever any one of them fires — and that cuts both ways:
118
+ - If a state-change trigger fires first, this deadline backstop is destroyed (not pending in the background).
119
+ - If THIS deadline fires first (e.g., a hung watched session never transitioned), all the companion state-change watchers are destroyed.
120
+
121
+ In either case, the woken-up turn starts with zero remaining scheduled wakes. If the woken-up turn decides to keep waiting (e.g., the wake fired prematurely on a transient flap, or the deadline hit but the watched session is still progressing), it MUST re-register the wakes it still needs — both the state-change watchers and a fresh deadline — before going back to sleep. The originals are gone.`;
117
122
  }
118
123
  export function wakeMeUpLaterTool(_server, clientFactory) {
119
124
  return {
@@ -180,17 +185,6 @@ export function wakeMeUpLaterTool(_server, clientFactory) {
180
185
  };
181
186
  }
182
187
  const client = clientFactory();
183
- if (parseAllowedAgentRoots() !== null) {
184
- return {
185
- content: [
186
- {
187
- type: 'text',
188
- text: 'Error: wake_me_up_later is not allowed when ALLOWED_AGENT_ROOTS is set. Triggers cannot be created because sessions are restricted to specific preconfigured agent roots.',
189
- },
190
- ],
191
- isError: true,
192
- };
193
- }
194
188
  const session = await client.getSession(session_id);
195
189
  // Reject states the Rails API can't auto-sleep from. `needs_input` →
196
190
  // immediate sleep; `running` → deferred sleep via pending_sleep metadata;
@@ -259,6 +253,8 @@ export function wakeMeUpLaterTool(_server, clientFactory) {
259
253
  '**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.',
260
254
  '',
261
255
  '⚠️ **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.',
256
+ '',
257
+ '**Sibling-destroy reminder:** if this trigger is paired with `wake_me_up_when_session_changes_state` triggers (the triple-wake + deadline pattern), whichever wake fires first destroys ALL the others belonging to this requester. If this deadline fires while the watched session is still progressing, the woken-up turn must re-register the state-change watchers AND a new deadline before going back to sleep — the originals are gone.',
262
258
  ];
263
259
  return { content: [{ type: 'text', text: lines.join('\n') }] };
264
260
  }
@@ -16,7 +16,16 @@ This is the **state-based analog of \`wake_me_up_later\`**. Use \`wake_me_up_lat
16
16
  - \`session_needs_input\` — the watched session paused for user input or finished a turn awaiting follow-up.
17
17
  - \`session_failed\` — the watched session crashed.
18
18
 
19
- Pair these three triggers with ONE \`wake_me_up_later\` deadline backstop so a hung watched session can't keep you sleeping forever. The first trigger to fire wins; the others are silently consumed when the requester resumes. **Picking only ONE of the three is a footgun** — if you only schedule \`session_needs_input\` and the downstream session self-archives directly, the only thing that wakes you is the deadline (long, wasteful). Schedule all three unless you have a specific reason to wake only on one outcome.
19
+ Pair these three triggers with ONE \`wake_me_up_later\` deadline backstop so a hung watched session can't keep you sleeping forever. The first trigger to fire wakes the requester. **Picking only ONE of the three is a footgun** — if you only schedule \`session_needs_input\` and the downstream session self-archives directly, the only thing that wakes you is the deadline (long, wasteful). Schedule all three unless you have a specific reason to wake only on one outcome.
20
+
21
+ **⚠️ Sibling-destroy semantics (read carefully — this is a footgun).** When ANY one of the registered wake-ups fires for the requester (any of the three state-change triggers OR the \`wake_me_up_later\` deadline backstop), the AO firing path **destroys all of the requester's other one-time wake-up triggers as a side effect** — they are not just "consumed," they are deleted from the database. This applies across event names *and* across tool types: a fired \`session_needs_input\` watcher destroys the \`session_archived\` watcher, the \`session_failed\` watcher, and the \`wake_me_up_later\` deadline. After any wake fires, the requester has zero remaining scheduled wakes.
22
+
23
+ This matters when a wake fires *prematurely* — for example, a watched session that flaps \`running → needs_input → running\` during startup will fire your \`session_needs_input\` watcher even though the session is still in flight. Your woken-up turn will look at the watched session, see it's still working, and want to go back to sleep. **You must re-register the wakes you still need before going back to sleep** (the other two state events plus a fresh deadline backstop) — the originals are gone. The same applies if your deadline backstop fires while the watched session is still progressing: re-register the state-change watchers and a new deadline if you want to keep waiting.
24
+
25
+ Concretely, in a woken-up turn that determines the watched session has not actually reached its terminal/idle state:
26
+ 1. Inspect the watched session's current state and decide whether to keep waiting.
27
+ 2. If keeping waiting: call \`wake_me_up_when_session_changes_state\` again for each of the state events you care about (typically all three), and call \`wake_me_up_later\` again for a fresh deadline. Then end the turn.
28
+ 3. The originals are NOT still pending in the background — assume nothing remains.
20
29
 
21
30
  **IMPORTANT — Use this tool instead of polling.** When this tool is available, it is the correct way to wait on another session's state. Do NOT use these alternatives:
22
31
  - **Repeated \`get_session\` calls in a poll loop**: wastes compute, racks up tool-call overhead, and either polls too often (waste) or too rarely (latency). The trigger fires immediately on transition with no polling latency.
@@ -24,11 +33,11 @@ Pair these three triggers with ONE \`wake_me_up_later\` deadline backstop so a h
24
33
 
25
34
  **The watched session can be ANY session**, not just one the requester spawned. You can watch a peer session, a session a different agent created, or even a session run by a different user — as long as the requester knows the watched session's id.
26
35
 
27
- **One-shot semantics.** Each trigger auto-disables after firing. If the watched session transitions, the requester wakes up exactly once and only the first-firing trigger's prompt is delivered; any companion triggers (the other two state events plus the deadline backstop) are silently consumed and gone. To wake on a future transition too, schedule another trigger from the woken-up turn.
36
+ **One-shot semantics.** Each trigger auto-disables after firing. If the watched session transitions, the requester wakes up exactly once and only the first-firing trigger's prompt is delivered; any companion triggers (the other two state events plus the deadline backstop) are destroyed as a side effect of the fire — see the sibling-destroy semantics above. To wake on a future transition too, schedule another trigger from the woken-up turn.
28
37
 
29
38
  **Important — fires on transitions, not on current state.** The trigger fires when the watched session *moves into* the target state, not when it is *already in* it. \`failed\` and \`archived\` are both terminal under typical flow — a session that is already \`failed\` will not transition to \`failed\` again, and a session that is already \`archived\` will not transition to \`archived\` again unless someone unarchives it and re-archives it (rare and surprising). \`needs_input\` is non-terminal: if the watched session is already \`needs_input\` when you create the trigger, it waits for the next transition out and back in. This tool rejects up front any case where the trigger could never fire — already-failed and already-archived watched sessions (terminal states), plus the self-watch case (requester == watched) — so the requester doesn't sleep on a trigger that can never fire.
30
39
 
31
- **Deadline backstop pattern.** Always pair the state-change triggers with one \`wake_me_up_later\` trigger so the requester eventually wakes even if the watched session hangs. First trigger to fire wins; the AO firing path resumes the requester once and the others are silently dropped.
40
+ **Deadline backstop pattern.** Always pair the state-change triggers with one \`wake_me_up_later\` trigger so the requester eventually wakes even if the watched session hangs. First trigger to fire wins; firing destroys the requester's other one-time wake-ups (see sibling-destroy semantics above), so a woken-up turn that wants to keep waiting must re-register the wakes it still needs.
32
41
 
33
42
  **Parameters:**
34
43
  - **session_id**: The session to wake up (the requester). 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.
@@ -101,17 +110,6 @@ export function wakeMeUpWhenSessionChangesStateTool(_server, clientFactory) {
101
110
  };
102
111
  }
103
112
  const { session_id, watched_session_id, event_name, prompt } = validated;
104
- if (parseAllowedAgentRoots() !== null) {
105
- return {
106
- content: [
107
- {
108
- type: 'text',
109
- text: 'Error: wake_me_up_when_session_changes_state is not allowed when ALLOWED_AGENT_ROOTS is set. Triggers cannot be created because sessions are restricted to specific preconfigured agent roots.',
110
- },
111
- ],
112
- isError: true,
113
- };
114
- }
115
113
  const client = clientFactory();
116
114
  const session = await client.getSession(session_id);
117
115
  // The trigger fires on the requester's auto-sleep+resume cycle when the
@@ -167,6 +165,28 @@ export function wakeMeUpWhenSessionChangesStateTool(_server, clientFactory) {
167
165
  isError: true,
168
166
  };
169
167
  }
168
+ // ALLOWED_AGENT_ROOTS scopes which agent roots this server is permitted
169
+ // to operate on. The constraint prevents an agent on a restricted server
170
+ // from scheduling wakes on sessions that belong to roots outside its
171
+ // scope. The requester is, by definition, already on an allowed root
172
+ // (it is the calling agent's session); we only need to validate the
173
+ // watched session belongs to the same scope.
174
+ const allowedRoots = parseAllowedAgentRoots();
175
+ if (allowedRoots !== null) {
176
+ const watchedAgentRoot = watchedSession.metadata?.agent_root_key ?? null;
177
+ if (watchedAgentRoot === null || !allowedRoots.includes(watchedAgentRoot)) {
178
+ const watchedAgentRootStr = watchedAgentRoot ?? '(unknown)';
179
+ return {
180
+ content: [
181
+ {
182
+ type: 'text',
183
+ text: `Error: ALLOWED_AGENT_ROOTS is set — watched session ${watched_session_id} belongs to agent root "${watchedAgentRootStr}", which is not in the allowed list [${allowedRoots.join(', ')}]. The trigger would let this server schedule wakes on a session outside its permitted scope. Pass a watched_session_id whose agent root is in the allowed list, or run this tool from a server without ALLOWED_AGENT_ROOTS restrictions.`,
184
+ },
185
+ ],
186
+ isError: true,
187
+ };
188
+ }
189
+ }
170
190
  if (event_name === 'session_failed' && watchedSession.status === 'failed') {
171
191
  return {
172
192
  content: [
@@ -250,6 +270,8 @@ export function wakeMeUpWhenSessionChangesStateTool(_server, clientFactory) {
250
270
  '⚠️ **Warning:** If you do not end your conversation turn, the requester may still be running when the watched session transitions. 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.',
251
271
  '',
252
272
  '**One-shot:** the trigger auto-deletes after firing. If you want to wake on the next transition too, schedule another trigger from the woken-up turn.',
273
+ '',
274
+ "**Sibling-destroy reminder:** when ANY of this requester's one-time wakes fires (this trigger, a companion state-change trigger, or a `wake_me_up_later` deadline), the AO firing path destroys all the others. If the woken-up turn determines the watched session is still in flight (e.g., a transient `needs_input` flap during startup), you MUST re-register the wakes you still need before going back to sleep — they are gone, not pending.",
253
275
  ];
254
276
  return { content: [{ type: 'text', text: lines.join('\n') }] };
255
277
  }