@vellumai/assistant 0.4.23 → 0.4.25

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 (60) hide show
  1. package/bun.lock +3 -0
  2. package/package.json +2 -1
  3. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -15
  4. package/src/__tests__/assistant-events-sse-hardening.test.ts +9 -3
  5. package/src/__tests__/config-schema.test.ts +38 -178
  6. package/src/__tests__/conversation-routes-guardian-reply.test.ts +4 -1
  7. package/src/__tests__/credential-security-invariants.test.ts +0 -2
  8. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +2 -2
  9. package/src/__tests__/ipc-snapshot.test.ts +0 -9
  10. package/src/__tests__/onboarding-template-contract.test.ts +10 -20
  11. package/src/__tests__/relay-server.test.ts +3 -3
  12. package/src/__tests__/runtime-events-sse-parity.test.ts +10 -0
  13. package/src/__tests__/runtime-events-sse.test.ts +7 -0
  14. package/src/__tests__/session-runtime-assembly.test.ts +34 -8
  15. package/src/__tests__/system-prompt.test.ts +7 -1
  16. package/src/__tests__/trusted-contact-approval-notifier.test.ts +12 -8
  17. package/src/__tests__/twilio-routes-twiml.test.ts +2 -2
  18. package/src/__tests__/twilio-routes.test.ts +2 -3
  19. package/src/__tests__/voice-quality.test.ts +21 -132
  20. package/src/calls/relay-server.ts +11 -5
  21. package/src/calls/twilio-routes.ts +4 -38
  22. package/src/calls/voice-quality.ts +7 -63
  23. package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +7 -10
  24. package/src/config/bundled-skills/messaging/SKILL.md +3 -5
  25. package/src/config/bundled-skills/phone-calls/SKILL.md +143 -82
  26. package/src/config/bundled-skills/sms-setup/SKILL.md +0 -20
  27. package/src/config/bundled-skills/twilio-setup/SKILL.md +9 -17
  28. package/src/config/bundled-skills/voice-setup/SKILL.md +36 -1
  29. package/src/config/bundled-skills/voice-setup/icon.svg +20 -0
  30. package/src/config/calls-schema.ts +3 -53
  31. package/src/config/elevenlabs-schema.ts +33 -0
  32. package/src/config/schema.ts +183 -137
  33. package/src/config/types.ts +0 -1
  34. package/src/daemon/handlers/browser.ts +1 -6
  35. package/src/daemon/ipc-contract/browser.ts +5 -14
  36. package/src/daemon/ipc-contract-inventory.json +0 -2
  37. package/src/daemon/session-agent-loop-handlers.ts +3 -0
  38. package/src/daemon/session-runtime-assembly.ts +9 -7
  39. package/src/mcp/client.ts +2 -1
  40. package/src/memory/conversation-crud.ts +339 -166
  41. package/src/runtime/routes/events-routes.ts +7 -0
  42. package/src/runtime/routes/inbound-message-handler.ts +3 -4
  43. package/src/schedule/scheduler.ts +159 -45
  44. package/src/security/secure-keys.ts +3 -3
  45. package/src/tools/browser/browser-manager.ts +72 -228
  46. package/src/tools/browser/browser-screencast.ts +0 -5
  47. package/src/tools/network/script-proxy/certs.ts +7 -237
  48. package/src/tools/network/script-proxy/connect-tunnel.ts +1 -82
  49. package/src/tools/network/script-proxy/http-forwarder.ts +2 -151
  50. package/src/tools/network/script-proxy/logging.ts +12 -196
  51. package/src/tools/network/script-proxy/mitm-handler.ts +2 -270
  52. package/src/tools/network/script-proxy/policy.ts +4 -152
  53. package/src/tools/network/script-proxy/router.ts +2 -60
  54. package/src/tools/network/script-proxy/server.ts +5 -137
  55. package/src/tools/network/script-proxy/types.ts +19 -125
  56. package/src/tools/system/voice-config.ts +23 -1
  57. package/src/util/logger.ts +4 -1
  58. package/src/__tests__/elevenlabs-config.test.ts +0 -95
  59. package/src/__tests__/twilio-routes-elevenlabs.test.ts +0 -407
  60. package/src/calls/elevenlabs-config.ts +0 -32
@@ -116,6 +116,13 @@ export function handleSubscribeAssistantEvents(
116
116
  return;
117
117
  }
118
118
 
