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.
package/docs/api/User.md CHANGED
@@ -11,6 +11,7 @@ import { User } from 'lua-cli';
11
11
  ## Overview
12
12
 
13
13
  The User API allows you to:
14
+
14
15
  - Retrieve user profile data
15
16
  - Update user information
16
17
  - Send messages to users
@@ -24,11 +25,13 @@ The User API allows you to:
24
25
  Retrieves user data by userId, email, or phone.
25
26
 
26
27
  **Signature:**
28
+
27
29
  ```typescript
28
30
  async get(identifier?: string | UserLookupOptions): Promise<UserDataInstance | null>
29
31
  ```
30
32
 
31
33
  **Parameters:**
34
+
32
35
  - `identifier` (optional) - One of:
33
36
  - No parameter: Returns current user's data (in tools with conversation context)
34
37
  - `string`: User ID to retrieve
@@ -65,6 +68,7 @@ console.log(specificUser.email);
65
68
  ```
66
69
 
67
70
  **Use Cases:**
71
+
68
72
  - Fetch data for specific users in admin tools
69
73
  - Access user information in webhooks/jobs
70
74
  - Multi-user data operations
@@ -75,6 +79,7 @@ console.log(specificUser.email);
75
79
  Look up a user by email or phone number. This is particularly useful in webhooks where you don't have the user ID but know the user's contact information.
76
80
 
77
81
  **Signature:**
82
+
78
83
  ```typescript
79
84
  async get(options: { email?: string; phone?: string }): Promise<UserDataInstance | null>
80
85
  ```
@@ -104,6 +109,7 @@ if (!user) {
104
109
  ```
105
110
 
106
111
  **Use Cases:**
112
+
107
113
  - Webhooks that receive user email/phone from external systems
108
114
  - Admin tools where operators search by contact info
109
115
  - Integration with third-party systems that don't have Lua user IDs
