agent-orchestrator-mcp-server 0.8.9 → 0.8.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/README.md CHANGED
@@ -21,24 +21,24 @@ MCP server for PulseMCP's agent-orchestrator: a Claude Code + MCP-powered agent-
21
21
 
22
22
  ### Tools
23
23
 
24
- | Tool | Tool Group | Read/Write | Description |
25
- | -------------------------- | ------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
26
- | `quick_search_sessions` | sessions | read | Quick title-based search/list sessions with optional ID lookup, title query, and status filter |
27
- | `get_session` | sessions | read | Get detailed session info with optional logs, transcripts, and transcript format (text/json) |
28
- | `get_configs` | sessions | read | Fetch all static configuration (MCP servers, agent roots, goals) |
29
- | `get_transcript_archive` | sessions | read | Get download URL and metadata for the transcript archive zip file |
30
- | `start_session` | sessions | write | Create and start a new agent session |
31
- | `action_session` | sessions | write | Perform actions: follow_up, pause, restart, archive, unarchive, change_mcp_servers, change_model, fork, refresh, refresh_all, update_notes, update_title, toggle_favorite, bulk_archive |
32
- | `manage_enqueued_messages` | sessions | write | Manage session message queue: list, get, create, update, delete, reorder, interrupt |
33
- | `manage_categories` | sessions | write | Manage dashboard categories that organize sessions: list, create, update, delete, reorder, set_session_category |
34
- | `respond_to_elicitation` | sessions | write | Accept or decline a pending elicitation by its request_id, with optional structured content on accept |
35
- | `get_notifications` | notifications | read | Get/list notifications and badge count |
36
- | `send_push_notification` | notifications | write | Send a push notification about a session needing human attention |
37
- | `action_notification` | notifications | write | Mark read, mark all read, dismiss, dismiss all read notifications |
38
- | `search_triggers` | triggers | read | Search/list automation triggers with optional channel info |
39
- | `action_trigger` | triggers | write | Create, update, delete, toggle automation triggers |
40
- | `get_system_health` | health | read | Get system health report and optional CLI status |
41
- | `action_health` | health | write | System maintenance: cleanup_processes, retry_sessions, archive_old, cli_refresh, cli_clear_cache |
24
+ | Tool | Tool Group | Read/Write | Description |
25
+ | -------------------------- | ------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
26
+ | `quick_search_sessions` | sessions | read | Quick title-based search/list sessions with optional ID lookup, title query, and status filter |
27
+ | `get_session` | sessions | read | Get detailed session info with optional logs, transcripts, and transcript format (text/json) |
28
+ | `get_configs` | sessions | read | Fetch all static configuration (MCP servers, agent roots, goals) |
29
+ | `get_transcript_archive` | sessions | read | Get download URL and metadata for the transcript archive zip file |
30
+ | `start_session` | sessions | write | Create and start a new agent session |
31
+ | `action_session` | sessions | write | Perform actions: follow_up, pause, restart, archive, unarchive, change_mcp_servers, change_model, set_heartbeat, fork, refresh, refresh_all, update_notes, update_title, toggle_favorite, bulk_archive |
32
+ | `manage_enqueued_messages` | sessions | write | Manage session message queue: list, get, create, update, delete, reorder, interrupt |
33
+ | `manage_categories` | sessions | write | Manage dashboard categories that organize sessions: list, create, update, delete, reorder, set_session_category |
34
+ | `respond_to_elicitation` | sessions | write | Accept or decline a pending elicitation by its request_id, with optional structured content on accept |
35
+ | `get_notifications` | notifications | read | Get/list notifications and badge count |
36
+ | `send_push_notification` | notifications | write | Send a push notification about a session needing human attention |
37
+ | `action_notification` | notifications | write | Mark read, mark all read, dismiss, dismiss all read notifications |
38
+ | `search_triggers` | triggers | read | Search/list automation triggers with optional channel info |
39
+ | `action_trigger` | triggers | write | Create, update, delete, toggle automation triggers |
40
+ | `get_system_health` | health | read | Get system health report and optional CLI status |
41
+ | `action_health` | health | write | System maintenance: cleanup_processes, retry_sessions, archive_old, cli_refresh, cli_clear_cache |
42
42
 
43
43
  ### Resources
44
44
 
@@ -213,6 +213,7 @@ This MCP server provides tools for the following Agent Orchestrator REST API end
213
213
  - `POST /api/v1/sessions/:id/refresh` - Refresh session status
214
214
  - `POST /api/v1/sessions/refresh_all` - Refresh all active sessions
215
215
  - `POST /api/v1/sessions/:id/toggle_favorite` - Toggle favorite
216
+ - `PATCH /api/v1/sessions/:id/heartbeat` - Toggle heartbeat and/or set interval (set_heartbeat)
216
217
  - `POST /api/v1/sessions/bulk_archive` - Bulk archive sessions
217
218
 
218
219
  ### Transcript Archive
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-orchestrator-mcp-server",
3
- "version": "0.8.9",
3
+ "version": "0.8.11",
4
4
  "description": "Local implementation of agent-orchestrator MCP server",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * A client for interacting with the Agent Orchestrator REST API.
5
5
  */
