lua-cli 3.43.0 → 3.45.0

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.
@@ -717,7 +717,7 @@ if (Lua.request.webhook) {
717
717
 
718
718
  **Properties:**
719
719
 
720
- - `Lua.request.channel` - Channel type (`'whatsapp'`, `'web'`, `'api'`)
720
+ - `Lua.request.channel` - Channel type (`'whatsapp'`, `'pop'` for the embeddable chat widget, `'phone'` for voice, `'web'` for the Lua web workspace (not the widget), `'api'`, and others; unrecognised channels arrive as plain strings)
721
721
  - `Lua.request.webhook` - Webhook info (if running in webhook)
722
722
  - `Lua.request.agentId` - The agent handling this turn
723
723
  - `Lua.request.orgId` - The agent's organization, on deployed runtimes that know it (absent offline in `lua test`
@@ -24,6 +24,13 @@ const result = await Agents.invoke('base_agent-other', { prompt: 'hi', threadId:
24
24
  - **The target knows who called.** The target's tools see `Lua.request.invokedBy = { agentId, supervisor, scopes, delegatedUserId? }`.
25
25
  - `supervisor` is true only when an org admin granted the caller `supervision:act` on the target.
26
26
  - A well-behaved target refuses writes unless `supervisor` is true.
27
+ - **`input.signal` cancels the call.** Optional `AbortSignal`. A signal that is already aborted rejects at once and sends no request. Aborting mid-flight rejects with `signal.reason` (an `AbortError` by default) and cancels the request, which stops the target's turn best-effort: a tool call already running on the target may still finish. Under `lua test` / `lua dev` the signal is honored by closing the HTTP request. A call without a signal is unchanged.
28
+
29
+ ```typescript
30
+ const controller = new AbortController();
31
+ setTimeout(() => controller.abort(), 10_000);
32
+ const result = await Agents.invoke('base_agent-other', { prompt: 'hi', signal: controller.signal });
33
+ ```
27
34
 
28
35
  ## Supervision
29
36
 
@@ -33,7 +40,7 @@ Supervision lets one agent observe, preview or release another agent in the same
33
40
 
34
41
  | Action | Allows |
35
42
  | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
36
- | `observe` | `logs` (without message bodies), `runs`, `data` (only the collections named on the grant), `versions.list/get/diff`, `persona.versions`, `source`, `env.keys`, `workflows.get` |
43
+ | `observe` | `logs` (without message bodies), `runs`, `data` (only the collections named on the grant), `versions.list/get/diff`, `persona.versions`, `source`, `env.keys`, `workflows.get`, `deployed` |
37
44
  | `observe-content` | Log message bodies in `logs` |
38
45
  | `preview` | `versions.preview`: chat a staged version |
39
46
  | `release` | `versions.promote`, `persona.activate`, `versions.delete` (a `superseded` version only), `workflows.create` (an inert, inactive workflow with no human author, so it cannot carry a schedule), `workflows.push` (a new, never-activated version of an existing workflow) and `workflows.deploy`. Promote, activate and deploy always need `expectActive`. Every release op runs only from a live workflow step of the supervisor whose workflow the grant names (`releaseWorkflowIds`). |
@@ -48,6 +55,10 @@ const errors = await Agents.logs('base_agent-tp', { level: 'error', limit: 20 })
48
55
  const runs = await Agents.runs('base_agent-tp').list({ status: 'failed' });
49
56
  const state = await Agents.data('base_agent-tp').get('pilot_state', 'config'); // collection named on the grant
50
57
  const keys = await Agents.env('base_agent-tp').keys(); // names only, never values
58
+ // What it serves right now, even with no promoted agent version (activeAgentVersion: null): live skills and tools,
59
+ // the published persona and processors, each with the version id an agent version snapshot would pin, and the
60
+ // live model (live.model; compare with a version's snapshot.model, since a promote applies it).
61
+ const live = await Agents.deployed('base_agent-tp');
51
62
 
52
63
  const versions = await Agents.versions('base_agent-tp').list();
53
64
  const diff = await Agents.versions('base_agent-tp').diff(7, 8);
@@ -64,12 +75,14 @@ await Agents.workflows('base_agent-tp').push('wf_123', definition); // the body
64
75
  // A workflow the target does not have yet: create it (inert), push, then deploy with expectActive: null.
65
76
  // const { workflowId } = await Agents.workflows('base_agent-tp').create('nightly-digest');
66
77
  await Agents.workflows('base_agent-tp').deploy('wf_123', '1.4.0', { expectActive: wf.activeVersionId });
78
+ // Optionally name the source commit the version was built from: deploy(wf, v, { expectActive, commitHash? })
67
79
 
68
80
  // Clean up: only a superseded version can be deleted.
69
81
  await Agents.versions('base_agent-tp').delete(5);
70
82
  ```
71
83
 
72
84
  - A workflow deploy also records a new active agent version. Read `versions.list` again before a later `promote`.
85
+ - `commitHash` on `workflows.deploy` is optional and forwarded unchanged. A call without it behaves as before, and the platform may fill it from the Job workspace.
73
86
 
74
87
  ### Platform switches
75
88
 
@@ -81,13 +94,14 @@ await Agents.versions('base_agent-tp').delete(5);
81
94
 
82
95
  ### What a supervisor can and cannot see
83
96
 
84
- | Surface | What is returned |
85
- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
86
- | Logs | Timestamp, level, log source and primitive or tool name. No user ids. The window is at most 7 days and at most 100 rows. The message (truncated to 2,000 characters) is included only with `observe-content`. |
87
- | Runs | Metadata only: id, workflow, status, reason, trigger and times. No inputs or outputs. |
88
- | Data | Read-only. Only the collections the org admin named on the grant (`dataCollections`). |
89
- | Env | Variable names only. |
90
- | Source | The files in the target's source backup, each up to 512 KB. Backup exclusions and credential-like paths are never listed or returned: dotfiles, `.env*`, `*.pem`, `*.key`, `id_rsa*`, `.npmrc`, `credentials*`, `*secret*`. |
97
+ | Surface | What is returned |
98
+ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
99
+ | Logs | Timestamp, level, log source and primitive or tool name. No user ids. The window is at most 7 days and at most 100 rows. The message (truncated to 2,000 characters) is included only with `observe-content`. |
100
+ | Runs | Metadata only: id, workflow, status, reason, trigger and times. No inputs or outputs. |
101
+ | Data | Read-only. Only the collections the org admin named on the grant (`dataCollections`). |
102
+ | Env | Variable names only. |
103
+ | Deployed | Live skills (tool names, bundle hashes, whether source is attached), the published persona (id, version, content hash) and processors, with version ids. No code, tool source or persona text. |
104
+ | Source | The files in the target's source backup, each up to 512 KB. Backup exclusions and credential-like paths are never listed or returned: dotfiles, `.env*`, `*.pem`, `*.key`, `id_rsa*`, `.npmrc`, `credentials*`, `*secret*`. |
91
105
 
92
106
  ### Guarantees
93
107
 
@@ -102,7 +116,7 @@ await Agents.versions('base_agent-tp').delete(5);
102
116
 
103
117
  - **Read members** call the developer API with **your** credential and mirror the projections above: env keys only, and logs without user ids or message bodies.
104
118
  - **Refused offline:** `versions.promote`, `persona.activate`, `versions.delete`, `workflows.create`, `workflows.push`, `workflows.deploy`, `versions.preview` and `data`. A local test never changes a live agent, and readable collections live on the grant.
105
- - **Not available offline:** `Agents.list`, `Agents.get` and `source.get`.
119
+ - **Not available offline:** `Agents.list`, `Agents.get`, `source.get` and `Agents.deployed` (`supervision_offline_unsupported`).
106
120
 
107
121
  ### Errors
108
122
 
@@ -32,10 +32,13 @@ The Baskets API enables:
32
32
  enum BasketStatus {
33
33
  ACTIVE = 'active',
34
34
  CHECKED_OUT = 'checked_out',
35
- ABANDONED = 'abandoned'
35
+ ABANDONED = 'abandoned',
36
+ EXPIRED = 'expired'
36
37
  }
37
38
  ```
38
39
 
40
+ `Baskets.get(status)` and `basket.updateStatus(status)` reject any other value (including a different letter case) with HTTP 400 `Invalid basket status '<value>'. Allowed: active, checked_out, abandoned, expired`, before anything is read or persisted. A missing basket is HTTP 404 and a failure inside the commerce service is a generic HTTP 500 (503 when it is unreachable). The SDK surfaces each as a thrown `Error`; there is no `success: false` body to branch on.
41
+
39
42
  ## BasketInstance Methods
40
43
 
41
44
  Once you have a basket instance, you can call these methods:
@@ -112,6 +115,13 @@ Updates the basket's status.
112
115
 
113
116
  **Returns:** `Promise<Basket>` - Updated basket data
114
117
 
118
+ **Errors:**
119
+ - An unknown status is rejected with HTTP 400 `Invalid basket status '<value>'. Allowed: active, checked_out, abandoned, expired`; nothing is persisted.
120
+ - A missing basket is HTTP 404.
121
+ - A failure inside the commerce service is a generic HTTP 500 (503 when the commerce service itself is down or unreachable).
122
+
123
+ The SDK surfaces each of these as a thrown `Error`; there is no `success: false` body to branch on.
124
+
115
125
  **Example:**
116
126
  ```typescript
117
127
  await basket.updateStatus(BasketStatus.ABANDONED);
@@ -11,6 +11,7 @@ import { LuaJob } from 'lua-cli';
11
11
  ## Overview
12
12
 
13
13
  Jobs enable:
14
+
14
15
  - Schedule tasks with cron expressions
15
16
  - Run one-time tasks at specific times
16
17
  - Execute tasks at fixed intervals
@@ -76,6 +77,7 @@ interface LuaJobConfig {
76
77
  ```
77
78
 
78
79
  **Cron Expression Format:**
80
+
79
81
  ```
80
82
  ┌───────────── minute (0 - 59)
81
83
  │ ┌───────────── hour (0 - 23)
@@ -87,6 +89,7 @@ interface LuaJobConfig {
87
89
  ```
88
90
 
89
91
  **Examples:**
92
+
90
93
  - `'0 9 * * *'` - Every day at 9:00 AM
91
94
  - `'0 */6 * * *'` - Every 6 hours
92
95
  - `'0 9 * * 1'` - Every Monday at 9:00 AM
@@ -131,6 +134,8 @@ interface JobInstance {
131
134
  }
132
135
  ```
133
136
 
137
+ > **`user.send()` from a job.** `user.send()` returns a boolean and never throws: `false` means the message was not delivered, and a bare `try/catch` never fires. Check the result (log it, or retry). It goes to the user's last-interaction channel; for a webchat-only user it lands in the user's base conversation thread and is pushed live to the widget, not into a specific workspace conversation. See [`user.send`](./User.md#usersendmessages) for the targeting rules.
138
+
134
139
  ## Complete Examples
135
140
 
136
141
  ### Daily Cleanup Job
@@ -144,19 +149,19 @@ export const dailyCleanup = new LuaJob({
144
149
 
145
150
  schedule: {
146
151
  type: 'cron',
147
- expression: '0 2 * * *', // 2 AM every day
148
- timezone: 'America/New_York'
152
+ expression: '0 2 * * *', // 2 AM every day
153
+ timezone: 'America/New_York',
149
154
  },
150
155
 
151
- timeout: 600, // 10 minutes
156
+ timeout: 600, // 10 minutes
152
157
 
153
158
  retry: {
154
159
  maxAttempts: 3,
155
- backoffSeconds: 60
160
+ backoffSeconds: 60,
156
161
  },
157
162
 
158
163
  metadata: {
159
- retentionDays: 30
164
+ retentionDays: 30,
160
165
  },
161
166
 
162
167
  execute: async (job) => {
@@ -180,15 +185,15 @@ export const dailyCleanup = new LuaJob({
180
185
  await job.updateMetadata({
181
186
  lastRun: new Date().toISOString(),
182
187
  deletedCount,
183
- nextRetentionCheck: new Date(cutoffDate).toISOString()
188
+ nextRetentionCheck: new Date(cutoffDate).toISOString(),
184
189
  });
185
190
 
186
191
  return {
187
192
  success: true,
188
193
  deletedCount,
189
- retentionDays
194
+ retentionDays,
190
195
  };
191
- }
196
+ },
192
197
  });
193
198
  ```
194
199
 
@@ -203,8 +208,8 @@ export const weeklyReport = new LuaJob({
203
208
 
204
209
  schedule: {
205
210
  type: 'cron',
206
- expression: '0 9 * * 1', // Every Monday at 9 AM
207
- timezone: 'UTC'
211
+ expression: '0 9 * * 1', // Every Monday at 9 AM
212
+ timezone: 'UTC',
208
213
  },
209
214
 
210
215
  execute: async (job) => {
@@ -215,30 +220,32 @@ export const weeklyReport = new LuaJob({
215
220
  const weekAgo = new Date();
216
221
  weekAgo.setDate(weekAgo.getDate() - 7);
217
222
 
218
- const recentActivities = activities.filter(a =>
219
- new Date(a.data.timestamp) > weekAgo
220
- );
223
+ const recentActivities = activities.filter((a) => new Date(a.data.timestamp) > weekAgo);
221
224
 
222
225
  // Generate AI summary
223
- const summary = await AI.generate(
224
- 'Summarize this week\'s activity in 2-3 bullet points.',
225
- [{
226
+ const summary = await AI.generate("Summarize this week's activity in 2-3 bullet points.", [
227
+ {
226
228
  type: 'text',
227
- text: JSON.stringify(recentActivities.map(a => a.data))
228
- }]
229
- );
229
+ text: JSON.stringify(recentActivities.map((a) => a.data)),
230
+ },
231
+ ]);
230
232
 
231
- // Send report to user
232
- await user.send([{
233
- type: 'text',
234
- text: `📊 Weekly Activity Report\n\n${summary}\n\nTotal activities: ${recentActivities.length}`
235
- }]);
233
+ // Send report to user. user.send() never throws: check the boolean result.
234
+ const delivered = await user.send([
235
+ {
236
+ type: 'text',
237
+ text: `📊 Weekly Activity Report\n\n${summary}\n\nTotal activities: ${recentActivities.length}`,
238
+ },
239
+ ]);
240
+ if (!delivered) {
241
+ console.warn('Weekly report was not delivered');
242
+ }
236
243
 
237
244
  return {
238
- success: true,
239
- activitiesCount: recentActivities.length
245
+ success: delivered,
246
+ activitiesCount: recentActivities.length,
240
247
  };
241
- }
248
+ },
242
249
  });
243
250
  ```
244
251
 
@@ -253,16 +260,16 @@ export const healthCheck = new LuaJob({
253
260
 
254
261
  schedule: {
255
262
  type: 'interval',
256
- seconds: 300 // Every 5 minutes
263
+ seconds: 300, // Every 5 minutes
257
264
  },
258
265
 
259
- timeout: 30, // 30 seconds
266
+ timeout: 30, // 30 seconds
260
267
 
261
268
  execute: async (job) => {
262
269
  const checks = {
263
270
  database: false,
264
271
  api: false,
265
- storage: false
272
+ storage: false,
266
273
  };
267
274
 
268
275
  // Check database
@@ -285,37 +292,42 @@ export const healthCheck = new LuaJob({
285
292
  try {
286
293
  await Data.create('health_check', {
287
294
  timestamp: new Date().toISOString(),
288
- status: 'ok'
295
+ status: 'ok',
289
296
  });
290
297
  checks.storage = true;
291
298
  } catch (error) {
292
299
  console.error('Storage check failed:', error);
293
300
  }
294
301
 
295
- const allHealthy = Object.values(checks).every(c => c);
302
+ const allHealthy = Object.values(checks).every((c) => c);
296
303
 
297
304
  // Log health status
298
305
  await Data.create('health_logs', {
299
306
  timestamp: new Date().toISOString(),
300
307
  checks,
301
- healthy: allHealthy
308
+ healthy: allHealthy,
302
309
  });
303
310
 
304
311
  // Alert if unhealthy
305
312
  if (!allHealthy) {
306
313
  const user = await job.user();
307
- await user.send([{
308
- type: 'text',
309
- text: `⚠️ System health check failed!\n${JSON.stringify(checks, null, 2)}`
310
- }]);
314
+ const alerted = await user.send([
315
+ {
316
+ type: 'text',
317
+ text: `⚠️ System health check failed!\n${JSON.stringify(checks, null, 2)}`,
318
+ },
319
+ ]);
320
+ if (!alerted) {
321
+ console.warn('Health alert was not delivered');
322
+ }
311
323
  }
312
324
 
313
325
  return {
314
326
  success: true,
315
327
  healthy: allHealthy,
316
- checks
328
+ checks,
317
329
  };
318
- }
330
+ },
319
331
  });
320
332
  ```
321
333
 
@@ -330,7 +342,7 @@ export const birthdayReminder = new LuaJob({
330
342
 
331
343
  schedule: {
332
344
  type: 'cron',
333
- expression: '0 9 * * *', // Every day at 9 AM
345
+ expression: '0 9 * * *', // Every day at 9 AM
334
346
  },
335
347
 
336
348
  execute: async (job) => {
@@ -352,21 +364,27 @@ export const birthdayReminder = new LuaJob({
352
364
  if (birthdayStr === todayStr) {
353
365
  // Send birthday message
354
366
  const user = await User.get(userEntry.data.userId);
355
- await user.send([{
356
- type: 'text',
357
- text: `🎉 Happy Birthday, ${userEntry.data.name}! Wishing you an amazing day!`
358
- }]);
359
-
360
- remindersSent++;
367
+ const sent = await user.send([
368
+ {
369
+ type: 'text',
370
+ text: `🎉 Happy Birthday, ${userEntry.data.name}! Wishing you an amazing day!`,
371
+ },
372
+ ]);
373
+
374
+ if (sent) {
375
+ remindersSent++;
376
+ } else {
377
+ console.warn(`Birthday message not delivered to ${userEntry.data.userId}`);
378
+ }
361
379
  }
362
380
  }
363
381
 
364
382
  return {
365
383
  success: true,
366
384
  remindersSent,
367
- date: today.toISOString()
385
+ date: today.toISOString(),
368
386
  };
369
- }
387
+ },
370
388
  });
371
389
  ```
372
390
 
@@ -382,17 +400,17 @@ export const dataSyncJob = new LuaJob({
382
400
 
383
401
  schedule: {
384
402
  type: 'interval',
385
- seconds: 3600 // Every hour
403
+ seconds: 3600, // Every hour
386
404
  },
387
405
 
388
406
  retry: {
389
407
  maxAttempts: 5,
390
- backoffSeconds: 300 // 5 minutes
408
+ backoffSeconds: 300, // 5 minutes
391
409
  },
392
410
 
393
411
  metadata: {
394
412
  lastSyncId: null,
395
- totalSynced: 0
413
+ totalSynced: 0,
396
414
  },
397
415
 
398
416
  execute: async (job) => {
@@ -402,8 +420,8 @@ export const dataSyncJob = new LuaJob({
402
420
  try {
403
421
  // Fetch updates since last sync
404
422
  const response = await axios.get(`${apiUrl}/updates`, {
405
- headers: { 'Authorization': `Bearer ${apiKey}` },
406
- params: { since: job.metadata.lastSyncId }
423
+ headers: { Authorization: `Bearer ${apiKey}` },
424
+ params: { since: job.metadata.lastSyncId },
407
425
  });
408
426
 
409
427
  const updates = response.data;
@@ -414,7 +432,7 @@ export const dataSyncJob = new LuaJob({
414
432
  await Data.create('synced_data', {
415
433
  externalId: update.id,
416
434
  data: update.data,
417
- syncedAt: new Date().toISOString()
435
+ syncedAt: new Date().toISOString(),
418
436
  });
419
437
  syncedCount++;
420
438
  }
@@ -424,19 +442,19 @@ export const dataSyncJob = new LuaJob({
424
442
  await job.updateMetadata({
425
443
  lastSyncId: lastId,
426
444
  totalSynced: job.metadata.totalSynced + syncedCount,
427
- lastSyncAt: new Date().toISOString()
445
+ lastSyncAt: new Date().toISOString(),
428
446
  });
429
447
 
430
448
  return {
431
449
  success: true,
432
450
  syncedCount,
433
- lastId
451
+ lastId,
434
452
  };
435
453
  } catch (error) {
436
454
  console.error('Sync failed:', error);
437
- throw error; // Will trigger retry
455
+ throw error; // Will trigger retry
438
456
  }
439
- }
457
+ },
440
458
  });
441
459
  ```
442
460
 
@@ -451,30 +469,37 @@ export const sendWelcomeEmail = new LuaJob({
451
469
 
452
470
  schedule: {
453
471
  type: 'once',
454
- executeAt: new Date(Date.now() + 3600000) // 1 hour from now
472
+ executeAt: new Date(Date.now() + 3600000), // 1 hour from now
455
473
  },
456
474
 
457
475
  metadata: {
458
476
  userId: 'user_123',
459
- userName: 'John Doe'
477
+ userName: 'John Doe',
460
478
  },
461
479
 
462
480
  execute: async (job) => {
463
481
  const user = await User.get(job.metadata.userId);
464
482
 
465
- await user.send([{
466
- type: 'text',
467
- text: `Welcome to our platform, ${job.metadata.userName}! 🎉\n\nWe're excited to have you here. Here are some things you can do:\n\n• Explore our features\n• Complete your profile\n• Connect with the community\n\nIf you have any questions, just ask!`
468
- }]);
483
+ const delivered = await user.send([
484
+ {
485
+ type: 'text',
486
+ text: `Welcome to our platform, ${job.metadata.userName}! 🎉\n\nWe're excited to have you here. Here are some things you can do:\n\n• Explore our features\n• Complete your profile\n• Connect with the community\n\nIf you have any questions, just ask!`,
487
+ },
488
+ ]);
489
+
490
+ if (!delivered) {
491
+ // Keep the job so it can be retried instead of losing the welcome message
492
+ return { success: false, userId: job.metadata.userId };
493
+ }
469
494
 
470
495
  // Delete job after execution (one-time only)
471
496
  await job.delete();
472
497
 
473
498
  return {
474
499
  success: true,
475
- userId: job.metadata.userId
500
+ userId: job.metadata.userId,
476
501
  };
477
- }
502
+ },
478
503
  });
479
504
  ```
480
505
 
@@ -505,34 +530,34 @@ export const sendWelcomeEmail = new LuaJob({
505
530
 
506
531
  ```typescript
507
532
  // Every minute
508
- '* * * * *'
533
+ '* * * * *';
509
534
 
510
535
  // Every 5 minutes
511
- '*/5 * * * *'
536
+ '*/5 * * * *';
512
537
 
513
538
  // Every hour at minute 0
514
- '0 * * * *'
539
+ '0 * * * *';
515
540
 
516
541
  // Every day at midnight
517
- '0 0 * * *'
542
+ '0 0 * * *';
518
543
 
519
544
  // Every day at 3:30 PM
520
- '30 15 * * *'
545
+ '30 15 * * *';
521
546
 
522
547
  // Every Monday at 9 AM
523
- '0 9 * * 1'
548
+ '0 9 * * 1';
524
549
 
525
550
  // Every weekday at 6 PM
526
- '0 18 * * 1-5'
551
+ '0 18 * * 1-5';
527
552
 
528
553
  // First day of every month
529
- '0 0 1 * *'
554
+ '0 0 1 * *';
530
555
 
531
556
  // Last day of every month (not directly supported, use workaround)
532
- '0 0 28-31 * *'
557
+ '0 0 28-31 * *';
533
558
 
534
559
  // Every 6 hours
535
- '0 */6 * * *'
560
+ '0 */6 * * *';
536
561
  ```
537
562
 
538
563
  ## Error Handling
@@ -544,12 +569,12 @@ export const robustJob = new LuaJob({
544
569
 
545
570
  schedule: {
546
571
  type: 'cron',
547
- expression: '0 * * * *' // Every hour
572
+ expression: '0 * * * *', // Every hour
548
573
  },
549
574
 
550
575
  retry: {
551
576
  maxAttempts: 3,
552
- backoffSeconds: 60
577
+ backoffSeconds: 60,
553
578
  },
554
579
 
555
580
  execute: async (job) => {
@@ -560,12 +585,12 @@ export const robustJob = new LuaJob({
560
585
  // Update success metadata
561
586
  await job.updateMetadata({
562
587
  lastSuccess: new Date().toISOString(),
563
- consecutiveFailures: 0
588
+ consecutiveFailures: 0,
564
589
  });
565
590
 
566
591
  return {
567
592
  success: true,
568
- result
593
+ result,
569
594
  };
570
595
  } catch (error) {
571
596
  // Track failures
@@ -573,21 +598,26 @@ export const robustJob = new LuaJob({
573
598
  await job.updateMetadata({
574
599
  lastFailure: new Date().toISOString(),
575
600
  lastError: error.message,
576
- consecutiveFailures: failures
601
+ consecutiveFailures: failures,
577
602
  });
578
603
 
579
604
  // Alert after 3 consecutive failures
580
605
  if (failures >= 3) {
581
606
  const user = await job.user();
582
- await user.send([{
583
- type: 'text',
584
- text: `⚠️ Job "${job.metadata.name}" has failed 3 times in a row. Last error: ${error.message}`
585
- }]);
607
+ const alerted = await user.send([
608
+ {
609
+ type: 'text',
610
+ text: `⚠️ Job "${job.metadata.name}" has failed 3 times in a row. Last error: ${error.message}`,
611
+ },
612
+ ]);
613
+ if (!alerted) {
614
+ console.warn('Failure alert was not delivered');
615
+ }
586
616
  }
587
617
 
588
- throw error; // Re-throw to trigger retry
618
+ throw error; // Re-throw to trigger retry
589
619
  }
590
- }
620
+ },
591
621
  });
592
622
  ```
593
623