@@ -113,6 +119,7 @@ if (!user) {
113
119
  Gets the conversation history for the current user.
114
120
 
115
121
  **Signature:**
122
+
116
123
  ```typescript
117
124
  async getChatHistory(): Promise<ChatHistoryMessage[]>
118
125
  ```
@@ -120,6 +127,7 @@ async getChatHistory(): Promise<ChatHistoryMessage[]>
120
127
  **Returns:** Array of chat messages
121
128
 
122
129
  **Example:**
130
+
123
131
  ```typescript
124
132
  const history = await User.getChatHistory();
125
133
 
@@ -155,17 +163,20 @@ user.data.email = 'new@email.com';
155
163
  Starting in v3.1.0, the user object includes a read-only `_luaProfile` property that provides secure access to core user identity data.
156
164
 
157
165
  **Properties:**
166
+
158
167
  - `userId` - The unique user identifier
159
168
  - `fullName` - User's full name
160
169
  - `mobileNumbers` - Array of mobile numbers
161
170
  - `emailAddresses` - Array of email addresses
162
171
 
163
172
  **Important:**
173
+
164
174
  - `_luaProfile` is **read-only** - attempts to modify it are silently ignored
165
175
  - Uses the `_lua` prefix to avoid conflicts with custom `user.profile` fields
166
176
  - `user.userId` is deprecated in favor of `user._luaProfile.userId`
167
177
 
168
178
  **Example:**
179
+
169
180
  ```typescript
170
181
  const user = await User.get();
171
182
 
@@ -189,11 +200,13 @@ user._luaProfile.userId = 'new-id'; // Silently ignored
189
200
  Updates user data.
190
201
 
191
202
  **Parameters:**
203
+
192
204
  - `data` (object) - Data to update
193
205
 
194
206
  **Returns:** Promise<object> - Updated user data
195
207
 
196
208
  **Example:**
209
+
197
210
  ```typescript
198
211
  const user = await User.get();
199
212
 
@@ -201,11 +214,11 @@ await user.update({
201
214
  preferences: {
202
215
  theme: 'dark',
203
216
  language: 'en',
204
- notifications: true
217
+ notifications: true,
205
218
  },
206
219
  metadata: {
207
- lastActive: new Date().toISOString()
208
- }
220
+ lastActive: new Date().toISOString(),
221
+ },
209
222
  });
210
223
  ```
211
224
 
@@ -213,33 +226,41 @@ await user.update({
213
226
 
214
227
  Sends messages to the user.
215
228
 
229
+ **Where it lands:** `user.send()` targets the user's last-interaction channel (WhatsApp, Teams, SMS and so on). For a user who only chats on webchat, it writes the message to the user's base conversation thread (the `${userId}-${agentId}` root thread) and pushes it live to the webchat widget. It does **not** target a specific workspace conversation; there is no conversation or thread option.
230
+
216
231
  **Parameters:**
232
+
217
233
  - `messages` (ChatMessage[]) - Array of messages to send
218
234
 
219
- **Returns:** Promise<boolean> - `true` only when the platform confirmed delivery, `false` otherwise (including an unconfirmed result). It never throws, so check the result instead of using try/catch.
235
+ **Returns:** Promise<boolean> - `true` only when the platform confirmed delivery, `false` otherwise (including an unconfirmed result). It never throws, so a bare `try/catch` never fires: check the result instead (in a job, log it or retry on `false`). For a message that must be seen with certainty, use `User.Inbox.push`, which is receipted.
220
236
 
221
237
  **Example:**
238
+
222
239
  ```typescript
223
240
  const user = await User.get();
224
241
 
225
242
  // Send text message
226
- await user.send([{
227
- type: 'text',
228
- text: 'Hello! Your order has shipped.'
229
- }]);
243
+ await user.send([
244
+ {
245
+ type: 'text',
246
+ text: 'Hello! Your order has shipped.',
247
+ },
248
+ ]);
230
249
 
231
250
  // Send image
232
- await user.send([{
233
- type: 'image',
234
- imageUrl: 'https://example.com/product.jpg',
235
- altText: 'Product image'
236
- }]);
251
+ await user.send([
252
+ {
253
+ type: 'image',
254
+ imageUrl: 'https://example.com/product.jpg',
255
+ altText: 'Product image',
256
+ },
257
+ ]);
237
258
 
238
259
  // Send multiple messages
239
260
  await user.send([
240
261
  { type: 'text', text: 'Your order summary:' },
241
262
  { type: 'text', text: '- Item 1: $10' },
242
- { type: 'text', text: 'Total: $10' }
263
+ { type: 'text', text: 'Total: $10' },
243
264
  ]);
244
265
  ```
245
266
 
@@ -250,6 +271,7 @@ Clears all user data.
250
271
  **Returns:** `Promise<{ success: boolean }>` - `{ success: true }` once the data is cleared. Throws if the clear fails.
251
272
 
252
273
  **Example:**
274
+
253
275
  ```typescript
254
276
  const user = await User.get();
255
277
  await user.clear();
@@ -262,6 +284,7 @@ Saves current user data state.
262
284
  **Returns:** Promise<boolean>
263
285
 
264
286
  **Example:**
287
+
265
288
  ```typescript
266
289
  const user = await User.get();
267
290
  user.data.lastLogin = new Date().toISOString();
@@ -275,12 +298,13 @@ Gets chat history for this user.
275
298
  **Returns:** Promise<ChatHistoryMessage[]>
276
299
 
277
300
  **Example:**
301
+
278
302
  ```typescript
279
303
  const user = await User.get();
280
304
  const history = await user.getChatHistory();
281
305
 
282
306
  // Analyze conversation
283
- const userMessages = history.filter(m => m.role === 'user');
307
+ const userMessages = history.filter((m) => m.role === 'user');
284
308
  console.log(`User sent ${userMessages.length} messages`);
285
309
  ```
286
310
 
@@ -295,32 +319,32 @@ import { z } from 'zod';
295
319
  export default class UpdatePreferencesTool implements LuaTool {
296
320
  name = 'update_preferences';
297
321
  description = 'Update user preferences';
298
-
322
+
299
323
  inputSchema = z.object({
300
324
  theme: z.enum(['light', 'dark']).optional(),
301
325
  language: z.string().optional(),
302
- notifications: z.boolean().optional()
326
+ notifications: z.boolean().optional(),
303
327
  });
304
-
328
+
305
329
  async execute(input: z.infer<typeof this.inputSchema>) {
306
330
  const user = await User.get();
307
-
331
+
308
332
  // Get current preferences
309
333
  const currentPrefs = user.data.preferences || {};
310
-
334
+
311
335
  // Merge with new preferences
312
336
  const newPrefs = {
313
337
  ...currentPrefs,
314
- ...input
338
+ ...input,
315
339
  };
316
-
340
+
317
341
  // Update
318
342
  await user.update({ preferences: newPrefs });
319
-
343
+
320
344
  return {
321
345
  success: true,
322
346
  message: 'Preferences updated',
323
- preferences: newPrefs
347
+ preferences: newPrefs,
324
348
  };
325
349
  }
326
350
  }
@@ -335,36 +359,38 @@ import { z } from 'zod';
335
359
  export default class NotifyUserTool implements LuaTool {
336
360
  name = 'notify_user';
337
361
  description = 'Send a notification to the user';
338
-
362
+
339
363
  inputSchema = z.object({
340
364
  title: z.string(),
341
365
  message: z.string(),
342
- urgent: z.boolean().optional()
366
+ urgent: z.boolean().optional(),
343
367
  });
344
-
368
+
345
369
  async execute(input: z.infer<typeof this.inputSchema>) {
346
370
  const user = await User.get();
347
-
371
+
348
372
  const prefix = input.urgent ? '🚨 URGENT: ' : '📬 ';
349
373
  const fullMessage = `${prefix}${input.title}\n\n${input.message}`;
350
-
351
- await user.send([{
352
- type: 'text',
353
- text: fullMessage
354
- }]);
355
-
374
+
375
+ await user.send([
376
+ {
377
+ type: 'text',
378
+ text: fullMessage,
379
+ },
380
+ ]);
381
+
356
382
  // Log the notification
357
383
  await user.update({
358
384
  lastNotification: {
359
385
  title: input.title,
360
386
  sentAt: new Date().toISOString(),
361
- urgent: input.urgent || false
362
- }
387
+ urgent: input.urgent || false,
388
+ },
363
389
  });
364
-
390
+
365
391
  return {
366
392
  success: true,
367
- message: 'Notification sent'
393
+ message: 'Notification sent',
368
394
  };
369
395
  }
370
396
  }
@@ -379,35 +405,31 @@ import { z } from 'zod';
379
405
  export default class ChatAnalyticsTool implements LuaTool {
380
406
  name = 'analyze_conversation';
381
407
  description = 'Analyze the conversation history';
382
-
408
+
383
409
  inputSchema = z.object({});
384
-
410
+
385
411
  async execute(input: z.infer<typeof this.inputSchema>) {
386
412
  const history = await User.getChatHistory();
387
-
413
+
388
414
  const stats = {
389
415
  totalMessages: history.length,
390
- userMessages: history.filter(m => m.role === 'user').length,
391
- assistantMessages: history.filter(m => m.role === 'assistant').length,
416
+ userMessages: history.filter((m) => m.role === 'user').length,
417
+ assistantMessages: history.filter((m) => m.role === 'assistant').length,
392
418
  averageLength: 0,
393
- topics: [] as string[]
419
+ topics: [] as string[],
394
420
  };
395
-
421
+
396
422
  // Calculate average message length
397
- const lengths = history.map(m =>
398
- m.content.reduce((sum, c) => sum + (c.text?.length || 0), 0)
399
- );
400
- stats.averageLength = Math.round(
401
- lengths.reduce((a, b) => a + b, 0) / lengths.length
402
- );
403
-
423
+ const lengths = history.map((m) => m.content.reduce((sum, c) => sum + (c.text?.length || 0), 0));
424
+ stats.averageLength = Math.round(lengths.reduce((a, b) => a + b, 0) / lengths.length);
425
+
404
426
  return {
405
427
  success: true,
406
428
  analytics: stats,
407
- recentMessages: history.slice(-5).map(m => ({
429
+ recentMessages: history.slice(-5).map((m) => ({
408
430
  role: m.role,
409
- preview: m.content[0]?.text?.substring(0, 50) + '...'
410
- }))
431
+ preview: m.content[0]?.text?.substring(0, 50) + '...',
432
+ })),
411
433
  };
412
434
  }
413
435
  }
@@ -416,6 +438,7 @@ export default class ChatAnalyticsTool implements LuaTool {
416
438
  ## Message Types
417
439
 
418
440
  ### Text Message
441
+
419
442
  ```typescript
420
443
  {
421
444
  type: 'text',
@@ -424,6 +447,7 @@ export default class ChatAnalyticsTool implements LuaTool {
424
447
  ```
425
448
 
426
449
  ### Image Message
450
+
427
451
  ```typescript
428
452
  {
429
453
  type: 'image',
@@ -433,6 +457,7 @@ export default class ChatAnalyticsTool implements LuaTool {
433
457
  ```
434
458
 
435
459
  ### File Message
460
+
436
461
  ```typescript
437
462
  {
438
463
  type: 'file',
@@ -445,12 +470,14 @@ export default class ChatAnalyticsTool implements LuaTool {
445
470
  ## Best Practices
446
471
 
447
472
  ### ✅ DO
473
+
448
474
  - Always handle missing user data gracefully
449
475
  - Use `user.update()` instead of modifying and saving separately
450
476
  - Check if data exists before accessing nested properties
451
477
  - Send clear, actionable messages to users
452
478
 
453
479
  ### ❌ DON'T
480
+
454
481
  - Don't assume user data structure
455
482
  - Don't modify user data without validation
456
483
  - Don't send empty or malformed messages
@@ -479,7 +506,7 @@ const user = await User.get();
479
506
  await user.update({
480
507
  name: 'John Doe',
481
508
  email: 'john@example.com',
482
- preferences: { theme: 'dark' }
509
+ preferences: { theme: 'dark' },
483
510
  });
484
511
 
485
512
  // ❌ Multiple separate updates (slower)
@@ -492,16 +519,20 @@ await user.update({ preferences: { theme: 'dark' } });
492
519
 
493
520
  ```typescript
494
521
  // ✅ Clear, formatted message
495
- await user.send([{
496
- type: 'text',
497
- text: `Order Confirmation\n\nOrder ID: ${orderId}\nTotal: $${total}\n\nThank you for your order!`
498
- }]);
522
+ await user.send([
523
+ {
524
+ type: 'text',
525
+ text: `Order Confirmation\n\nOrder ID: ${orderId}\nTotal: $${total}\n\nThank you for your order!`,
526
+ },
527
+ ]);
499
528
 
500
529
  // ❌ Unformatted dump
501
- await user.send([{
502
- type: 'text',
503
- text: JSON.stringify(orderData)
504
- }]);
530
+ await user.send([
531
+ {
532
+ type: 'text',
533
+ text: JSON.stringify(orderData),
534
+ },
535
+ ]);
505
536
  ```
506
537
 
507
538
  ## Webhook Example: User Lookup by Phone
@@ -514,30 +545,30 @@ import { LuaWebhook, User } from 'lua-cli';
514
545
  export default new LuaWebhook({
515
546
  name: 'update-routing-preference',
516
547
  description: 'Update user routing preference by phone number',
517
-
548
+
518
549
  execute: async (event) => {
519
550
  const { phone, routingEnabled } = event.body;
520
-
551
+
521
552
  if (!phone) {
522
553
  return { error: 'Phone number is required' };
523
554
  }
524
-
555
+
525
556
  // Look up user by phone
526
557
  const user = await User.get({ phone });
527
558
  if (!user) {
528
559
  return { error: 'User not found' };
529
560
  }
530
-
561
+
531
562
  // Update routing preference
532
563
  user.routingEnabled = routingEnabled;
533
564
  await user.save();
534
-
535
- return {
536
- success: true,
565
+
566
+ return {
567
+ success: true,
537
568
  userId: user._luaProfile.userId,
538
- routingEnabled
569
+ routingEnabled,
539
570
  };
540
- }
571
+ },
541
572
  });
542
573
  ```
543
574
 
@@ -564,4 +595,3 @@ interface UserLookupOptions {
564
595
 
565
596
  - [User Data Tool Example](../../template/src/skills/tools/UserDataTool.ts)
566
597
  - [Notification Patterns](../guides/NotificationPatterns.md)
567
-
@@ -76,7 +76,7 @@ Returns the run summary (status, counts, gate, lineage, `output` when readable).
76
76
 
77
77
  ### `Workflows.list(opts?)`
78
78
 
79
- `opts`: `{ limit?, status?, workflow?, correlationKey?, tags?, sort? }` — `tags` are AND-ed; `sort` is one of `-createdAt | createdAt | -durationMs | durationMs`.
79
+ `opts`: `{ limit?, status?, workflow?, correlationKey?, tags?, sort? }` — `tags` are AND-ed; `sort` is one of `-createdAt | createdAt | -durationMs | durationMs`. `workflow` is a name or an id; an id (UUID, `wf_…`, `wfc_…`) is queried as-is. Under `lua test` a name that matches no workflow definition returns `[]`, and a failed definition lookup throws a typed `WorkflowApiError`; deployed, the bridge matches runs by recorded name or id. A `workflow` filter never widens to every run in either runtime.
80
80
 
81
81
  ### `Workflows.cancel(runId, opts?)`
82
82
 
@@ -90,6 +90,12 @@ Resumes a step suspended with `ctx.suspend(...)`. The loser of a resume race rec
90
90
 
91
91
  Delivers a named signal to a run waiting on `waitForSignal(name)`. `opts.dedupeKey` makes a re-send a no-op (`{ accepted: true, duplicate: true }`). Returns `{ accepted, reason?: 'source_not_accepted' | 'duplicate' | 'parked' }`. The waiting step completes with `{ payload, source: { kind, id, principalId? }, signalId, receivedAt }` — read the payload as `stepResults.<id>.payload`.
92
92
 
93
+ ### `Workflows.raiseBudget(runId, patch)`
94
+
95
+ Raises the caps of one of the calling agent's runs. `patch` is `{ maxCredits?, maxSteps?, maxJobSeconds?, maxDurationSeconds?, note? }` — at least one cap, increases only, each at or under the org cap. Returns `{ raised: true, budget, runStatus, resumed }`, or the idempotent `{ raised: false, reason: 'already_raised' }` when the same raise was already applied.
96
+
97
+ Only a run parked on its budget gate, or still queued/running, is raisable; any other state (including a terminal run, or a run past its per-run raise limit) throws `BUDGET_NOT_RAISABLE` (409). Only the run's creator/holder or an org admin may raise it (`NOT_RUN_CREATOR`, 403 — so a call from a job or workflow step with no user fails closed). A run that is not the calling agent's throws `RUN_NOT_FOUND` (404). An over-cap value throws `CAP_EXCEEDED` (400, with `cap`, `value`, `ceiling`); a malformed patch throws `VALIDATION_FAILED` (400). Behaviour is the same under `lua test` and deployed.
98
+
93
99
  #### Reserved payload keys
94
100
 
95
101
  The run ledger redacts by **key name**, not by value: a payload member whose key is one of
@@ -100,14 +106,16 @@ is persisted as `[REDACTED]` whatever it holds. A key matches when one of those
100
106
 
101
107
  This is by design (a credential must never land in a run), so the placeholder is what `stepResults.<id>.payload`, the run output, `lua workflows status --steps`, exports and the chat run card show — a workflow cannot read a data field with one of these names back. Name data fields otherwise (`ticketRef`, `handle`, `code`, `verifier`). The same rule applies to every step output and suspend payload. A `waitForSignal` whose `schema` declares a property with a reserved name draws the non-blocking `signal-reserved-key` warning at push / compose (the property, the placeholder it would read as, the rename), and `lua workflows signal` warns before sending a payload that carries a reserved key (the signal is still delivered).
102
108
 
103
- ### Reserved members
109
+ ### Other members
104
110
 
105
- `startBatch`, `signalByKey`, `raiseBudget`, `setGoal` and `goals.{list,get,pause,resume,close}` are part of the API surface (the member list is frozen) and throw `WorkflowApiError { code: 'WORKFLOWS_API_UNAVAILABLE' }` until their server routes ship. For `startBatch`: from an agent turn, prefer the `startWorkflowRunBatch` tool (one consent card for the whole batch, D23-r5).
111
+ `startBatch`, `signalByKey`, `setGoal` and `goals.{list,get,pause,resume,close}` are part of the API surface (the member list is frozen). For `startBatch`: from an agent turn, prefer the `startWorkflowRunBatch` tool (one consent card for the whole batch, D23-r5).
106
112
 
107
113
  ## Errors
108
114
 
109
115
  All failures throw `WorkflowApiError` with `code` (the server discriminator — `RUNS_IN_FLIGHT`, `SUSPENDED_CAP`, `WORKFLOW_NOT_FOUND`, `CONTROL_UNAVAILABLE`, …), `statusCode`, and any extra fields the server returned (for example `blockingRunId`).
110
116
 
117
+ - `WORKFLOW_INACTIVE` (409): the workflow was deactivated (`lua workflows deactivate`), so `Workflows.start` by name or by id starts no new run. Runs already going keep going; activate the workflow to start again.
118
+
111
119
  ```typescript
112
120
  try {
113
121
  await Workflows.start('nightly-sync', {});
@@ -130,7 +138,7 @@ lua test workflow outreach --input @leads.json \
130
138
 
131
139
  - `--step-output <id>=@file|<json>` completes a step without running it (validated against its `outputSchema`) — this is how a predicate over an agent output gets both truth values under the default `--agents fake`.
132
140
  - `--approve <id>[=@payload]` / `--deny <id>[=@reason]` pre-answer approvals; `--signal <name>=<json>` pre-supplies `waitForSignal` payloads in order.
133
- - `--fixtures <dir>` replays recorded agent/tool outputs (`--record <dir>` writes them); a missing fixture exits **5** — never a silent fake.
141
+ - `--fixtures <dir>` replays recorded fixtures (`--record <dir>` writes each live agent reply — requires `--agents live`; tool steps never run offline, so nothing else is recorded); a missing fixture exits **5** — never a silent fake.
134
142
  - `--from-run <runId>` seeds every completed step from a real run and restarts at the first non-completed one.
135
143
  - `--ledger-out <file>` writes the in-memory ledger; `lua workflows replay <runId> --local` re-evaluates a real run's predicates and mappings against your compiled artifact and reports `LEDGER_DIVERGENCE`.
136
144
  - `--park <id>` simulates a platform-fault park of a `sideEffects:'external'` step (retry / skip / complete / fail, same verbs as production); `--fast-retries` collapses retry backoff to 0; `--real-time` actually waits instead of fast-forwarding the virtual clock; `--artefacts-dir <dir>` backs `ctx.artefacts.*` on disk.
@@ -46,6 +46,8 @@ Offline, `lua workflows run --approve <id>` completes the node with `decidedBy:
46
46
 
47
47
  `timeoutHours` runs on **business time** when `businessHours: { tz, calendar }` is set. `onTimeout` is a chain: each `{ escalateTo, timeoutHours }` hop re-prompts the next audience; the terminal `'deny'` (or `'cancel-run'`/`'continue'`) completes the node — e.g. `{approved:false, timedOut:true, escalations:2}`.
48
48
 
49
+ A numeric `timeoutHours` (on `approval` and `waitForSignal`) must be in 1..720: outside that range `lua compile` and `lua push` refuse it as `timeout-hours-out-of-range`. A `waitForSignal` signal name must match `[a-zA-Z0-9_.-]{1,64}` or it is refused as `signal-name-invalid`. The run's own wall (7 days by default) still caps the live deadline, so a window longer than the run wall is clamped at runtime (the `deadline-clamped` warning).
50
+
49
51
  ## Reminders
50
52
 
51
53
  `remindAfterHours: 24` re-sends the approval push to the approvers who still have to decide, every 24 hours while the approval is pending: at most **3 reminders** per ask (an escalation hop is a new ask, so its count starts again), and never at or after the deadline. The reminder replaces the earlier notification in place (same collapse key) and reads "Reminder: …". It takes a plain number of hours in `1..720` (no binding); anything else is refused as `remind-after-invalid`, and a value at or above the first deadline pushes with the `remind-after-exceeds-timeout` warning, because it can never fire. The first deadline is the first escalation hop's `timeoutHours` when `onTimeout` starts with a hop, otherwise `timeoutHours` (168 hours when you leave it out). Reminders ride the approval **push**, so they reach the approvers an agent's approval routing names (shared-agent approvers); an approval with no routing gets the inbox row and the channel fallback but no push, and so no reminder. They are sent on the platform's sweep cadence, so a reminder can arrive a few minutes after it is due, and quiet hours and do-not-disturb drop it like any other push.
@@ -8,7 +8,7 @@ _Source of truth: workflows-spec 05 §5.17 (D19-r1/r2). This page is the develop
8
8
 
9
9
  `workspace` on `createWorkflow` declares the checkout: `{ kind:'git', repo, ref, credentialsRef, sizeGb?, verify? }` or `{ kind:'empty' }`. Steps mount it with `workspace: { mount: 'rw'|'ro', isolation?: 'worktree' }` and read `ctx.workspace = { root, mount, branch, headSha, isolation, baseSha?, arm?, backend? }` — `root` is the absolute directory of the checkout (`/workspace`); `baseSha` is the commit the run's base ref resolved to at provision (`headSha` is where this step's checkout is), `arm` the worktree arm id under `isolation:'worktree'`, `backend` the volume backend — the three are present when the pod hands the run's stamps to the step and absent otherwise (an `empty` workspace, a shared mount). The pre-3.33 name `path` is served by the Job pod as a deprecated alias for one minor (it warns once per step) and then removed; new code reads `root`.
10
10
 
11
- `ttlHours` is how long a released or held volume is kept before it expires: 72 h by default, 24 h for a completed `keepArtefacts` volume. The platform has a **168 h** ceiling on it, a platform setting that applies once the platform enables it: a larger value is then refused at `lua push` with `workspace-ttl-exceeds-cap` (a non-positive or non-numeric one with `workspace-ttl-invalid`) and held volumes are clamped to 168 h. Until then `lua compile` does not check it, so do not rely on a long hold.
11
+ `ttlHours` is how long a released or held volume is kept before it expires: 72 h by default, 24 h for a completed `keepArtefacts` volume. The platform has a **168 h** ceiling on it, a platform setting that applies once the platform enables it: a larger value is then refused at `lua push` — and likewise at compose and dynamic-definition validation — with `workspace-ttl-exceeds-cap` (a non-positive or non-numeric one with `workspace-ttl-invalid`) and held volumes are clamped to 168 h. Until then `lua compile` does not check it, so do not rely on a long hold.
12
12
 
13
13
  `credentialsRef` is a connection id **or a key you declare under `connections: [{ key, integrationType }]`** — declare a key; it resolves against the owner agent's own connections on any agent at run time (see [Git credentials](./git-credentials.md)), so the definition never freezes a production id.
14
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lua-cli",
3
- "version": "3.43.0",
3
+ "version": "3.45.0",
4
4
  "description": "Build, test, and deploy AI agents with custom tools, webhooks, and scheduled jobs. Features LuaAgent unified configuration, streaming chat, and batch deployment.",
5
5
  "readmeFilename": "README.md",
6
6
  "main": "dist/api-exports.js",
@@ -87,7 +87,7 @@
87
87
  "dotenv": "^17.2.3",
88
88
  "esbuild": "^0.28.1",
89
89
  "inquirer": "^12.9.6",
90
- "js-yaml": "^4.1.0",
90
+ "js-yaml": "^4.3.2",
91
91
  "open": "^10.1.0",
92
92
  "posthog-node": "^5.26.0",
93
93
  "ts-evaluator": "^2.0.0",
@@ -114,11 +114,11 @@
114
114
  "stripe": "^19.2.0",
115
115
  "ts-node": "^10.9.2",
116
116
  "tsup": "^8.5.1",
117
- "@lua/shared-source-sync": "0.0.1",
117
+ "@lua/sandbox-runtime": "0.0.1",
118
118
  "@lua/shared-sandbox": "0.0.1",
119
+ "@lua/shared-source-sync": "0.0.1",
119
120
  "@lua/shared-types": "0.0.1",
120
- "@lua/workflow-graph": "0.0.1",
121
- "@lua/sandbox-runtime": "0.0.1"
121
+ "@lua/workflow-graph": "0.0.1"
122
122
  },
123
123
  "scripts": {
124
124
  "clean": "rm -rf dist temp",
@@ -1,16 +1,17 @@
1
- import { PostProcessor, UserDataInstance } from 'lua-cli';
1
+ import { PostProcessor, UserDataInstance, PostProcessorMessage, messageText } from 'lua-cli';
2
2
 
3
3
  const modifyResponsePostProcessor = new PostProcessor({
4
4
  name: 'modify-response',
5
5
  description: 'Modifies the response to the user',
6
- execute: async (user: UserDataInstance, message: string, response: string, channel: string) => {
6
+ execute: async (user: UserDataInstance, message: PostProcessorMessage, response: string, channel: string) => {
7
+ // `message` is a string or the message's parts; messageText() gives its text either way.
7
8
  console.log('Modify response post processor', user, message, response, channel);
8
9
  console.log('User data', await user.data);
9
10
  console.log('Message', message);
10
11
  console.log('Response', response);
11
12
  console.log('Channel', channel);
12
13
  if (response.includes('test')) {
13
- return { modifiedResponse: message.toUpperCase() };
14
+ return { modifiedResponse: messageText(message).toUpperCase() };
14
15
  }
15
16
  return { modifiedResponse: response };
16
17
  },
@@ -20,7 +20,7 @@
20
20
  "inquirer": "^12.9.6",
21
21
  "stripe": "^17.5.0",
22
22
  "js-yaml": "^4.1.0",
23
- "lua-cli": "^3.43.0",
23
+ "lua-cli": "^3.45.0",
24
24
  "openai": "^5.23.0",
25
25
  "uuid": "^13.0.0",
26
26
  "zod": "^3.24.1"
@@ -244,18 +244,23 @@ class LuaAppClient {
244
244
  /**
245
245
  * One post per allowed shell origin: the browser delivers it only when the
246
246
  * framing window really has that origin, so no other page ever receives it.
247
- * A browser refuses some shell origins as a target outright (a web browser
248
- * throws on the desktop app's `tauri://localhost`), so each post stands
249
- * alone: one origin this browser cannot address must not stop the others,
250
- * or fail the session that posts `lua:ready` when it opens.
247
+ *
248
+ * A shell on a custom scheme (lua-desktop's `tauri://localhost`) is not a
249
+ * valid postMessage target origin: WebKit throws "Invalid target origin",
250
+ * which aborted the loop and the session ("Could not load" in lua-desktop),
251
+ * and skipping it would leave the desktop shell without `lua:ready`.
252
+ * Such an origin is posted with '*': the messages carry only {type, v}, and
253
+ * the CSP's frame-ancestors already limits who can frame the app. Each post
254
+ * is isolated so one origin can never stop the others.
251
255
  */
252
256
  private post(message: { type: string; v: number }): void {
253
257
  if (window.parent === window) return;
254
- for (const origin of this.hostOrigins()) {
258
+ const targets = new Set(this.hostOrigins().map((origin) => (/^https?:\/\//i.test(origin) ? origin : '*')));
259
+ for (const target of targets) {
255
260
  try {
256
- window.parent.postMessage(message, origin);
261
+ window.parent.postMessage(message, target);
257
262
  } catch {
258
- // Not addressable from this browser; the shell that framed us has one of the others.
263
+ // An origin this browser will not target: skip it, keep the rest.
259
264
  }
260
265
  }
261
266
  }