@lobu/gateway 3.0.8 → 3.0.12

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.
Files changed (219) hide show
  1. package/dist/api/platform.d.ts.map +1 -1
  2. package/dist/api/platform.js +8 -26
  3. package/dist/api/platform.js.map +1 -1
  4. package/dist/auth/mcp/proxy.d.ts +14 -0
  5. package/dist/auth/mcp/proxy.d.ts.map +1 -1
  6. package/dist/auth/mcp/proxy.js +149 -13
  7. package/dist/auth/mcp/proxy.js.map +1 -1
  8. package/dist/cli/gateway.d.ts.map +1 -1
  9. package/dist/cli/gateway.js +29 -0
  10. package/dist/cli/gateway.js.map +1 -1
  11. package/dist/cli/index.js +2 -2
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/connections/chat-instance-manager.d.ts.map +1 -1
  14. package/dist/connections/chat-instance-manager.js +2 -1
  15. package/dist/connections/chat-instance-manager.js.map +1 -1
  16. package/dist/connections/interaction-bridge.d.ts +9 -2
  17. package/dist/connections/interaction-bridge.d.ts.map +1 -1
  18. package/dist/connections/interaction-bridge.js +132 -230
  19. package/dist/connections/interaction-bridge.js.map +1 -1
  20. package/dist/connections/message-handler-bridge.d.ts.map +1 -1
  21. package/dist/connections/message-handler-bridge.js +44 -26
  22. package/dist/connections/message-handler-bridge.js.map +1 -1
  23. package/dist/interactions.d.ts +9 -43
  24. package/dist/interactions.d.ts.map +1 -1
  25. package/dist/interactions.js +10 -52
  26. package/dist/interactions.js.map +1 -1
  27. package/dist/orchestration/base-deployment-manager.js +7 -7
  28. package/dist/orchestration/base-deployment-manager.js.map +1 -1
  29. package/dist/platform/unified-thread-consumer.d.ts.map +1 -1
  30. package/dist/platform/unified-thread-consumer.js +38 -34
  31. package/dist/platform/unified-thread-consumer.js.map +1 -1
  32. package/dist/routes/public/agent.d.ts +4 -0
  33. package/dist/routes/public/agent.d.ts.map +1 -1
  34. package/dist/routes/public/agent.js +21 -0
  35. package/dist/routes/public/agent.js.map +1 -1
  36. package/dist/services/core-services.d.ts.map +1 -1
  37. package/dist/services/core-services.js +4 -0
  38. package/dist/services/core-services.js.map +1 -1
  39. package/package.json +2 -2
  40. package/src/__tests__/agent-config-routes.test.ts +0 -254
  41. package/src/__tests__/agent-history-routes.test.ts +0 -72
  42. package/src/__tests__/agent-routes.test.ts +0 -68
  43. package/src/__tests__/agent-schedules-routes.test.ts +0 -59
  44. package/src/__tests__/agent-settings-store.test.ts +0 -323
  45. package/src/__tests__/bedrock-model-catalog.test.ts +0 -40
  46. package/src/__tests__/bedrock-openai-service.test.ts +0 -157
  47. package/src/__tests__/bedrock-provider-module.test.ts +0 -56
  48. package/src/__tests__/chat-instance-manager-slack.test.ts +0 -204
  49. package/src/__tests__/chat-response-bridge.test.ts +0 -131
  50. package/src/__tests__/config-memory-plugins.test.ts +0 -92
  51. package/src/__tests__/config-request-store.test.ts +0 -127
  52. package/src/__tests__/connection-routes.test.ts +0 -144
  53. package/src/__tests__/core-services-store-selection.test.ts +0 -92
  54. package/src/__tests__/docker-deployment.test.ts +0 -1211
  55. package/src/__tests__/embedded-deployment.test.ts +0 -342
  56. package/src/__tests__/grant-store.test.ts +0 -148
  57. package/src/__tests__/http-proxy.test.ts +0 -281
  58. package/src/__tests__/instruction-service.test.ts +0 -37
  59. package/src/__tests__/link-buttons.test.ts +0 -112
  60. package/src/__tests__/lobu.test.ts +0 -32
  61. package/src/__tests__/mcp-config-service.test.ts +0 -347
  62. package/src/__tests__/mcp-proxy.test.ts +0 -694
  63. package/src/__tests__/message-handler-bridge.test.ts +0 -17
  64. package/src/__tests__/model-selection.test.ts +0 -172
  65. package/src/__tests__/oauth-templates.test.ts +0 -39
  66. package/src/__tests__/platform-adapter-slack-send.test.ts +0 -114
  67. package/src/__tests__/platform-helpers-model-resolution.test.ts +0 -253
  68. package/src/__tests__/provider-inheritance.test.ts +0 -212
  69. package/src/__tests__/routes/cli-auth.test.ts +0 -337
  70. package/src/__tests__/routes/interactions.test.ts +0 -121
  71. package/src/__tests__/secret-proxy.test.ts +0 -85
  72. package/src/__tests__/session-manager.test.ts +0 -572
  73. package/src/__tests__/setup.ts +0 -133
  74. package/src/__tests__/skill-and-mcp-registry.test.ts +0 -203
  75. package/src/__tests__/slack-routes.test.ts +0 -161
  76. package/src/__tests__/system-config-resolver.test.ts +0 -75
  77. package/src/__tests__/system-message-limiter.test.ts +0 -89
  78. package/src/__tests__/system-skills-service.test.ts +0 -362
  79. package/src/__tests__/transcription-service.test.ts +0 -222
  80. package/src/__tests__/utils/rate-limiter.test.ts +0 -102
  81. package/src/__tests__/worker-connection-manager.test.ts +0 -497
  82. package/src/__tests__/worker-job-router.test.ts +0 -722
  83. package/src/api/index.ts +0 -1
  84. package/src/api/platform.ts +0 -292
  85. package/src/api/response-renderer.ts +0 -157
  86. package/src/auth/agent-metadata-store.ts +0 -168
  87. package/src/auth/api-auth-middleware.ts +0 -69
  88. package/src/auth/api-key-provider-module.ts +0 -213
  89. package/src/auth/base-provider-module.ts +0 -201
  90. package/src/auth/bedrock/provider-module.ts +0 -110
  91. package/src/auth/chatgpt/chatgpt-oauth-module.ts +0 -185
  92. package/src/auth/chatgpt/device-code-client.ts +0 -218
  93. package/src/auth/chatgpt/index.ts +0 -1
  94. package/src/auth/claude/oauth-module.ts +0 -280
  95. package/src/auth/cli/token-service.ts +0 -249
  96. package/src/auth/external/client.ts +0 -560
  97. package/src/auth/external/device-code-client.ts +0 -235
  98. package/src/auth/mcp/config-service.ts +0 -420
  99. package/src/auth/mcp/proxy.ts +0 -1086
  100. package/src/auth/mcp/string-substitution.ts +0 -17
  101. package/src/auth/mcp/tool-cache.ts +0 -90
  102. package/src/auth/oauth/base-client.ts +0 -267
  103. package/src/auth/oauth/client.ts +0 -153
  104. package/src/auth/oauth/credentials.ts +0 -7
  105. package/src/auth/oauth/providers.ts +0 -69
  106. package/src/auth/oauth/state-store.ts +0 -150
  107. package/src/auth/oauth-templates.ts +0 -179
  108. package/src/auth/provider-catalog.ts +0 -220
  109. package/src/auth/provider-model-options.ts +0 -41
  110. package/src/auth/settings/agent-settings-store.ts +0 -565
  111. package/src/auth/settings/auth-profiles-manager.ts +0 -216
  112. package/src/auth/settings/index.ts +0 -12
  113. package/src/auth/settings/model-preference-store.ts +0 -52
  114. package/src/auth/settings/model-selection.ts +0 -135
  115. package/src/auth/settings/resolved-settings-view.ts +0 -298
  116. package/src/auth/settings/template-utils.ts +0 -44
  117. package/src/auth/settings/token-service.ts +0 -88
  118. package/src/auth/system-env-store.ts +0 -98
  119. package/src/auth/user-agents-store.ts +0 -68
  120. package/src/channels/binding-service.ts +0 -214
  121. package/src/channels/index.ts +0 -4
  122. package/src/cli/gateway.ts +0 -1312
  123. package/src/cli/index.ts +0 -74
  124. package/src/commands/built-in-commands.ts +0 -80
  125. package/src/commands/command-dispatcher.ts +0 -94
  126. package/src/commands/command-reply-adapters.ts +0 -27
  127. package/src/config/file-loader.ts +0 -618
  128. package/src/config/index.ts +0 -588
  129. package/src/config/network-allowlist.ts +0 -71
  130. package/src/connections/chat-instance-manager.ts +0 -1284
  131. package/src/connections/chat-response-bridge.ts +0 -618
  132. package/src/connections/index.ts +0 -7
  133. package/src/connections/interaction-bridge.ts +0 -831
  134. package/src/connections/message-handler-bridge.ts +0 -415
  135. package/src/connections/platform-auth-methods.ts +0 -15
  136. package/src/connections/types.ts +0 -84
  137. package/src/gateway/connection-manager.ts +0 -291
  138. package/src/gateway/index.ts +0 -698
  139. package/src/gateway/job-router.ts +0 -201
  140. package/src/gateway-main.ts +0 -200
  141. package/src/index.ts +0 -41
  142. package/src/infrastructure/queue/index.ts +0 -12
  143. package/src/infrastructure/queue/queue-producer.ts +0 -148
  144. package/src/infrastructure/queue/redis-queue.ts +0 -361
  145. package/src/infrastructure/queue/types.ts +0 -133
  146. package/src/infrastructure/redis/system-message-limiter.ts +0 -94
  147. package/src/interactions/config-request-store.ts +0 -198
  148. package/src/interactions.ts +0 -363
  149. package/src/lobu.ts +0 -311
  150. package/src/metrics/prometheus.ts +0 -159
  151. package/src/modules/module-system.ts +0 -179
  152. package/src/orchestration/base-deployment-manager.ts +0 -900
  153. package/src/orchestration/deployment-utils.ts +0 -98
  154. package/src/orchestration/impl/docker-deployment.ts +0 -620
  155. package/src/orchestration/impl/embedded-deployment.ts +0 -268
  156. package/src/orchestration/impl/index.ts +0 -8
  157. package/src/orchestration/impl/k8s/deployment.ts +0 -1061
  158. package/src/orchestration/impl/k8s/helpers.ts +0 -610
  159. package/src/orchestration/impl/k8s/index.ts +0 -1
  160. package/src/orchestration/index.ts +0 -333
  161. package/src/orchestration/message-consumer.ts +0 -584
  162. package/src/orchestration/scheduled-wakeup.ts +0 -704
  163. package/src/permissions/approval-policy.ts +0 -36
  164. package/src/permissions/grant-store.ts +0 -219
  165. package/src/platform/file-handler.ts +0 -66
  166. package/src/platform/link-buttons.ts +0 -57
  167. package/src/platform/renderer-utils.ts +0 -44
  168. package/src/platform/response-renderer.ts +0 -84
  169. package/src/platform/unified-thread-consumer.ts +0 -187
  170. package/src/platform.ts +0 -318
  171. package/src/proxy/http-proxy.ts +0 -752
  172. package/src/proxy/proxy-manager.ts +0 -81
  173. package/src/proxy/secret-proxy.ts +0 -402
  174. package/src/proxy/token-refresh-job.ts +0 -143
  175. package/src/routes/internal/audio.ts +0 -141
  176. package/src/routes/internal/device-auth.ts +0 -652
  177. package/src/routes/internal/files.ts +0 -226
  178. package/src/routes/internal/history.ts +0 -69
  179. package/src/routes/internal/images.ts +0 -127
  180. package/src/routes/internal/interactions.ts +0 -84
  181. package/src/routes/internal/middleware.ts +0 -23
  182. package/src/routes/internal/schedule.ts +0 -226
  183. package/src/routes/internal/types.ts +0 -22
  184. package/src/routes/openapi-auto.ts +0 -239
  185. package/src/routes/public/agent-access.ts +0 -23
  186. package/src/routes/public/agent-config.ts +0 -675
  187. package/src/routes/public/agent-history.ts +0 -422
  188. package/src/routes/public/agent-schedules.ts +0 -296
  189. package/src/routes/public/agent.ts +0 -1086
  190. package/src/routes/public/agents.ts +0 -373
  191. package/src/routes/public/channels.ts +0 -191
  192. package/src/routes/public/cli-auth.ts +0 -896
  193. package/src/routes/public/connections.ts +0 -574
  194. package/src/routes/public/landing.ts +0 -16
  195. package/src/routes/public/oauth.ts +0 -147
  196. package/src/routes/public/settings-auth.ts +0 -104
  197. package/src/routes/public/slack.ts +0 -173
  198. package/src/routes/shared/agent-ownership.ts +0 -101
  199. package/src/routes/shared/token-verifier.ts +0 -34
  200. package/src/services/bedrock-model-catalog.ts +0 -217
  201. package/src/services/bedrock-openai-service.ts +0 -658
  202. package/src/services/core-services.ts +0 -1072
  203. package/src/services/image-generation-service.ts +0 -257
  204. package/src/services/instruction-service.ts +0 -318
  205. package/src/services/mcp-registry.ts +0 -94
  206. package/src/services/platform-helpers.ts +0 -287
  207. package/src/services/session-manager.ts +0 -262
  208. package/src/services/settings-resolver.ts +0 -74
  209. package/src/services/system-config-resolver.ts +0 -89
  210. package/src/services/system-skills-service.ts +0 -229
  211. package/src/services/transcription-service.ts +0 -684
  212. package/src/session.ts +0 -110
  213. package/src/spaces/index.ts +0 -1
  214. package/src/spaces/space-resolver.ts +0 -17
  215. package/src/stores/in-memory-agent-store.ts +0 -403
  216. package/src/stores/redis-agent-store.ts +0 -279
  217. package/src/utils/public-url.ts +0 -44
  218. package/src/utils/rate-limiter.ts +0 -94
  219. package/tsconfig.json +0 -33