6
- import type { Session, Log, SubagentTranscript, SessionsResponse, SearchSessionsResponse, SessionActionResponse, LogsResponse, SubagentTranscriptsResponse, CreateSessionRequest, UpdateSessionRequest, CreateLogRequest, UpdateLogRequest, CreateSubagentTranscriptRequest, UpdateSubagentTranscriptRequest, SessionStatus, LogLevel, SubagentStatus, MCPServerInfo, AgentRootInfo, ConfigsResponse, SendPushNotificationResponse, EnqueuedMessage, EnqueuedMessagesResponse, EnqueuedMessageInterruptResponse, EnqueuedMessageStatus, Category, CategoriesResponse, CreateCategoryRequest, UpdateCategoryRequest, SetSessionCategoryResponse, Trigger, TriggerType, TriggerStatus, TriggersResponse, TriggerResponse, TriggerChannelsResponse, CreateTriggerRequest, UpdateTriggerRequest, Notification, NotificationsResponse, NotificationBadgeResponse, NotificationMarkAllReadResponse, NotificationDismissAllReadResponse, HealthReport, HealthActionResponse, CliStatusResponse, CliActionResponse, ForkSessionResponse, RefreshSessionResponse, RefreshAllSessionsResponse, BulkArchiveResponse, TranscriptResponse, TranscriptArchiveStatusResponse, ElicitationActionType, ElicitationResponse } from '../types.js';
6
+ import type { Session, Log, SubagentTranscript, SessionsResponse, SearchSessionsResponse, SessionActionResponse, SetHeartbeatResponse, LogsResponse, SubagentTranscriptsResponse, CreateSessionRequest, UpdateSessionRequest, CreateLogRequest, UpdateLogRequest, CreateSubagentTranscriptRequest, UpdateSubagentTranscriptRequest, SessionStatus, LogLevel, SubagentStatus, MCPServerInfo, AgentRootInfo, ConfigsResponse, SendPushNotificationResponse, EnqueuedMessage, EnqueuedMessagesResponse, EnqueuedMessageInterruptResponse, EnqueuedMessageStatus, Category, CategoriesResponse, CreateCategoryRequest, UpdateCategoryRequest, SetSessionCategoryResponse, Trigger, TriggerType, TriggerStatus, TriggersResponse, TriggerResponse, TriggerChannelsResponse, TriggerConditionAttributes, TriggerConditionType, CreateTriggerRequest, UpdateTriggerRequest, Notification, NotificationsResponse, NotificationBadgeResponse, NotificationMarkAllReadResponse, NotificationDismissAllReadResponse, HealthReport, HealthActionResponse, CliStatusResponse, CliActionResponse, ForkSessionResponse, RefreshSessionResponse, RefreshAllSessionsResponse, BulkArchiveResponse, TranscriptResponse, TranscriptArchiveStatusResponse, ElicitationActionType, ElicitationResponse } from '../types.js';
7
7
  /** Raw agent root shape as returned by the Rails API */