119
+ // Immediately enqueue a heartbeat comment so the HTTP status line and
120
+ // headers are flushed to the client without waiting for a real event.
121
+ // Without this, Bun may buffer the headers until the first data chunk
122
+ // arrives, causing clients (e.g. Python `requests`) to hang until the
123
+ // periodic heartbeat fires or an event is published.
124
+ controller.enqueue(encoder.encode(formatSseHeartbeat()));
125
+
119
126
  // Send a keep-alive comment on each interval to prevent proxies and
120
127
  // load-balancers from treating idle connections as timed out.
121
128
  heartbeatTimer = setInterval(() => {
@@ -9,6 +9,7 @@
9
9
  import type { ChannelId, InterfaceId } from '../../channels/types.js';
10
10
  import { CHANNEL_IDS, INTERFACE_IDS, isChannelId, parseInterfaceId } from '../../channels/types.js';
11
11
  import { getGatewayInternalBaseUrl } from '../../config/env.js';
12
+ import { resolveUserReference } from '../../config/user-reference.js';
12
13
  import { RESEND_COOLDOWN_MS } from '../../daemon/handlers/config-channels.js';
13
14
  import * as attachmentsStore from '../../memory/attachments-store.js';
14
15
  import {
@@ -1577,10 +1578,8 @@ function startTrustedContactApprovalNotifier(params: {
1577
1578
  const guardianName = resolveGuardianDisplayName(
1578
1579
  assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
1579
1580
  sourceChannel,
1580
- );
1581
- const waitingText = guardianName
1582
- ? `Waiting for ${guardianName}'s approval...`
1583
- : 'Waiting for your guardian\'s approval...';
1581
+ ) ?? resolveUserReference();
1582
+ const waitingText = `Waiting for ${guardianName}'s approval...`;
1584
1583
  try {
1585
1584
  await deliverChannelReply(replyCallbackUrl, {
1586
1585
  chatId: externalChatId,
@@ -1,18 +1,31 @@
1
- import { createConversation } from '../memory/conversation-store.js';
2
- import { GENERATING_TITLE, queueGenerateConversationTitle } from '../memory/conversation-title-service.js';
3
- import { invalidateAssistantInferredItemsForConversation } from '../memory/task-memory-cleanup.js';
4
- import { runSequencesOnce } from '../sequence/engine.js';
5
- import { claimDueReminders, completeReminder, failReminder, type RoutingIntent,setReminderConversationId } from '../tools/reminder/reminder-store.js';
6
- import { getLogger } from '../util/logger.js';
7
- import { runWatchersOnce, type WatcherEscalator,type WatcherNotifier } from '../watcher/engine.js';
8
- import { hasSetConstructs } from './recurrence-engine.js';
1
+ import { createConversation } from "../memory/conversation-store.js";
2
+ import {
3
+ GENERATING_TITLE,
4
+ queueGenerateConversationTitle,
5
+ } from "../memory/conversation-title-service.js";
6
+ import { invalidateAssistantInferredItemsForConversation } from "../memory/task-memory-cleanup.js";
7
+ import { runSequencesOnce } from "../sequence/engine.js";
8
+ import {
9
+ claimDueReminders,
10
+ completeReminder,
11
+ failReminder,
12
+ type RoutingIntent,
13
+ setReminderConversationId,
14
+ } from "../tools/reminder/reminder-store.js";
15
+ import { getLogger } from "../util/logger.js";
16
+ import {
17
+ runWatchersOnce,
18
+ type WatcherEscalator,
19
+ type WatcherNotifier,
20
+ } from "../watcher/engine.js";
21
+ import { hasSetConstructs } from "./recurrence-engine.js";
9
22
  import {
10
23
  claimDueSchedules,
11
24
  completeScheduleRun,
12
25
  createScheduleRun,
13
- } from './schedule-store.js';
26
+ } from "./schedule-store.js";
14
27
 
15
- const log = getLogger('scheduler');
28
+ const log = getLogger("scheduler");
16
29
 
17
30
  export type ScheduleMessageProcessor = (
18
31
  conversationId: string,
@@ -50,21 +63,35 @@ export function startScheduler(
50
63
  if (stopped || tickRunning) return;
51
64
  tickRunning = true;
52
65
  try {
53
- await runScheduleOnce(processMessage, notifyReminder, notifySchedule, watcherNotifier, watcherEscalator);
66
+ await runScheduleOnce(
67
+ processMessage,
68
+ notifyReminder,
69
+ notifySchedule,
70
+ watcherNotifier,
71
+ watcherEscalator,
72
+ );
54
73
  } catch (err) {
55
- log.error({ err }, 'Schedule tick failed');
74
+ log.error({ err }, "Schedule tick failed");
56
75
  } finally {
57
76
  tickRunning = false;
58
77
  }
59
78
  };
60
79
 
61
- const timer = setInterval(() => { void tick(); }, TICK_INTERVAL_MS);
80
+ const timer = setInterval(() => {
81
+ void tick();
82
+ }, TICK_INTERVAL_MS);
62
83
  timer.unref();
63
84
  void tick();
64
85
 
65
86
  return {
66
87
  async runOnce(): Promise<number> {
67
- return runScheduleOnce(processMessage, notifyReminder, notifySchedule, watcherNotifier, watcherEscalator);
88
+ return runScheduleOnce(
89
+ processMessage,
90
+ notifyReminder,
91
+ notifySchedule,
92
+ watcherNotifier,
93
+ watcherEscalator,
94
+ );
68
95
  },
69
96
  stop(): void {
70
97
  stopped = true;
@@ -90,62 +117,123 @@ async function runScheduleOnce(
90
117
  const taskMatch = job.message.match(/^run_task:(\S+)$/);
91
118
  if (taskMatch) {
92
119
  const taskId = taskMatch[1];
93
- const isRruleSet = job.syntax === 'rrule' && hasSetConstructs(job.expression);
120
+ const isRruleSet =
121
+ job.syntax === "rrule" && hasSetConstructs(job.expression);
94
122
  try {
95
- log.info({ jobId: job.id, name: job.name, taskId, syntax: job.syntax, expression: job.expression, isRruleSet }, 'Executing scheduled task');
96
- const { runTask } = await import('../tasks/task-runner.js');
123
+ log.info(
124
+ {
125
+ jobId: job.id,
126
+ name: job.name,
127
+ taskId,
128
+ syntax: job.syntax,
129
+ expression: job.expression,
130
+ isRruleSet,
131
+ },
132
+ "Executing scheduled task",
133
+ );
134
+ const { runTask } = await import("../tasks/task-runner.js");
97
135
  const result = await runTask(
98
- { taskId, workingDir: process.cwd(), source: 'schedule' },
99
- processMessage as (conversationId: string, message: string, taskRunId: string) => Promise<void>,
136
+ { taskId, workingDir: process.cwd(), source: "schedule" },
137
+ processMessage as (
138
+ conversationId: string,
139
+ message: string,
140
+ taskRunId: string,
141
+ ) => Promise<void>,
100
142
  );
101
143
 
102
144
  // Track the schedule run using the task's conversation
103
145
  const runId = createScheduleRun(job.id, result.conversationId);
104
- if (result.status === 'failed') {
105
- completeScheduleRun(runId, { status: 'error', error: result.error ?? 'Task run failed' });
146
+ if (result.status === "failed") {
147
+ completeScheduleRun(runId, {
148
+ status: "error",
149
+ error: result.error ?? "Task run failed",
150
+ });
106
151
  } else {
107
- completeScheduleRun(runId, { status: 'ok' });
152
+ completeScheduleRun(runId, { status: "ok" });
108
153
  notifySchedule({ id: job.id, name: job.name });
109
154
  }
110
155
  processed += 1;
111
156
  } catch (err) {
112
157
  const message = err instanceof Error ? err.message : String(err);
113
- log.warn({ err, jobId: job.id, name: job.name, taskId, syntax: job.syntax, expression: job.expression, isRruleSet }, 'Scheduled task execution failed');
158
+ log.warn(
159
+ {
160
+ err,
161
+ jobId: job.id,
162
+ name: job.name,
163
+ taskId,
164
+ syntax: job.syntax,
165
+ expression: job.expression,
166
+ isRruleSet,
167
+ },
168
+ "Scheduled task execution failed",
169
+ );
114
170
  // Create a fallback conversation for the schedule run record
115
- const fallbackConversation = createConversation({ title: GENERATING_TITLE, source: 'schedule' });
171
+ const fallbackConversation = createConversation({
172
+ title: GENERATING_TITLE,
173
+ source: "schedule",
174
+ scheduleJobId: job.id,
175
+ });
116
176
  queueGenerateConversationTitle({
117
177
  conversationId: fallbackConversation.id,
118
- context: { origin: 'schedule', systemHint: `Schedule: ${job.name}` },
178
+ context: { origin: "schedule", systemHint: `Schedule: ${job.name}` },
119
179
  });
120
180
  const runId = createScheduleRun(job.id, fallbackConversation.id);
121
- completeScheduleRun(runId, { status: 'error', error: message });
181
+ completeScheduleRun(runId, { status: "error", error: message });
122
182
  }
123
183
  continue;
124
184
  }
125
185
 
126
- const conversation = createConversation({ title: GENERATING_TITLE, source: 'schedule' });
186
+ const conversation = createConversation({
187
+ title: GENERATING_TITLE,
188
+ source: "schedule",
189
+ scheduleJobId: job.id,
190
+ });
127
191
  queueGenerateConversationTitle({
128
192
  conversationId: conversation.id,
129
- context: { origin: 'schedule', systemHint: `Schedule: ${job.name}` },
193
+ context: { origin: "schedule", systemHint: `Schedule: ${job.name}` },
130
194
  });
131
195
  const runId = createScheduleRun(job.id, conversation.id);
132
- const isRruleSetMsg = job.syntax === 'rrule' && hasSetConstructs(job.expression);
196
+ const isRruleSetMsg =
197
+ job.syntax === "rrule" && hasSetConstructs(job.expression);
133
198
 
134
199
  try {
135
- log.info({ jobId: job.id, name: job.name, syntax: job.syntax, expression: job.expression, isRruleSet: isRruleSetMsg, conversationId: conversation.id }, 'Executing schedule');
200
+ log.info(
201
+ {
202
+ jobId: job.id,
203
+ name: job.name,
204
+ syntax: job.syntax,
205
+ expression: job.expression,
206
+ isRruleSet: isRruleSetMsg,
207
+ conversationId: conversation.id,
208
+ },
209
+ "Executing schedule",
210
+ );
136
211
  await processMessage(conversation.id, job.message);
137
- completeScheduleRun(runId, { status: 'ok' });
212
+ completeScheduleRun(runId, { status: "ok" });
138
213
  notifySchedule({ id: job.id, name: job.name });
139
214
  processed += 1;
140
215
  } catch (err) {
141
216
  const message = err instanceof Error ? err.message : String(err);
142
- log.warn({ err, jobId: job.id, name: job.name, syntax: job.syntax, expression: job.expression, isRruleSet: isRruleSetMsg }, 'Schedule execution failed');
143
- completeScheduleRun(runId, { status: 'error', error: message });
217
+ log.warn(
218
+ {
219
+ err,
220
+ jobId: job.id,
221
+ name: job.name,
222
+ syntax: job.syntax,
223
+ expression: job.expression,
224
+ isRruleSet: isRruleSetMsg,
225
+ },
226
+ "Schedule execution failed",
227
+ );
228
+ completeScheduleRun(runId, { status: "error", error: message });
144
229
 
145
230
  try {
146
231
  invalidateAssistantInferredItemsForConversation(conversation.id);
147
232
  } catch (cleanupErr) {
148
- log.warn({ err: cleanupErr, conversationId: conversation.id }, 'Failed to invalidate assistant-inferred memory items');
233
+ log.warn(
234
+ { err: cleanupErr, conversationId: conversation.id },
235
+ "Failed to invalidate assistant-inferred memory items",
236
+ );
149
237
  }
150
238
  }
151
239
  }
@@ -153,24 +241,43 @@ async function runScheduleOnce(
153
241
  // ── One-shot reminders ──────────────────────────────────────────────
154
242
  const dueReminders = claimDueReminders(now);
155
243
  for (const reminder of dueReminders) {
156
- if (reminder.mode === 'execute') {
157
- const conversation = createConversation({ title: GENERATING_TITLE, source: 'reminder' });
244
+ if (reminder.mode === "execute") {
245
+ const conversation = createConversation({
246
+ title: GENERATING_TITLE,
247
+ source: "reminder",
248
+ });
158
249
  queueGenerateConversationTitle({
159
250
  conversationId: conversation.id,
160
- context: { origin: 'reminder', systemHint: `Reminder: ${reminder.label}` },
251
+ context: {
252
+ origin: "reminder",
253
+ systemHint: `Reminder: ${reminder.label}`,
254
+ },
161
255
  });
162
256
  setReminderConversationId(reminder.id, conversation.id);
163
257
  try {
164
- log.info({ reminderId: reminder.id, label: reminder.label, conversationId: conversation.id }, 'Executing reminder');
258
+ log.info(
259
+ {
260
+ reminderId: reminder.id,
261
+ label: reminder.label,
262
+ conversationId: conversation.id,
263
+ },
264
+ "Executing reminder",
265
+ );
165
266
  await processMessage(conversation.id, reminder.message);
166
267
  completeReminder(reminder.id);
167
268
  } catch (err) {
168
- log.warn({ err, reminderId: reminder.id }, 'Reminder execution failed, reverting to pending');
269
+ log.warn(
270
+ { err, reminderId: reminder.id },
271
+ "Reminder execution failed, reverting to pending",
272
+ );
169
273
  failReminder(reminder.id);
170
274
  }
171
275
  } else {
172
276
  try {
173
- log.info({ reminderId: reminder.id, label: reminder.label }, 'Firing reminder notification');
277
+ log.info(
278
+ { reminderId: reminder.id, label: reminder.label },
279
+ "Firing reminder notification",
280
+ );
174
281
  notifyReminder({
175
282
  id: reminder.id,
176
283
  label: reminder.label,
@@ -180,7 +287,10 @@ async function runScheduleOnce(
180
287
  });
181
288
  completeReminder(reminder.id);
182
289
  } catch (err) {
183
- log.warn({ err, reminderId: reminder.id }, 'Reminder notification failed, reverting to pending');
290
+ log.warn(
291
+ { err, reminderId: reminder.id },
292
+ "Reminder notification failed, reverting to pending",
293
+ );
184
294
  failReminder(reminder.id);
185
295
  }
186
296
  }
@@ -190,10 +300,14 @@ async function runScheduleOnce(
190
300
  // ── Watchers (event-driven polling) ────────────────────────────────
191
301
  if (watcherNotifier && watcherEscalator) {
192
302
  try {
193
- const watcherProcessed = await runWatchersOnce(processMessage, watcherNotifier, watcherEscalator);
303
+ const watcherProcessed = await runWatchersOnce(
304
+ processMessage,
305
+ watcherNotifier,
306
+ watcherEscalator,
307
+ );
194
308
  processed += watcherProcessed;
195
309
  } catch (err) {
196
- log.error({ err }, 'Watcher tick failed');
310
+ log.error({ err }, "Watcher tick failed");
197
311
  }
198
312
  }
199
313
 
@@ -202,11 +316,11 @@ async function runScheduleOnce(
202
316
  const sequenceProcessed = await runSequencesOnce(processMessage);
203
317
  processed += sequenceProcessed;
204
318
  } catch (err) {
205
- log.error({ err }, 'Sequence engine tick failed');
319
+ log.error({ err }, "Sequence engine tick failed");
206
320
  }
207
321
 
208
322
  if (processed > 0) {
209
- log.info({ processed }, 'Schedule tick complete');
323
+ log.info({ processed }, "Schedule tick complete");
210
324
  }
211
325
  return processed;
212
326
  }
@@ -161,7 +161,7 @@ export function setSecureKey(account: string, value: string): boolean {
161
161
  try {
162
162
  // Only attempt deletion if the key actually exists in keychain to
163
163
  // avoid spawning a subprocess on every write.
164
- if (keychain.getKey(account) !== undefined) {
164
+ if (keychain.getKey(account) != null) {
165
165
  keychain.deleteKey(account);
166
166
  }
167
167
  } catch { /* best-effort */ }
@@ -301,7 +301,7 @@ export async function setSecureKeyAsync(
301
301
  // Only attempt deletion if the key actually exists in keychain to
302
302
  // avoid spawning a subprocess on every write.
303
303
  const exists = await keychain.getKeyAsync(account);
304
- if (exists !== undefined) {
304
+ if (exists != null) {
305
305
  await keychain.deleteKeyAsync(account);
306
306
  }
307
307
  } catch { /* best-effort */ }
@@ -323,7 +323,7 @@ export async function setSecureKeyAsync(
323
323
  keychainMissCache.delete(account);
324
324
  try {
325
325
  const exists = await keychain.getKeyAsync(account);
326
- if (exists !== undefined) {
326
+ if (exists != null) {
327
327
  await keychain.deleteKeyAsync(account);
328
328
  }
329
329
  } catch { /* best-effort */ }