@@ -1,704 +0,0 @@
1
- /**
2
- * Scheduled Wake-Up Service
3
- *
4
- * Allows workers (Claude) to schedule future tasks that will wake them up.
5
- * Uses Redis for storage and BullMQ for delayed job processing.
6
- * Supports one-time delays (delayMinutes) and recurring schedules (cron expressions).
7
- */
8
-
9
- import { randomUUID } from "node:crypto";
10
- import { createLogger } from "@lobu/core";
11
- import { CronExpressionParser } from "cron-parser";
12
- import type { IMessageQueue, QueueJob } from "../infrastructure/queue";
13
-
14
- const logger = createLogger("scheduled-wakeup");
15
-
16
- // ============================================================================
17
- // Types
18
- // ============================================================================
19
-
20
- export interface ScheduledWakeup {
21
- id: string;
22
- deploymentName: string;
23
- conversationId: string;
24
- channelId: string;
25
- userId: string;
26
- agentId: string;
27
- teamId: string;
28
- platform: string;
29
- task: string;
30
- context?: Record<string, unknown>;
31
- scheduledAt: string; // ISO timestamp
32
- triggerAt: string; // ISO timestamp (next trigger time)
33
- status: "pending" | "triggered" | "cancelled";
34
- // Recurring fields
35
- cron?: string; // Cron expression (if recurring)
36
- iteration: number; // Current iteration (1-based, starts at 1)
37
- maxIterations: number; // Max iterations (default 1 for one-time, 10 for recurring)
38
- isRecurring: boolean; // Quick check flag
39
- unlimited?: boolean; // No iteration cap, TTL refreshed each iteration
40
- source?: string; // Caller identifier
41
- }
42
-
43
- export interface ScheduleParams {
44
- deploymentName: string;
45
- conversationId: string;
46
- channelId: string;
47
- userId: string;
48
- agentId: string;
49
- teamId: string;
50
- platform: string;
51
- task: string;
52
- context?: Record<string, unknown>;
53
- // ONE OF: delayMinutes OR cron (not both)
54
- delayMinutes?: number; // Minutes from now (one-time)
55
- cron?: string; // Cron expression (recurring)
56
- maxIterations?: number; // Max iterations for recurring (default 10)
57
- unlimited?: boolean; // Skip iteration cap, refresh TTL each iteration (for external/service schedules)
58
- source?: string; // Caller identifier (e.g., "owletto:watcher:123")
59
- }
60
-
61
- interface ScheduledJobPayload {
62
- scheduleId: string;
63
- deploymentName: string;
64
- conversationId: string;
65
- channelId: string;
66
- userId: string;
67
- agentId: string;
68
- teamId: string;
69
- platform: string;
70
- }
71
-
72
- // ============================================================================
73
- // Constants
74
- // ============================================================================
75
-
76
- const QUEUE_NAME = "scheduled_wakeups";
77
- const REDIS_KEY_PREFIX = "schedule:wakeup:";
78
- const REDIS_INDEX_PREFIX = "schedule:deployment:";
79
- const REDIS_AGENT_INDEX_PREFIX = "schedule:agent:";
80
-
81
- // Limits
82
- const MAX_PENDING_PER_DEPLOYMENT = 10;
83
- const MAX_DELAY_MINUTES = 1440; // 24 hours
84
- const SCHEDULE_TTL_SECONDS = 60 * 60 * 24 * 8; // 8 days (for recurring schedules)
85
- // Cron-specific limits
86
- const MIN_CRON_INTERVAL_MINUTES = 5; // Minimum 5 minutes between triggers
87
- const MAX_ITERATIONS = 100; // Maximum iterations for recurring
88
- const DEFAULT_RECURRING_ITERATIONS = 10; // Default max iterations for recurring
89
- const MAX_FIRST_TRIGGER_DAYS = 7; // First trigger must be within 7 days
90
-
91
- // ============================================================================
92
- // Module-level singleton reference
93
- // ============================================================================
94
-
95
- let scheduledWakeupServiceInstance: ScheduledWakeupService | undefined;
96
-
97
- /**
98
- * Set the global ScheduledWakeupService instance
99
- * Called by CoreServices after initialization
100
- */
101
- export function setScheduledWakeupService(
102
- service: ScheduledWakeupService
103
- ): void {
104
- scheduledWakeupServiceInstance = service;
105
- logger.debug("ScheduledWakeupService instance set");
106
- }
107
-
108
- /**
109
- * Get the global ScheduledWakeupService instance (if available)
110
- * Used by BaseDeploymentManager for cleanup
111
- */
112
- export function getScheduledWakeupService():
113
- | ScheduledWakeupService
114
- | undefined {
115
- return scheduledWakeupServiceInstance;
116
- }
117
-
118
- // ============================================================================
119
- // Service
120
- // ============================================================================
121
-
122
- export class ScheduledWakeupService {
123
- private queue: IMessageQueue;
124
- private isInitialized = false;
125
-
126
- constructor(queue: IMessageQueue) {
127
- this.queue = queue;
128
- }
129
-
130
- /**
131
- * Initialize the service - creates queue and starts worker
132
- */
133
- async start(): Promise<void> {
134
- await this.queue.createQueue(QUEUE_NAME);
135
-
136
- // Register worker to process delayed jobs
137
- await this.queue.work(
138
- QUEUE_NAME,
139
- async (job: QueueJob<ScheduledJobPayload>) => {
140
- await this.processScheduledJob(job);
141
- }
142
- );
143
-
144
- this.isInitialized = true;
145
- logger.debug("Scheduled wakeup service started");
146
- }
147
-
148
- /**
149
- * Schedule a future wakeup (one-time or recurring)
150
- */
151
- async schedule(params: ScheduleParams): Promise<ScheduledWakeup> {
152
- if (!this.isInitialized) {
153
- throw new Error("Scheduled wakeup service not initialized");
154
- }
155
-
156
- // Validate: must have either delayMinutes OR cron, not both
157
- if (params.delayMinutes && params.cron) {
158
- throw new Error(
159
- "Cannot specify both delayMinutes and cron - use one or the other"
160
- );
161
- }
162
- if (!params.delayMinutes && !params.cron) {
163
- throw new Error("Must specify either delayMinutes or cron");
164
- }
165
-
166
- const isRecurring = !!params.cron;
167
- let triggerAt: Date;
168
- let delayMs: number;
169
-
170
- if (params.cron) {
171
- // Validate and parse cron expression
172
- const cronValidation = this.validateCron(params.cron);
173
- if (!cronValidation.valid) {
174
- throw new Error(cronValidation.error);
175
- }
176
- triggerAt = cronValidation.firstTrigger!;
177
- delayMs = triggerAt.getTime() - Date.now();
178
- } else {
179
- // Validate delay
180
- if (
181
- params.delayMinutes! < 1 ||
182
- params.delayMinutes! > MAX_DELAY_MINUTES
183
- ) {
184
- throw new Error(
185
- `Delay must be between 1 and ${MAX_DELAY_MINUTES} minutes`
186
- );
187
- }
188
- triggerAt = new Date(Date.now() + params.delayMinutes! * 60 * 1000);
189
- delayMs = params.delayMinutes! * 60 * 1000;
190
- }
191
-
192
- // Validate maxIterations (unlimited skips the cap)
193
- const maxIterations = params.unlimited
194
- ? Number.MAX_SAFE_INTEGER
195
- : params.maxIterations
196
- ? Math.min(Math.max(1, params.maxIterations), MAX_ITERATIONS)
197
- : isRecurring
198
- ? DEFAULT_RECURRING_ITERATIONS
199
- : 1;
200
-
201
- // Check pending count limit
202
- const pending = await this.listPending(params.deploymentName);
203
- if (pending.length >= MAX_PENDING_PER_DEPLOYMENT) {
204
- throw new Error(
205
- `Maximum of ${MAX_PENDING_PER_DEPLOYMENT} pending schedules per deployment`
206
- );
207
- }
208
-
209
- const redis = this.queue.getRedisClient();
210
- const scheduleId = randomUUID();
211
- const now = new Date();
212
-
213
- const schedule: ScheduledWakeup = {
214
- id: scheduleId,
215
- deploymentName: params.deploymentName,
216
- conversationId: params.conversationId,
217
- channelId: params.channelId,
218
- userId: params.userId,
219
- agentId: params.agentId,
220
- teamId: params.teamId,
221
- platform: params.platform,
222
- task: params.task,
223
- context: params.context,
224
- scheduledAt: now.toISOString(),
225
- triggerAt: triggerAt.toISOString(),
226
- status: "pending",
227
- // Recurring fields
228
- cron: params.cron,
229
- iteration: 1,
230
- maxIterations,
231
- isRecurring,
232
- unlimited: params.unlimited,
233
- source: params.source,
234
- };
235
-
236
- // Store in Redis with TTL
237
- const redisKey = `${REDIS_KEY_PREFIX}${scheduleId}`;
238
- await redis.setex(redisKey, SCHEDULE_TTL_SECONDS, JSON.stringify(schedule));
239
-
240
- // Add to deployment index
241
- const deploymentIndexKey = `${REDIS_INDEX_PREFIX}${params.deploymentName}`;
242
- await redis.sadd(deploymentIndexKey, scheduleId);
243
- await redis.expire(deploymentIndexKey, SCHEDULE_TTL_SECONDS);
244
-
245
- // Add to agent index (for settings UI)
246
- const agentIndexKey = `${REDIS_AGENT_INDEX_PREFIX}${params.agentId}`;
247
- await redis.sadd(agentIndexKey, scheduleId);
248
- await redis.expire(agentIndexKey, SCHEDULE_TTL_SECONDS);
249
-
250
- // Create delayed job in BullMQ
251
- const jobPayload: ScheduledJobPayload = {
252
- scheduleId,
253
- deploymentName: params.deploymentName,
254
- conversationId: params.conversationId,
255
- channelId: params.channelId,
256
- userId: params.userId,
257
- agentId: params.agentId,
258
- teamId: params.teamId,
259
- platform: params.platform,
260
- };
261
-
262
- await this.queue.send(QUEUE_NAME, jobPayload, {
263
- delayMs,
264
- singletonKey: `schedule-${scheduleId}`,
265
- });
266
-
267
- logger.info(
268
- {
269
- scheduleId,
270
- deploymentName: params.deploymentName,
271
- triggerAt: triggerAt.toISOString(),
272
- isRecurring,
273
- cron: params.cron,
274
- maxIterations,
275
- },
276
- "Scheduled wakeup created"
277
- );
278
-
279
- return schedule;
280
- }
281
-
282
- /**
283
- * Schedule from an external service (no worker context needed).
284
- * Synthesizes deployment/conversation context from agentId.
285
- */
286
- async scheduleExternal(params: {
287
- agentId: string;
288
- task: string;
289
- context?: Record<string, unknown>;
290
- cron?: string;
291
- delayMinutes?: number;
292
- maxIterations?: number;
293
- source?: string;
294
- }): Promise<ScheduledWakeup> {
295
- return this.schedule({
296
- deploymentName: `external-${params.agentId.slice(0, 8)}`,
297
- conversationId: params.agentId,
298
- channelId: params.agentId,
299
- userId: "system",
300
- agentId: params.agentId,
301
- teamId: "external",
302
- platform: "api",
303
- task: params.task,
304
- context: params.context,
305
- cron: params.cron,
306
- delayMinutes: params.delayMinutes,
307
- maxIterations: params.maxIterations,
308
- unlimited: !!params.cron, // cron schedules from external are unlimited by default
309
- source: params.source,
310
- });
311
- }
312
-
313
- /**
314
- * Validate a cron expression and return first trigger time
315
- */
316
- private validateCron(cronExpr: string): {
317
- valid: boolean;
318
- error?: string;
319
- firstTrigger?: Date;
320
- } {
321
- try {
322
- const interval = CronExpressionParser.parse(cronExpr);
323
-
324
- // Get next two occurrences to check interval
325
- const first = interval.next().toDate();
326
- const second = interval.next().toDate();
327
-
328
- // Check minimum interval
329
- const intervalMs = second.getTime() - first.getTime();
330
- const intervalMinutes = intervalMs / (60 * 1000);
331
- if (intervalMinutes < MIN_CRON_INTERVAL_MINUTES) {
332
- return {
333
- valid: false,
334
- error: `Cron interval must be at least ${MIN_CRON_INTERVAL_MINUTES} minutes (got ${intervalMinutes.toFixed(1)} minutes)`,
335
- };
336
- }
337
-
338
- // Check first trigger is not too far in the future
339
- const daysUntilFirst =
340
- (first.getTime() - Date.now()) / (24 * 60 * 60 * 1000);
341
- if (daysUntilFirst > MAX_FIRST_TRIGGER_DAYS) {
342
- return {
343
- valid: false,
344
- error: `First trigger must be within ${MAX_FIRST_TRIGGER_DAYS} days (got ${daysUntilFirst.toFixed(1)} days)`,
345
- };
346
- }
347
-
348
- return { valid: true, firstTrigger: first };
349
- } catch (error) {
350
- return {
351
- valid: false,
352
- error: `Invalid cron expression: ${error instanceof Error ? error.message : String(error)}`,
353
- };
354
- }
355
- }
356
-
357
- /**
358
- * Cancel a scheduled wakeup
359
- */
360
- async cancel(scheduleId: string, deploymentName: string): Promise<boolean> {
361
- const redis = this.queue.getRedisClient();
362
- const redisKey = `${REDIS_KEY_PREFIX}${scheduleId}`;
363
-
364
- // Get current schedule
365
- const data = await redis.get(redisKey);
366
- if (!data) {
367
- return false;
368
- }
369
-
370
- const schedule: ScheduledWakeup = JSON.parse(data);
371
-
372
- // Verify ownership
373
- if (schedule.deploymentName !== deploymentName) {
374
- throw new Error("Schedule does not belong to this deployment");
375
- }
376
-
377
- // Update status to cancelled
378
- schedule.status = "cancelled";
379
- await redis.setex(redisKey, 60 * 60, JSON.stringify(schedule)); // Keep for 1 hour for auditing
380
-
381
- // Remove from indices
382
- const deploymentIndexKey = `${REDIS_INDEX_PREFIX}${deploymentName}`;
383
- await redis.srem(deploymentIndexKey, scheduleId);
384
-
385
- const agentIndexKey = `${REDIS_AGENT_INDEX_PREFIX}${schedule.agentId}`;
386
- await redis.srem(agentIndexKey, scheduleId);
387
-
388
- logger.info({ scheduleId, deploymentName }, "Scheduled wakeup cancelled");
389
- return true;
390
- }
391
-
392
- /**
393
- * List pending schedules for a deployment
394
- */
395
- async listPending(deploymentName: string): Promise<ScheduledWakeup[]> {
396
- const redis = this.queue.getRedisClient();
397
- const deploymentIndexKey = `${REDIS_INDEX_PREFIX}${deploymentName}`;
398
-
399
- const scheduleIds = await redis.smembers(deploymentIndexKey);
400
- const schedules: ScheduledWakeup[] = [];
401
-
402
- for (const scheduleId of scheduleIds) {
403
- try {
404
- const redisKey = `${REDIS_KEY_PREFIX}${scheduleId}`;
405
- const data = await redis.get(redisKey);
406
- if (data) {
407
- const schedule: ScheduledWakeup = JSON.parse(data);
408
- if (schedule.status === "pending") {
409
- schedules.push(schedule);
410
- }
411
- }
412
- } catch (error) {
413
- logger.warn(
414
- { scheduleId, deploymentName, error },
415
- "Failed to fetch schedule entry, skipping"
416
- );
417
- }
418
- }
419
-
420
- // Sort by trigger time
421
- schedules.sort(
422
- (a, b) =>
423
- new Date(a.triggerAt).getTime() - new Date(b.triggerAt).getTime()
424
- );
425
-
426
- return schedules;
427
- }
428
-
429
- /**
430
- * List pending schedules for an agent (used by settings UI)
431
- */
432
- async listPendingForAgent(agentId: string): Promise<ScheduledWakeup[]> {
433
- const redis = this.queue.getRedisClient();
434
- const agentIndexKey = `${REDIS_AGENT_INDEX_PREFIX}${agentId}`;
435
-
436
- const scheduleIds = await redis.smembers(agentIndexKey);
437
- const schedules: ScheduledWakeup[] = [];
438
-
439
- for (const scheduleId of scheduleIds) {
440
- try {
441
- const redisKey = `${REDIS_KEY_PREFIX}${scheduleId}`;
442
- const data = await redis.get(redisKey);
443
- if (data) {
444
- const schedule: ScheduledWakeup = JSON.parse(data);
445
- if (schedule.status === "pending") {
446
- schedules.push(schedule);
447
- }
448
- }
449
- } catch (error) {
450
- logger.warn(
451
- { scheduleId, agentId, error },
452
- "Failed to fetch schedule entry, skipping"
453
- );
454
- }
455
- }
456
-
457
- // Sort by trigger time
458
- schedules.sort(
459
- (a, b) =>
460
- new Date(a.triggerAt).getTime() - new Date(b.triggerAt).getTime()
461
- );
462
-
463
- return schedules;
464
- }
465
-
466
- /**
467
- * Cancel a schedule by ID (for settings UI - verifies agent ownership)
468
- */
469
- async cancelByAgent(scheduleId: string, agentId: string): Promise<boolean> {
470
- const redis = this.queue.getRedisClient();
471
- const redisKey = `${REDIS_KEY_PREFIX}${scheduleId}`;
472
-
473
- // Get current schedule
474
- const data = await redis.get(redisKey);
475
- if (!data) {
476
- return false;
477
- }
478
-
479
- const schedule: ScheduledWakeup = JSON.parse(data);
480
-
481
- // Verify agent ownership
482
- if (schedule.agentId !== agentId) {
483
- throw new Error("Schedule does not belong to this agent");
484
- }
485
-
486
- // Update status to cancelled
487
- schedule.status = "cancelled";
488
- await redis.setex(redisKey, 60 * 60, JSON.stringify(schedule));
489
-
490
- // Remove from indices
491
- const deploymentIndexKey = `${REDIS_INDEX_PREFIX}${schedule.deploymentName}`;
492
- await redis.srem(deploymentIndexKey, scheduleId);
493
-
494
- const agentIndexKey = `${REDIS_AGENT_INDEX_PREFIX}${agentId}`;
495
- await redis.srem(agentIndexKey, scheduleId);
496
-
497
- logger.info({ scheduleId, agentId }, "Scheduled wakeup cancelled by agent");
498
- return true;
499
- }
500
-
501
- /**
502
- * Clean up schedules when a deployment is deleted
503
- */
504
- async cleanupForDeployment(deploymentName: string): Promise<void> {
505
- const redis = this.queue.getRedisClient();
506
- const deploymentIndexKey = `${REDIS_INDEX_PREFIX}${deploymentName}`;
507
-
508
- const scheduleIds = await redis.smembers(deploymentIndexKey);
509
-
510
- for (const scheduleId of scheduleIds) {
511
- const redisKey = `${REDIS_KEY_PREFIX}${scheduleId}`;
512
- const data = await redis.get(redisKey);
513
- if (data) {
514
- const schedule: ScheduledWakeup = JSON.parse(data);
515
- // Remove from agent index
516
- const agentIndexKey = `${REDIS_AGENT_INDEX_PREFIX}${schedule.agentId}`;
517
- await redis.srem(agentIndexKey, scheduleId);
518
- }
519
- await redis.del(redisKey);
520
- }
521
-
522
- await redis.del(deploymentIndexKey);
523
-
524
- if (scheduleIds.length > 0) {
525
- logger.info(
526
- { deploymentName, count: scheduleIds.length },
527
- "Cleaned up schedules for deployment"
528
- );
529
- }
530
- }
531
-
532
- /**
533
- * Process a scheduled job when it triggers
534
- */
535
- private async processScheduledJob(
536
- job: QueueJob<ScheduledJobPayload>
537
- ): Promise<void> {
538
- const { scheduleId, deploymentName } = job.data;
539
-
540
- const redis = this.queue.getRedisClient();
541
- const redisKey = `${REDIS_KEY_PREFIX}${scheduleId}`;
542
-
543
- // Get schedule data
544
- const data = await redis.get(redisKey);
545
- if (!data) {
546
- logger.warn(
547
- { scheduleId },
548
- "Schedule not found - may have expired or been deleted"
549
- );
550
- return;
551
- }
552
-
553
- const schedule: ScheduledWakeup = JSON.parse(data);
554
-
555
- // Check if cancelled
556
- if (schedule.status === "cancelled") {
557
- logger.info({ scheduleId }, "Schedule was cancelled - skipping");
558
- return;
559
- }
560
-
561
- // Build the message to inject into the thread
562
- const contextStr = schedule.context
563
- ? `\n\nContext: ${JSON.stringify(schedule.context, null, 2)}`
564
- : "";
565
-
566
- // Include iteration info for recurring schedules
567
- const iterationInfo = schedule.isRecurring
568
- ? ` (iteration ${schedule.iteration} of ${schedule.maxIterations})`
569
- : "";
570
- const cronInfo = schedule.cron ? `\nSchedule: ${schedule.cron}` : "";
571
-
572
- const messageText = `[System] Scheduled reminder from yourself${iterationInfo}:
573
-
574
- Task: ${schedule.task}${contextStr}
575
-
576
- ---${cronInfo}
577
- Originally scheduled at: ${schedule.scheduledAt}
578
- Schedule ID: ${schedule.id}`;
579
-
580
- // Enqueue to the main messages queue (same as platform messages)
581
- await this.queue.send(
582
- "messages",
583
- {
584
- userId: schedule.userId,
585
- conversationId: schedule.conversationId,
586
- messageId: `scheduled-${scheduleId}-${schedule.iteration}`,
587
- channelId: schedule.channelId,
588
- teamId: schedule.teamId,
589
- agentId: schedule.agentId,
590
- botId: "system",
591
- platform: schedule.platform,
592
- messageText,
593
- platformMetadata: {
594
- isScheduledWakeup: true,
595
- scheduleId,
596
- iteration: schedule.iteration,
597
- maxIterations: schedule.maxIterations,
598
- isRecurring: schedule.isRecurring,
599
- },
600
- agentOptions: {},
601
- },
602
- {
603
- priority: 5, // Medium priority
604
- }
605
- );
606
-
607
- logger.info(
608
- {
609
- scheduleId,
610
- deploymentName,
611
- conversationId: schedule.conversationId,
612
- iteration: schedule.iteration,
613
- maxIterations: schedule.maxIterations,
614
- isRecurring: schedule.isRecurring,
615
- },
616
- "Scheduled wakeup triggered - message enqueued"
617
- );
618
-
619
- // Handle recurring: schedule next iteration or complete
620
- const hasIterationsLeft =
621
- schedule.unlimited || schedule.iteration < schedule.maxIterations;
622
- if (schedule.isRecurring && hasIterationsLeft && schedule.cron) {
623
- try {
624
- // Calculate next trigger from cron
625
- const interval = CronExpressionParser.parse(schedule.cron);
626
- const nextTrigger = interval.next().toDate();
627
- const delayMs = nextTrigger.getTime() - Date.now();
628
-
629
- const nextIteration = schedule.iteration + 1;
630
-
631
- // Persist iteration increment BEFORE enqueuing the next job.
632
- // This prevents duplicate processing if the process crashes
633
- // between enqueue and persist.
634
- schedule.iteration = nextIteration;
635
- schedule.triggerAt = nextTrigger.toISOString();
636
- await redis.setex(
637
- redisKey,
638
- SCHEDULE_TTL_SECONDS,
639
- JSON.stringify(schedule)
640
- );
641
-
642
- // Create next delayed job
643
- const jobPayload: ScheduledJobPayload = {
644
- scheduleId,
645
- deploymentName: schedule.deploymentName,
646
- conversationId: schedule.conversationId,
647
- channelId: schedule.channelId,
648
- userId: schedule.userId,
649
- agentId: schedule.agentId,
650
- teamId: schedule.teamId,
651
- platform: schedule.platform,
652
- };
653
-
654
- await this.queue.send(QUEUE_NAME, jobPayload, {
655
- delayMs,
656
- singletonKey: `schedule-${scheduleId}-${nextIteration}`,
657
- });
658
-
659
- logger.info(
660
- {
661
- scheduleId,
662
- nextIteration: schedule.iteration,
663
- nextTrigger: nextTrigger.toISOString(),
664
- },
665
- "Scheduled next recurring iteration"
666
- );
667
- } catch (error) {
668
- logger.error(
669
- { scheduleId, error },
670
- "Failed to schedule next recurring iteration"
671
- );
672
- // Mark as triggered (completed with error) and clean up
673
- schedule.status = "triggered";
674
- await redis.setex(redisKey, 60 * 60, JSON.stringify(schedule));
675
-
676
- const deploymentIndexKey = `${REDIS_INDEX_PREFIX}${deploymentName}`;
677
- await redis.srem(deploymentIndexKey, scheduleId);
678
- const agentIndexKey = `${REDIS_AGENT_INDEX_PREFIX}${schedule.agentId}`;
679
- await redis.srem(agentIndexKey, scheduleId);
680
- }
681
- } else {
682
- // One-time schedule or max iterations reached - mark as triggered and clean up
683
- schedule.status = "triggered";
684
- await redis.setex(redisKey, 60 * 60, JSON.stringify(schedule)); // Keep for 1 hour
685
-
686
- // Remove from indices
687
- const deploymentIndexKey = `${REDIS_INDEX_PREFIX}${deploymentName}`;
688
- await redis.srem(deploymentIndexKey, scheduleId);
689
-
690
- const agentIndexKey = `${REDIS_AGENT_INDEX_PREFIX}${schedule.agentId}`;
691
- await redis.srem(agentIndexKey, scheduleId);
692
-
693
- if (schedule.isRecurring) {
694
- logger.info(
695
- {
696
- scheduleId,
697
- completedIterations: schedule.iteration,
698
- },
699
- "Recurring schedule completed all iterations"
700
- );
701
- }
702
- }
703
- }
704
- }