8
8
  export interface RawAgentRoot {
9
9
  name: string;
@@ -25,6 +25,14 @@ export interface RawAgentRoot {
25
25
  * as they are not needed by MCP clients.
26
26
  */
27
27
  export declare function mapAgentRoot(raw: RawAgentRoot): AgentRootInfo;
28
+ /**
29
+ * Folds the ergonomic top-level `trigger_type` + `configuration` shape into the
30
+ * REST API's nested `trigger_conditions_attributes`. Mirrors the
31
+ * `trigger_type` → `condition_type` mapping that listTriggers uses for filtering.
32
+ * Returns undefined when there is no condition to build (no trigger_type), so
33
+ * callers can leave the field off the request entirely.
34
+ */
35
+ export declare function conditionAttributesFromLegacy(trigger_type: TriggerConditionType | undefined, configuration: Record<string, unknown> | undefined): TriggerConditionAttributes[] | undefined;
28
36
  /**
29
37
  * Interface for the Agent Orchestrator API client.
30
38
  * Used for dependency injection and testing.
@@ -59,6 +67,10 @@ export interface IAgentOrchestratorClient {
59
67
  restartSession(id: string | number): Promise<SessionActionResponse>;
60
68
  changeMcpServers(id: string | number, mcp_servers: string[]): Promise<Session>;
61
69
  changeModel(id: string | number, model: string): Promise<Session>;
70
+ setHeartbeat(id: string | number, options: {
71
+ enabled?: boolean;
72
+ interval_seconds?: number;
73
+ }): Promise<SetHeartbeatResponse>;
62
74
  listLogs(sessionId: string | number, options?: {
63
75
  level?: LogLevel;
64
76
  page?: number;
@@ -193,6 +205,10 @@ export declare class AgentOrchestratorClient implements IAgentOrchestratorClient
193
205
  restartSession(id: string | number): Promise<SessionActionResponse>;
194
206
  changeMcpServers(id: string | number, mcp_servers: string[]): Promise<Session>;
195
207
  changeModel(id: string | number, model: string): Promise<Session>;
208
+ setHeartbeat(id: string | number, options: {
209
+ enabled?: boolean;
210
+ interval_seconds?: number;
211
+ }): Promise<SetHeartbeatResponse>;
196
212
  listLogs(sessionId: string | number, options?: {
197
213
  level?: LogLevel;
198
214
  page?: number;
@@ -256,6 +272,13 @@ export declare class AgentOrchestratorClient implements IAgentOrchestratorClient
256
272
  getTrigger(id: number): Promise<TriggerResponse>;
257
273
  createTrigger(data: CreateTriggerRequest): Promise<Trigger>;
258
274
  updateTrigger(id: number, data: UpdateTriggerRequest): Promise<Trigger>;
275
+ /**
276
+ * Builds the trigger_conditions_attributes for an update expressed with the
277
+ * ergonomic top-level `trigger_type` + `configuration` shape. Fetches the
278
+ * trigger to reuse the matching existing condition's id (so the condition is
279
+ * updated in place rather than duplicated).
280
+ */
281
+ private resolveUpdatedTriggerConditions;
259
282
  deleteTrigger(id: number): Promise<void>;
260
283
  toggleTrigger(id: number): Promise<Trigger>;
261
284
  getTriggerChannels(): Promise<TriggerChannelsResponse>;
@@ -3,6 +3,25 @@
3
3
  *
4
4
  * Used in integration tests to simulate API responses without hitting real endpoints.
5
5
  */
6
+ import { conditionAttributesFromLegacy } from './orchestrator-client.js';
7
+ /**
8
+ * Builds mock TriggerCondition rows from a create/update request, mirroring the
9
+ * real client: fold the ergonomic top-level trigger_type + configuration shape
10
+ * into nested condition attributes when the caller didn't supply them directly.
11
+ */
12
+ function buildMockTriggerConditions(data) {
13
+ const conditionAttributes = data.trigger_conditions_attributes ??
14
+ conditionAttributesFromLegacy(data.trigger_type, data.configuration) ??
15
+ [];
16
+ return conditionAttributes.map((c, i) => ({
17
+ id: i + 1,
18
+ condition_type: c.condition_type,
19
+ configuration: c.configuration,
20
+ description: `${c.condition_type} condition`,
21
+ last_triggered_at: null,
22
+ last_polled_at: null,
23
+ }));
24
+ }
6
25
  /**
7
26
  * Creates a mock Agent Orchestrator client for integration testing.
8
27
  */
@@ -290,6 +309,31 @@ export function createIntegrationMockOrchestratorClient(initialMockData) {
290
309
  session.updated_at = new Date().toISOString();
291
310
  return session;
292
311
  },
312
+ async setHeartbeat(id, options) {
313
+ const session = mockData.sessions?.find((s) => s.id === Number(id) || s.slug === String(id));
314
+ if (!session) {
315
+ throw new Error(`API Error (404): Session not found`);
316
+ }
317
+ if (options.enabled === undefined && options.interval_seconds === undefined) {
318
+ throw new Error(`API Error (422): At least one of "enabled" or "interval_seconds" must be provided`);
319
+ }
320
+ if (options.interval_seconds !== undefined &&
321
+ (options.interval_seconds < 30 || options.interval_seconds > 86400)) {
322
+ throw new Error(`API Error (422): interval_seconds must be between 30 and 86400`);
323
+ }
324
+ if (options.enabled !== undefined) {
325
+ session.heartbeat_enabled = options.enabled;
326
+ }
327
+ if (options.interval_seconds !== undefined) {
328
+ session.heartbeat_interval_seconds = options.interval_seconds;
329
+ }
330
+ session.updated_at = new Date().toISOString();
331
+ return {
332
+ session,
333
+ heartbeat_enabled: session.heartbeat_enabled ?? false,
334
+ heartbeat_interval_seconds: session.heartbeat_interval_seconds ?? 300,
335
+ };
336
+ },
293
337
  async listLogs(sessionId, options) {
294
338
  let logs = (mockData.logs || []).filter((l) => l.session_id === Number(sessionId));
295
339
  if (options?.level) {
@@ -751,14 +795,7 @@ export function createIntegrationMockOrchestratorClient(initialMockData) {
751
795
  return { trigger, recent_sessions: [] };
752
796
  },
753
797
  async createTrigger(data) {
754
- const conditions = (data.trigger_conditions_attributes ?? []).map((c, i) => ({
755
- id: i + 1,
756
- condition_type: c.condition_type,
757
- configuration: c.configuration,
758
- description: `${c.condition_type} condition`,
759
- last_triggered_at: null,
760
- last_polled_at: null,
761
- }));
798
+ const conditions = buildMockTriggerConditions(data);
762
799
  return {
763
800
  id: 1,
764
801
  name: data.name,
@@ -777,14 +814,7 @@ export function createIntegrationMockOrchestratorClient(initialMockData) {
777
814
  };
778
815
  },
779
816
  async updateTrigger(id, data) {
780
- const conditions = (data.trigger_conditions_attributes ?? []).map((c, i) => ({
781
- id: i + 1,
782
- condition_type: c.condition_type,
783
- configuration: c.configuration,
784
- description: `${c.condition_type} condition`,
785
- last_triggered_at: null,
786
- last_polled_at: null,
787
- }));
817
+ const conditions = buildMockTriggerConditions(data);
788
818
  return {
789
819
  id,
790
820
  name: data.name || 'Updated Trigger',
@@ -22,6 +22,19 @@ export function mapAgentRoot(raw) {
22
22
  default_model: raw.default_model,
23
23
  };
24
24
  }
25
+ /**
26
+ * Folds the ergonomic top-level `trigger_type` + `configuration` shape into the
27
+ * REST API's nested `trigger_conditions_attributes`. Mirrors the
28
+ * `trigger_type` → `condition_type` mapping that listTriggers uses for filtering.
29
+ * Returns undefined when there is no condition to build (no trigger_type), so
30
+ * callers can leave the field off the request entirely.
31
+ */
32
+ export function conditionAttributesFromLegacy(trigger_type, configuration) {
33
+ if (trigger_type === undefined) {
34
+ return undefined;
35
+ }
36
+ return [{ condition_type: trigger_type, configuration: configuration ?? {} }];
37
+ }
25
38
  /** Default timeout for API requests in milliseconds */
26
39
  const DEFAULT_TIMEOUT_MS = 30000;
27
40
  /**
@@ -233,6 +246,14 @@ export class AgentOrchestratorClient {
233
246
  });
234
247
  return response.session;
235
248
  }
249
+ async setHeartbeat(id, options) {
250
+ const body = {};
251
+ if (options.enabled !== undefined)
252
+ body.enabled = options.enabled;
253
+ if (options.interval_seconds !== undefined)
254
+ body.interval_seconds = options.interval_seconds;
255
+ return this.request('PATCH', `/sessions/${id}/heartbeat`, body);
256
+ }
236
257
  // Logs
237
258
  async listLogs(sessionId, options) {
238
259
  return this.request('GET', `/sessions/${sessionId}/logs`, undefined, options);
@@ -387,13 +408,65 @@ export class AgentOrchestratorClient {
387
408
  return this.request('GET', `/triggers/${id}`);
388
409
  }
389
410
  async createTrigger(data) {
390
- const response = await this.request('POST', '/triggers', data);
411
+ const { trigger_type, configuration, trigger_conditions_attributes, ...rest } = data;
412
+ const body = { ...rest };
413
+ // The Rails v1 /triggers endpoint permits neither a top-level `trigger_type`
414
+ // nor a top-level `configuration`; the condition must be nested as
415
+ // `trigger_conditions_attributes: [{ condition_type:, configuration: }]`.
416
+ // An explicitly-supplied nested array (e.g. from wake_me_up_* tools) wins;
417
+ // otherwise fold the ergonomic top-level shape into a single condition.
418
+ const conditions = trigger_conditions_attributes ?? conditionAttributesFromLegacy(trigger_type, configuration);
419
+ if (conditions !== undefined) {
420
+ body.trigger_conditions_attributes = conditions;
421
+ }
422
+ const response = await this.request('POST', '/triggers', body);
391
423
  return response.trigger;
392
424
  }
393
425
  async updateTrigger(id, data) {
394
- const response = await this.request('PATCH', `/triggers/${id}`, data);
426
+ const { trigger_type, configuration, trigger_conditions_attributes, ...rest } = data;
427
+ const body = { ...rest };
428
+ if (trigger_conditions_attributes !== undefined) {
429
+ // Caller supplied the nested shape directly — forward verbatim.
430
+ body.trigger_conditions_attributes = trigger_conditions_attributes;
431
+ }
432
+ else if (trigger_type !== undefined || configuration !== undefined) {
433
+ // Ergonomic top-level shape. Resolve the existing condition's id so the
434
+ // condition is modified in place instead of a duplicate being appended
435
+ // (Rails uses accepts_nested_attributes_for). Metadata-only updates that
436
+ // omit both fields skip this entirely and never touch conditions.
437
+ body.trigger_conditions_attributes = await this.resolveUpdatedTriggerConditions(id, trigger_type, configuration);
438
+ }
439
+ const response = await this.request('PATCH', `/triggers/${id}`, body);
395
440
  return response.trigger;
396
441
  }
442
+ /**
443
+ * Builds the trigger_conditions_attributes for an update expressed with the
444
+ * ergonomic top-level `trigger_type` + `configuration` shape. Fetches the
445
+ * trigger to reuse the matching existing condition's id (so the condition is
446
+ * updated in place rather than duplicated).
447
+ */
448
+ async resolveUpdatedTriggerConditions(id, trigger_type, configuration) {
449
+ const { trigger } = await this.getTrigger(id);
450
+ const existing = trigger.conditions ?? [];
451
+ // Pick the condition to modify: the one matching the requested type, or the
452
+ // sole existing condition when no type was provided.
453
+ const target = trigger_type !== undefined
454
+ ? existing.find((c) => c.condition_type === trigger_type)
455
+ : existing.length === 1
456
+ ? existing[0]
457
+ : undefined;
458
+ const conditionType = trigger_type ?? target?.condition_type;
459
+ if (conditionType === undefined) {
460
+ throw new Error('Cannot update trigger configuration without a trigger_type when the trigger has zero or multiple conditions.');
461
+ }
462
+ return [
463
+ {
464
+ ...(target?.id !== undefined && { id: target.id }),
465
+ condition_type: conditionType,
466
+ configuration: configuration ?? target?.configuration ?? {},
467
+ },
468
+ ];
469
+ }
397
470
  async deleteTrigger(id) {
398
471
  await this.request('DELETE', `/triggers/${id}`);
399
472
  }
@@ -74,7 +74,7 @@ export function createRegisterResources(clientFactory) {
74
74
  triggers_readonly: 'Trigger tools (read only): search_triggers',
75
75
  health: 'All health tools (read + write): health report, CLI status, maintenance actions',
76
76
  health_readonly: 'Health tools (read only): get_system_health',
77
- self_session: 'Self-management tools for auto-injected servers: get_session, get_configs (read), action_session (filtered: update_notes, update_title, archive), send_push_notification',
77
+ self_session: 'Self-management tools for auto-injected servers: get_session, get_configs (read), action_session (filtered: update_notes, update_title, set_heartbeat, archive), send_push_notification',
78
78
  },
79
79
  };
80
80
  return {
@@ -3,34 +3,40 @@ import { z } from 'zod';
3
3
  import type { IAgentOrchestratorClient } from '../orchestrator-client/orchestrator-client.js';
4
4
  export declare const ActionSessionSchema: z.ZodObject<{
5
5
  session_id: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber]>>;
6
- action: z.ZodEnum<["follow_up", "pause", "restart", "archive", "unarchive", "change_mcp_servers", "change_model", "fork", "refresh", "refresh_all", "update_notes", "update_title", "toggle_favorite", "bulk_archive"]>;
6
+ action: z.ZodEnum<["follow_up", "pause", "restart", "archive", "unarchive", "change_mcp_servers", "change_model", "set_heartbeat", "fork", "refresh", "refresh_all", "update_notes", "update_title", "toggle_favorite", "bulk_archive"]>;
7
7
  prompt: z.ZodOptional<z.ZodString>;
8
8
  force_immediate: z.ZodOptional<z.ZodBoolean>;
9
9
  mcp_servers: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
10
10
  model: z.ZodOptional<z.ZodString>;
11
+ enabled: z.ZodOptional<z.ZodBoolean>;
12
+ interval_seconds: z.ZodOptional<z.ZodNumber>;
11
13
  message_index: z.ZodOptional<z.ZodNumber>;
12
14
  session_notes: z.ZodOptional<z.ZodString>;
13
15
  session_ids: z.ZodOptional<z.ZodArray<z.ZodNumber, "many">>;
14
16
  title: z.ZodOptional<z.ZodString>;
15
17
  }, "strip", z.ZodTypeAny, {
16
- action: "follow_up" | "pause" | "restart" | "archive" | "unarchive" | "change_mcp_servers" | "change_model" | "fork" | "refresh" | "refresh_all" | "update_notes" | "update_title" | "toggle_favorite" | "bulk_archive";
18
+ action: "follow_up" | "pause" | "restart" | "archive" | "unarchive" | "change_mcp_servers" | "change_model" | "set_heartbeat" | "fork" | "refresh" | "refresh_all" | "update_notes" | "update_title" | "toggle_favorite" | "bulk_archive";
19
+ enabled?: boolean | undefined;
17
20
  prompt?: string | undefined;
18
21
  title?: string | undefined;
19
22
  mcp_servers?: string[] | undefined;
20
23
  session_id?: string | number | undefined;
21
24
  force_immediate?: boolean | undefined;
22
25
  model?: string | undefined;
26
+ interval_seconds?: number | undefined;
23
27
  message_index?: number | undefined;
24
28
  session_notes?: string | undefined;
25
29
  session_ids?: number[] | undefined;
26
30
  }, {
27
- action: "follow_up" | "pause" | "restart" | "archive" | "unarchive" | "change_mcp_servers" | "change_model" | "fork" | "refresh" | "refresh_all" | "update_notes" | "update_title" | "toggle_favorite" | "bulk_archive";
31
+ action: "follow_up" | "pause" | "restart" | "archive" | "unarchive" | "change_mcp_servers" | "change_model" | "set_heartbeat" | "fork" | "refresh" | "refresh_all" | "update_notes" | "update_title" | "toggle_favorite" | "bulk_archive";
32
+ enabled?: boolean | undefined;
28
33
  prompt?: string | undefined;
29
34
  title?: string | undefined;
30
35
  mcp_servers?: string[] | undefined;
31
36
  session_id?: string | number | undefined;
32
37
  force_immediate?: boolean | undefined;
33
38
  model?: string | undefined;
39
+ interval_seconds?: number | undefined;
34
40
  message_index?: number | undefined;
35
41
  session_notes?: string | undefined;
36
42
  session_ids?: number[] | undefined;
@@ -49,8 +55,8 @@ export declare function actionSessionTool(_server: Server, clientFactory: () =>
49
55
  };
50
56
  action: {
51
57
  type: string;
52
- enum: readonly ["follow_up", "pause", "restart", "archive", "unarchive", "change_mcp_servers", "change_model", "fork", "refresh", "refresh_all", "update_notes", "update_title", "toggle_favorite", "bulk_archive"];
53
- description: "Action to perform: \"follow_up\", \"pause\", \"restart\", \"archive\", \"unarchive\", \"change_mcp_servers\", \"change_model\", \"fork\", \"refresh\", \"refresh_all\", \"update_notes\", \"update_title\", \"toggle_favorite\", \"bulk_archive\"";
58
+ enum: readonly ["follow_up", "pause", "restart", "archive", "unarchive", "change_mcp_servers", "change_model", "set_heartbeat", "fork", "refresh", "refresh_all", "update_notes", "update_title", "toggle_favorite", "bulk_archive"];
59
+ description: "Action to perform: \"follow_up\", \"pause\", \"restart\", \"archive\", \"unarchive\", \"change_mcp_servers\", \"change_model\", \"set_heartbeat\", \"fork\", \"refresh\", \"refresh_all\", \"update_notes\", \"update_title\", \"toggle_favorite\", \"bulk_archive\"";
54
60
  };
55
61
  prompt: {
56
62
  type: string;
@@ -71,6 +77,14 @@ export declare function actionSessionTool(_server: Server, clientFactory: () =>
71
77
  type: string;
72
78
  description: "Required for \"change_model\" action. The model identifier to use (e.g., \"opus\", \"sonnet\").";
73
79
  };
80
+ enabled: {
81
+ type: string;
82
+ description: "Optional for \"set_heartbeat\" action. When true, enables the session heartbeat; when false, disables it. Omit to leave the enabled state unchanged (at least one of \"enabled\" or \"interval_seconds\" must be provided).";
83
+ };
84
+ interval_seconds: {
85
+ type: string;
86
+ description: "Optional for \"set_heartbeat\" action. Heartbeat cadence in seconds (30–86400). Omit to leave the interval unchanged (at least one of \"enabled\" or \"interval_seconds\" must be provided).";
87
+ };
74
88
  message_index: {
75
89
  type: string;
76
90
  description: "Required for \"fork\" action. The transcript message index to fork from.";
@@ -109,18 +123,24 @@ export declare function actionSessionTool(_server: Server, clientFactory: () =>
109
123
  };
110
124
  export declare const SelfSessionActionSessionSchema: z.ZodObject<{
111
125
  session_id: z.ZodUnion<[z.ZodString, z.ZodNumber]>;
112
- action: z.ZodEnum<["update_notes", "update_title", "archive"]>;
126
+ action: z.ZodEnum<["update_notes", "update_title", "set_heartbeat", "archive"]>;
113
127
  session_notes: z.ZodOptional<z.ZodString>;
114
128
  title: z.ZodOptional<z.ZodString>;
129
+ enabled: z.ZodOptional<z.ZodBoolean>;
130
+ interval_seconds: z.ZodOptional<z.ZodNumber>;
115
131
  }, "strip", z.ZodTypeAny, {
116
132
  session_id: string | number;
117
- action: "archive" | "update_notes" | "update_title";
133
+ action: "archive" | "set_heartbeat" | "update_notes" | "update_title";
134
+ enabled?: boolean | undefined;
118
135
  title?: string | undefined;
136
+ interval_seconds?: number | undefined;
119
137
  session_notes?: string | undefined;
120
138
  }, {
121
139
  session_id: string | number;
122
- action: "archive" | "update_notes" | "update_title";
140
+ action: "archive" | "set_heartbeat" | "update_notes" | "update_title";
141
+ enabled?: boolean | undefined;
123
142
  title?: string | undefined;
143
+ interval_seconds?: number | undefined;
124
144
  session_notes?: string | undefined;
125
145
  }>;
126
146
  export declare function selfSessionActionSessionTool(_server: Server, clientFactory: () => IAgentOrchestratorClient): {
@@ -137,7 +157,7 @@ export declare function selfSessionActionSessionTool(_server: Server, clientFact
137
157
  };
138
158
  action: {
139
159
  type: string;
140
- enum: readonly ["update_notes", "update_title", "archive"];
160
+ enum: readonly ["update_notes", "update_title", "set_heartbeat", "archive"];
141
161
  description: string;
142
162
  };
143
163
  session_notes: {
@@ -148,6 +168,14 @@ export declare function selfSessionActionSessionTool(_server: Server, clientFact
148
168
  type: string;
149
169
  description: "Required for \"update_title\" action. The new title for the session.";
150
170
  };
171
+ enabled: {
172
+ type: string;
173
+ description: "Optional for \"set_heartbeat\" action. When true, enables the session heartbeat; when false, disables it. Omit to leave the enabled state unchanged (at least one of \"enabled\" or \"interval_seconds\" must be provided).";
174
+ };
175
+ interval_seconds: {
176
+ type: string;
177
+ description: "Optional for \"set_heartbeat\" action. Heartbeat cadence in seconds (30–86400). Omit to leave the interval unchanged (at least one of \"enabled\" or \"interval_seconds\" must be provided).";
178
+ };
151
179
  };
152
180
  required: string[];
153
181
  };
@@ -1,12 +1,44 @@
1
1
  import { z } from 'zod';
2
2
  import { parseAllowedAgentRoots } from '../allowed-agent-roots.js';
3
+ // Heartbeat interval bounds enforced by the Rails API (PATCH /sessions/:id/heartbeat).
4
+ const HEARTBEAT_MIN_INTERVAL_SECONDS = 30;
5
+ const HEARTBEAT_MAX_INTERVAL_SECONDS = 86400;
6
+ /**
7
+ * Validates set_heartbeat arguments. Returns an error message string if the
8
+ * arguments are invalid, or null if they are valid. Mirrors the Rails API's
9
+ * 422 conditions so callers get a clear error before the request is sent.
10
+ */
11
+ function validateHeartbeatArgs(enabled, interval_seconds) {
12
+ if (enabled === undefined && interval_seconds === undefined) {
13
+ return 'Error: The "set_heartbeat" action requires at least one of "enabled" or "interval_seconds".';
14
+ }
15
+ if (interval_seconds !== undefined &&
16
+ (interval_seconds < HEARTBEAT_MIN_INTERVAL_SECONDS ||
17
+ interval_seconds > HEARTBEAT_MAX_INTERVAL_SECONDS)) {
18
+ return `Error: "interval_seconds" must be between ${HEARTBEAT_MIN_INTERVAL_SECONDS} and ${HEARTBEAT_MAX_INTERVAL_SECONDS}.`;
19
+ }
20
+ return null;
21
+ }
22
+ /** Formats a set_heartbeat response into the standard markdown result block. */
23
+ function formatHeartbeatResult(response) {
24
+ return [
25
+ `## Heartbeat Updated`,
26
+ '',
27
+ `- **Session ID:** ${response.session.id}`,
28
+ `- **Title:** ${response.session.title}`,
29
+ `- **Heartbeat Enabled:** ${response.heartbeat_enabled ? 'Yes' : 'No'}`,
30
+ `- **Interval:** ${response.heartbeat_interval_seconds} seconds`,
31
+ ].join('\n');
32
+ }
3
33
  const PARAM_DESCRIPTIONS = {
4
34
  session_id: 'Session ID (numeric) or slug (string). Required for most actions. Not required for "refresh_all" and "bulk_archive".',
5
- action: 'Action to perform: "follow_up", "pause", "restart", "archive", "unarchive", "change_mcp_servers", "change_model", "fork", "refresh", "refresh_all", "update_notes", "update_title", "toggle_favorite", "bulk_archive"',
35
+ action: 'Action to perform: "follow_up", "pause", "restart", "archive", "unarchive", "change_mcp_servers", "change_model", "set_heartbeat", "fork", "refresh", "refresh_all", "update_notes", "update_title", "toggle_favorite", "bulk_archive"',
6
36
  prompt: 'Required for "follow_up" action. The prompt to send to the agent. Not used for other actions.',
7
37
  force_immediate: 'Optional for "follow_up" action. When true, interrupts a running session to deliver the prompt immediately instead of queuing it. Not used for other actions.',
8
38
  mcp_servers: 'Required for "change_mcp_servers" action. Array of MCP server names to set for the session.',
9
39
  model: 'Required for "change_model" action. The model identifier to use (e.g., "opus", "sonnet").',
40
+ enabled: 'Optional for "set_heartbeat" action. When true, enables the session heartbeat; when false, disables it. Omit to leave the enabled state unchanged (at least one of "enabled" or "interval_seconds" must be provided).',
41
+ interval_seconds: 'Optional for "set_heartbeat" action. Heartbeat cadence in seconds (30–86400). Omit to leave the interval unchanged (at least one of "enabled" or "interval_seconds" must be provided).',
10
42
  message_index: 'Required for "fork" action. The transcript message index to fork from.',
11
43
  session_notes: 'Required for "update_notes" action. The notes text to set on the session.',
12
44
  session_ids: 'Required for "bulk_archive" action. Array of session IDs to archive.',
@@ -20,6 +52,7 @@ const ACTION_ENUM = [
20
52
  'unarchive',
21
53
  'change_mcp_servers',
22
54
  'change_model',
55
+ 'set_heartbeat',
23
56
  'fork',
24
57
  'refresh',
25
58
  'refresh_all',
@@ -35,6 +68,8 @@ export const ActionSessionSchema = z.object({
35
68
  force_immediate: z.boolean().optional().describe(PARAM_DESCRIPTIONS.force_immediate),
36
69
  mcp_servers: z.array(z.string()).optional().describe(PARAM_DESCRIPTIONS.mcp_servers),
37
70
  model: z.string().optional().describe(PARAM_DESCRIPTIONS.model),
71
+ enabled: z.boolean().optional().describe(PARAM_DESCRIPTIONS.enabled),
72
+ interval_seconds: z.number().int().optional().describe(PARAM_DESCRIPTIONS.interval_seconds),
38
73
  message_index: z.number().optional().describe(PARAM_DESCRIPTIONS.message_index),
39
74
  session_notes: z.string().optional().describe(PARAM_DESCRIPTIONS.session_notes),
40
75
  session_ids: z.array(z.number()).optional().describe(PARAM_DESCRIPTIONS.session_ids),
@@ -50,6 +85,7 @@ const TOOL_DESCRIPTION = `Perform an action on an agent session.
50
85
  - **unarchive**: Restore an archived session to idle "needs_input" status
51
86
  - **change_mcp_servers**: Update the MCP servers for a session (requires "mcp_servers" parameter)
52
87
  - **change_model**: Update the model for a session (requires "model" parameter, e.g., "opus", "sonnet")
88
+ - **set_heartbeat**: Toggle a session's heartbeat and/or set its interval (provide "enabled" and/or "interval_seconds"). When enabled and the session sits in needs_input, a recurring nudge prompts it to keep working toward its goal; set "enabled" to false to stop the nudges.
53
89
  - **fork**: Fork a session from a specific transcript message (requires "message_index")
54
90
  - **refresh**: Refresh a single session's status from the execution provider
55
91
  - **refresh_all**: Refresh all active sessions (no session_id needed)
@@ -97,6 +133,14 @@ export function actionSessionTool(_server, clientFactory) {
97
133
  type: 'string',
98
134
  description: PARAM_DESCRIPTIONS.model,
99
135
  },
136
+ enabled: {
137
+ type: 'boolean',
138
+ description: PARAM_DESCRIPTIONS.enabled,
139
+ },
140
+ interval_seconds: {
141
+ type: 'number',
142
+ description: PARAM_DESCRIPTIONS.interval_seconds,
143
+ },
100
144
  message_index: {
101
145
  type: 'number',
102
146
  description: PARAM_DESCRIPTIONS.message_index,
@@ -121,7 +165,7 @@ export function actionSessionTool(_server, clientFactory) {
121
165
  try {
122
166
  const validatedArgs = ActionSessionSchema.parse(args);
123
167
  const client = clientFactory();
124
- const { session_id, action, prompt, force_immediate, mcp_servers, model, message_index, session_notes, session_ids, title, } = validatedArgs;
168
+ const { session_id, action, prompt, force_immediate, mcp_servers, model, enabled, interval_seconds, message_index, session_notes, session_ids, title, } = validatedArgs;
125
169
  // Actions that require session_id
126
170
  const requiresSessionId = [
127
171
  'follow_up',
@@ -131,6 +175,7 @@ export function actionSessionTool(_server, clientFactory) {
131
175
  'unarchive',
132
176
  'change_mcp_servers',
133
177
  'change_model',
178
+ 'set_heartbeat',
134
179
  'fork',
135
180
  'refresh',
136
181
  'update_notes',
@@ -184,6 +229,16 @@ export function actionSessionTool(_server, clientFactory) {
184
229
  isError: true,
185
230
  };
186
231
  }
232
+ // Validate set_heartbeat arguments
233
+ if (action === 'set_heartbeat') {
234
+ const heartbeatError = validateHeartbeatArgs(enabled, interval_seconds);
235
+ if (heartbeatError) {
236
+ return {
237
+ content: [{ type: 'text', text: heartbeatError }],
238
+ isError: true,
239
+ };
240
+ }
241
+ }
187
242
  // Block change_mcp_servers when ALLOWED_AGENT_ROOTS is active
188
243
  if (action === 'change_mcp_servers' && parseAllowedAgentRoots() !== null) {
189
244
  return {
@@ -375,6 +430,14 @@ export function actionSessionTool(_server, clientFactory) {
375
430
  result = lines.join('\n');
376
431
  break;
377
432
  }
433
+ case 'set_heartbeat': {
434
+ const response = await client.setHeartbeat(session_id, {
435
+ enabled,
436
+ interval_seconds,
437
+ });
438
+ result = formatHeartbeatResult(response);
439
+ break;
440
+ }
378
441
  case 'fork': {
379
442
  const response = await client.forkSession(session_id, message_index);
380
443
  const lines = [
@@ -503,27 +566,36 @@ export function actionSessionTool(_server, clientFactory) {
503
566
  // SELF-SESSION VARIANT
504
567
  // =============================================================================
505
568
  // Restricted version of action_session for the self_session composite group.
506
- // Only allows self-management actions: update_notes, update_title, archive.
569
+ // Only allows self-management actions: update_notes, update_title, set_heartbeat, archive.
507
570
  // =============================================================================
508
- const SELF_SESSION_ACTION_ENUM = ['update_notes', 'update_title', 'archive'];
571
+ const SELF_SESSION_ACTION_ENUM = [
572
+ 'update_notes',
573
+ 'update_title',
574
+ 'set_heartbeat',
575
+ 'archive',
576
+ ];
509
577
  export const SelfSessionActionSessionSchema = z.object({
510
578
  session_id: z.union([z.string(), z.number()]).describe(PARAM_DESCRIPTIONS.session_id),
511
579
  action: z
512
580
  .enum(SELF_SESSION_ACTION_ENUM)
513
- .describe('Action to perform: "update_notes", "update_title", "archive"'),
581
+ .describe('Action to perform: "update_notes", "update_title", "set_heartbeat", "archive"'),
514
582
  session_notes: z.string().optional().describe(PARAM_DESCRIPTIONS.session_notes),
515
583
  title: z.string().optional().describe(PARAM_DESCRIPTIONS.title),
584
+ enabled: z.boolean().optional().describe(PARAM_DESCRIPTIONS.enabled),
585
+ interval_seconds: z.number().int().optional().describe(PARAM_DESCRIPTIONS.interval_seconds),
516
586
  });
517
587
  const SELF_SESSION_TOOL_DESCRIPTION = `Perform a self-management action on a session.
518
588
 
519
589
  **Actions (limited to self-management):**
520
590
  - **update_notes**: Update the notes on a session (requires "session_notes")
521
591
  - **update_title**: Update the title of a session (requires "title")
592
+ - **set_heartbeat**: Toggle this session's own heartbeat and/or set its interval (provide "enabled" and/or "interval_seconds"). When the heartbeat is enabled and this session sits in needs_input, a recurring nudge prompts it to keep working toward its goal. If you are genuinely blocked or done, set "enabled" to false to stop the nudges.
522
593
  - **archive**: Archive a session (marks as completed)
523
594
 
524
595
  **Use cases:**
525
596
  - Update session notes to record progress or context
526
597
  - Set a meaningful session title
598
+ - Turn off this session's heartbeat when blocked or finished (set_heartbeat with enabled=false)
527
599
  - Archive the session when work is complete
528
600
 
529
601
  **Archive guidelines:**
@@ -544,7 +616,7 @@ export function selfSessionActionSessionTool(_server, clientFactory) {
544
616
  action: {
545
617
  type: 'string',
546
618
  enum: SELF_SESSION_ACTION_ENUM,
547
- description: 'Action to perform: "update_notes", "update_title", "archive"',
619
+ description: 'Action to perform: "update_notes", "update_title", "set_heartbeat", "archive"',
548
620
  },
549
621
  session_notes: {
550
622
  type: 'string',
@@ -554,6 +626,14 @@ export function selfSessionActionSessionTool(_server, clientFactory) {
554
626
  type: 'string',
555
627
  description: PARAM_DESCRIPTIONS.title,
556
628
  },
629
+ enabled: {
630
+ type: 'boolean',
631
+ description: PARAM_DESCRIPTIONS.enabled,
632
+ },
633
+ interval_seconds: {
634
+ type: 'number',
635
+ description: PARAM_DESCRIPTIONS.interval_seconds,
636
+ },
557
637
  },
558
638
  required: ['session_id', 'action'],
559
639
  },
@@ -561,7 +641,7 @@ export function selfSessionActionSessionTool(_server, clientFactory) {
561
641
  try {
562
642
  const validatedArgs = SelfSessionActionSessionSchema.parse(args);
563
643
  const client = clientFactory();
564
- const { session_id, action, session_notes, title } = validatedArgs;
644
+ const { session_id, action, session_notes, title, enabled, interval_seconds } = validatedArgs;
565
645
  // Validate update_notes requires session_notes
566
646
  if (action === 'update_notes' && session_notes === undefined) {
567
647
  return {
@@ -586,6 +666,16 @@ export function selfSessionActionSessionTool(_server, clientFactory) {
586
666
  isError: true,
587
667
  };
588
668
  }
669
+ // Validate set_heartbeat arguments
670
+ if (action === 'set_heartbeat') {
671
+ const heartbeatError = validateHeartbeatArgs(enabled, interval_seconds);
672
+ if (heartbeatError) {
673
+ return {
674
+ content: [{ type: 'text', text: heartbeatError }],
675
+ isError: true,
676
+ };
677
+ }
678
+ }
589
679
  let result;
590
680
  switch (action) {
591
681
  case 'archive': {
@@ -601,6 +691,14 @@ export function selfSessionActionSessionTool(_server, clientFactory) {
601
691
  result = lines.join('\n');
602
692
  break;
603
693
  }
694
+ case 'set_heartbeat': {
695
+ const response = await client.setHeartbeat(session_id, {
696
+ enabled,
697
+ interval_seconds,
698
+ });
699
+ result = formatHeartbeatResult(response);
700
+ break;
701
+ }
604
702
  case 'update_notes': {
605
703
  const session = await client.updateSessionNotes(session_id, session_notes);
606
704
  const lines = [
@@ -19,24 +19,24 @@ export declare const ActionTriggerSchema: z.ZodObject<{
19
19
  goal?: string | undefined;
20
20
  mcp_servers?: string[] | undefined;
21
21
  trigger_type?: "slack" | "schedule" | undefined;
22
+ configuration?: Record<string, unknown> | undefined;
22
23
  name?: string | undefined;
23
- id?: number | undefined;
24
24
  agent_root_name?: string | undefined;
25
25
  prompt_template?: string | undefined;
26
26
  reuse_session?: boolean | undefined;
27
- configuration?: Record<string, unknown> | undefined;
27
+ id?: number | undefined;
28
28
  }, {
29
29
  action: "create" | "update" | "delete" | "toggle";
30
30
  status?: "enabled" | "disabled" | undefined;
31
31
  goal?: string | undefined;
32
32
  mcp_servers?: string[] | undefined;
33
33
  trigger_type?: "slack" | "schedule" | undefined;
34
+ configuration?: Record<string, unknown> | undefined;
34
35
  name?: string | undefined;
35
- id?: number | undefined;
36
36
  agent_root_name?: string | undefined;
37
37
  prompt_template?: string | undefined;
38
38
  reuse_session?: boolean | undefined;
39
- configuration?: Record<string, unknown> | undefined;
39
+ id?: number | undefined;
40
40
  }>;
41
41
  export declare function actionTriggerTool(_server: Server, clientFactory: () => IAgentOrchestratorClient): {
42
42
  name: string;
@@ -11,16 +11,16 @@ export declare const ManageCategoriesSchema: z.ZodObject<{
11
11
  session_id: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber]>>;
12
12
  }, "strip", z.ZodTypeAny, {
13
13
  action: "list" | "create" | "update" | "delete" | "reorder" | "set_session_category";
14
- description?: string | undefined;
15
14
  name?: string | undefined;
15
+ description?: string | undefined;
16
16
  session_id?: string | number | undefined;
17
17
  category_id?: number | null | undefined;
18
18
  is_frozen?: boolean | undefined;
19
19
  ids?: (number | "uncategorized")[] | undefined;
20
20
  }, {
21
21
  action: "list" | "create" | "update" | "delete" | "reorder" | "set_session_category";
22
- description?: string | undefined;
23
22
  name?: string | undefined;
23
+ description?: string | undefined;
24
24
  session_id?: string | number | undefined;
25
25
  category_id?: number | null | undefined;
26
26
  is_frozen?: boolean | undefined;
package/shared/tools.js CHANGED
@@ -69,7 +69,7 @@ export function parseEnabledToolGroups(enabledGroupsParam) {
69
69
  * - get_configs: Fetch all static configuration (sessions, read; self_session)
70
70
  * - get_transcript_archive: Get transcript archive download URL and metadata (sessions, read)
71
71
  * - start_session: Create a new session (sessions, write)
72
- * - action_session: Perform session actions (sessions, write; self_session: filtered to update_notes, update_title, archive)
72
+ * - action_session: Perform session actions (sessions, write; self_session: filtered to update_notes, update_title, set_heartbeat, archive)
73
73
  * - manage_enqueued_messages: Manage session message queue (sessions, write)
74
74
  * - manage_categories: Manage dashboard categories and session assignment (sessions, write)
75
75
  * - respond_to_elicitation: Accept or decline a pending elicitation by request_id (sessions, write)
package/shared/types.d.ts CHANGED
@@ -34,6 +34,8 @@ export interface Session {
34
34
  archived_at: string | null;
35
35
  category_id?: number | null;
36
36
  category?: CategorySummary | null;
37
+ heartbeat_enabled?: boolean;
38
+ heartbeat_interval_seconds?: number;
37
39
  created_at: string;
38
40
  updated_at: string;
39
41
  transcript?: string;
@@ -82,6 +84,11 @@ export interface SessionActionResponse {
82
84
  session: Session;
83
85
  message?: string;
84
86
  }
87
+ export interface SetHeartbeatResponse {
88
+ session: Session;
89
+ heartbeat_enabled: boolean;
90
+ heartbeat_interval_seconds: number;
91
+ }
85
92
  export interface LogsResponse {
86
93
  logs: Log[];
87
94
  pagination: Pagination;
@@ -298,6 +305,7 @@ export interface TriggerChannelsResponse {
298
305
  }>;
299
306
  }
300
307
  export interface TriggerConditionAttributes {
308
+ id?: number;
301
309
  condition_type: TriggerConditionType;
302
310
  configuration: Record<string, unknown>;
303
311
  }