@vellumai/assistant 0.4.23 → 0.4.26

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 (63) 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__/call-controller.test.ts +80 -0
  6. package/src/__tests__/config-schema.test.ts +38 -178
  7. package/src/__tests__/conversation-routes-guardian-reply.test.ts +4 -1
  8. package/src/__tests__/credential-security-invariants.test.ts +0 -2
  9. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +2 -2
  10. package/src/__tests__/ipc-snapshot.test.ts +0 -9
  11. package/src/__tests__/onboarding-template-contract.test.ts +10 -20
  12. package/src/__tests__/relay-server.test.ts +3 -3
  13. package/src/__tests__/runtime-events-sse-parity.test.ts +10 -0
  14. package/src/__tests__/runtime-events-sse.test.ts +7 -0
  15. package/src/__tests__/session-runtime-assembly.test.ts +34 -8
  16. package/src/__tests__/system-prompt.test.ts +7 -1
  17. package/src/__tests__/trusted-contact-approval-notifier.test.ts +12 -8
  18. package/src/__tests__/twilio-routes-twiml.test.ts +2 -2
  19. package/src/__tests__/twilio-routes.test.ts +2 -3
  20. package/src/__tests__/voice-quality.test.ts +21 -132
  21. package/src/calls/call-controller.ts +34 -29
  22. package/src/calls/relay-server.ts +11 -5
  23. package/src/calls/twilio-routes.ts +4 -38
  24. package/src/calls/voice-quality.ts +7 -63
  25. package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +7 -10
  26. package/src/config/bundled-skills/messaging/SKILL.md +3 -5
  27. package/src/config/bundled-skills/phone-calls/SKILL.md +144 -83
  28. package/src/config/bundled-skills/sms-setup/SKILL.md +0 -20
  29. package/src/config/bundled-skills/twilio-setup/SKILL.md +9 -17
  30. package/src/config/bundled-skills/voice-setup/SKILL.md +36 -1
  31. package/src/config/bundled-skills/voice-setup/icon.svg +20 -0
  32. package/src/config/calls-schema.ts +3 -53
  33. package/src/config/elevenlabs-schema.ts +33 -0
  34. package/src/config/schema.ts +183 -137
  35. package/src/config/types.ts +0 -1
  36. package/src/daemon/handlers/browser.ts +1 -6
  37. package/src/daemon/ipc-contract/browser.ts +5 -14
  38. package/src/daemon/ipc-contract-inventory.json +0 -2
  39. package/src/daemon/session-agent-loop-handlers.ts +3 -0
  40. package/src/daemon/session-runtime-assembly.ts +9 -7
  41. package/src/mcp/client.ts +2 -1
  42. package/src/memory/conversation-crud.ts +339 -166
  43. package/src/runtime/auth/middleware.ts +87 -26
  44. package/src/runtime/routes/events-routes.ts +7 -0
  45. package/src/runtime/routes/inbound-message-handler.ts +3 -4
  46. package/src/schedule/scheduler.ts +159 -45
  47. package/src/security/secure-keys.ts +3 -3
  48. package/src/tools/browser/browser-manager.ts +72 -228
  49. package/src/tools/browser/browser-screencast.ts +0 -5
  50. package/src/tools/network/script-proxy/certs.ts +7 -237
  51. package/src/tools/network/script-proxy/connect-tunnel.ts +1 -82
  52. package/src/tools/network/script-proxy/http-forwarder.ts +2 -151
  53. package/src/tools/network/script-proxy/logging.ts +12 -196
  54. package/src/tools/network/script-proxy/mitm-handler.ts +2 -270
  55. package/src/tools/network/script-proxy/policy.ts +4 -152
  56. package/src/tools/network/script-proxy/router.ts +2 -60
  57. package/src/tools/network/script-proxy/server.ts +5 -137
  58. package/src/tools/network/script-proxy/types.ts +19 -125
  59. package/src/tools/system/voice-config.ts +23 -1
  60. package/src/util/logger.ts +4 -1
  61. package/src/__tests__/elevenlabs-config.test.ts +0 -95
  62. package/src/__tests__/twilio-routes-elevenlabs.test.ts +0 -407
  63. package/src/calls/elevenlabs-config.ts +0 -32
@@ -1,8 +1,18 @@
1
1
  /**
2
2
  * JWT bearer auth middleware for the runtime HTTP server.
3
3
  *
4
- * Extracts `Authorization: Bearer <token>`, verifies the JWT with
5
- * `aud=vellum-daemon`, and builds an AuthContext from the claims.
4
+ * Extracts `Authorization: Bearer <token>`, verifies the JWT, and
5
+ * builds an AuthContext from the claims.
6
+ *
7
+ * Accepts two JWT audiences:
8
+ * - `vellum-daemon` — primary audience, used by the gateway's runtime
9
+ * proxy after token exchange and by daemon-minted delivery tokens.
10
+ * - `vellum-gateway` — fallback audience, used by direct local clients
11
+ * (e.g., the macOS app's SettingsStore) that hold a guardian-issued
12
+ * JWT but call daemon endpoints directly without routing through the
13
+ * gateway's runtime proxy. Both daemon and gateway share the same
14
+ * HMAC signing key (~/.vellum/protected/actor-token-signing-key),
15
+ * so the signature is valid regardless of audience.
6
16
  *
7
17
  * Replaces both the legacy bearer shared-secret check and the
8
18
  * actor-token HMAC middleware with a single JWT verification path.
@@ -12,16 +22,16 @@
12
22
  * so downstream code always has a typed context to consume.
13
23
  */
14
24
 
15
- import { isHttpAuthDisabled } from '../../config/env.js';
16
- import { getLogger } from '../../util/logger.js';
17
- import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
18
- import { extractBearerToken } from '../middleware/auth.js';
19
- import { buildAuthContext } from './context.js';
20
- import { resolveScopeProfile } from './scopes.js';
21
- import { verifyToken } from './token-service.js';
22
- import type { AuthContext } from './types.js';
25
+ import { isHttpAuthDisabled } from "../../config/env.js";
26
+ import { getLogger } from "../../util/logger.js";
27
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
28
+ import { extractBearerToken } from "../middleware/auth.js";
29
+ import { buildAuthContext } from "./context.js";
30
+ import { resolveScopeProfile } from "./scopes.js";
31
+ import { verifyToken } from "./token-service.js";
32
+ import type { AuthContext } from "./types.js";
23
33
 
24
- const log = getLogger('auth-middleware');
34
+ const log = getLogger("auth-middleware");
25
35
 
26
36
  // ---------------------------------------------------------------------------
27
37
  // Result type
@@ -43,11 +53,11 @@ export type AuthenticateResult =
43
53
  function buildDevBypassContext(): AuthContext {
44
54
  return {
45
55
  subject: `actor:${DAEMON_INTERNAL_ASSISTANT_ID}:dev-bypass`,
46
- principalType: 'actor',
56
+ principalType: "actor",
47
57
  assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
48
- actorPrincipalId: 'dev-bypass',
49
- scopeProfile: 'actor_client_v1',
50
- scopes: resolveScopeProfile('actor_client_v1'),
58
+ actorPrincipalId: "dev-bypass",
59
+ scopeProfile: "actor_client_v1",
60
+ scopes: resolveScopeProfile("actor_client_v1"),
51
61
  policyEpoch: Number.MAX_SAFE_INTEGER,
52
62
  };
53
63
  }
@@ -72,36 +82,82 @@ export function authenticateRequest(req: Request): AuthenticateResult {
72
82
 
73
83
  const rawToken = extractBearerToken(req);
74
84
  if (!rawToken) {
75
- log.warn({ reason: 'missing_token', path }, 'Auth denied: missing Authorization header');
85
+ log.warn(
86
+ { reason: "missing_token", path },
87
+ "Auth denied: missing Authorization header",
88
+ );
76
89
  return {
77
90
  ok: false,
78
91
  response: Response.json(
79
- { error: { code: 'UNAUTHORIZED', message: 'Missing Authorization header' } },
92
+ {
93
+ error: {
94
+ code: "UNAUTHORIZED",
95
+ message: "Missing Authorization header",
96
+ },
97
+ },
80
98
  { status: 401 },
81
99
  ),
82
100
  };
83
101
  }
84
102
 
85
- // Verify the JWT with audience = vellum-daemon
86
- const verifyResult = verifyToken(rawToken, 'vellum-daemon');
103
+ // Verify the JWT prefer vellum-daemon audience (gateway-proxied requests
104
+ // and daemon-minted tokens), but also accept vellum-gateway audience for
105
+ // direct local clients (macOS SettingsStore) that hold a guardian-issued JWT
106
+ // and call daemon endpoints without routing through the gateway runtime proxy.
107
+ let verifyResult = verifyToken(rawToken, "vellum-daemon");
108
+ if (
109
+ !verifyResult.ok &&
110
+ verifyResult.reason?.startsWith("audience_mismatch")
111
+ ) {
112
+ verifyResult = verifyToken(rawToken, "vellum-gateway");
113
+ // Normalize gateway-audience claims to daemon context so that
114
+ // buildAuthContext applies the same assistantId normalization
115
+ // (aud=vellum-daemon → assistantId='self') that gateway-exchanged
116
+ // tokens receive. Without this rewrite, the external assistant ID
117
+ // from the guardian-issued JWT would leak into daemon-internal
118
+ // scoping (storage keys, routing), violating the invariant
119
+ // documented in context.ts:30-33.
120
+ if (verifyResult.ok) {
121
+ verifyResult = {
122
+ ok: true,
123
+ claims: { ...verifyResult.claims, aud: "vellum-daemon" },
124
+ };
125
+ }
126
+ }
87
127
  if (!verifyResult.ok) {
88
128
  // Stale policy epoch gets a specific error code so clients can refresh
89
- if (verifyResult.reason === 'stale_policy_epoch') {
90
- log.warn({ reason: 'stale_policy_epoch', path }, 'Auth denied: stale policy epoch');
129
+ if (verifyResult.reason === "stale_policy_epoch") {
130
+ log.warn(
131
+ { reason: "stale_policy_epoch", path },
132
+ "Auth denied: stale policy epoch",
133
+ );
91
134
  return {
92
135
  ok: false,
93
136
  response: Response.json(
94
- { error: { code: 'refresh_required', message: 'Token policy epoch is stale; refresh required' } },
137
+ {
138
+ error: {
139
+ code: "refresh_required",
140
+ message: "Token policy epoch is stale; refresh required",
141
+ },
142
+ },
95
143
  { status: 401 },
96
144
  ),
97
145
  };
98
146
  }
99
147
 
100
- log.warn({ reason: verifyResult.reason, path }, 'Auth denied: JWT verification failed');
148
+ log.warn(
149
+ { reason: verifyResult.reason, path },
150
+ "Auth denied: JWT verification failed",
151
+ );
101
152
  return {
102
153
  ok: false,
103
154
  response: Response.json(
104
- { error: { code: 'UNAUTHORIZED', message: `Invalid token: ${verifyResult.reason}` } },
155
+ {
156
+ error: {
157
+ code: "UNAUTHORIZED",
158
+ message: `Invalid token: ${verifyResult.reason}`,
159
+ },
160
+ },
105
161
  { status: 401 },
106
162
  ),
107
163
  };
@@ -112,12 +168,17 @@ export function authenticateRequest(req: Request): AuthenticateResult {
112
168
  if (!contextResult.ok) {
113
169
  log.warn(
114
170
  { reason: contextResult.reason, path, sub: verifyResult.claims.sub },
115
- 'Auth denied: invalid JWT claims',
171
+ "Auth denied: invalid JWT claims",
116
172
  );
117
173
  return {
118
174
  ok: false,
119
175
  response: Response.json(
120
- { error: { code: 'UNAUTHORIZED', message: `Invalid token claims: ${contextResult.reason}` } },
176
+ {
177
+ error: {
178
+ code: "UNAUTHORIZED",
179
+ message: `Invalid token claims: ${contextResult.reason}`,
180
+ },
181
+ },
121
182
  { status: 401 },
122
183
  ),
123
184
  };
@@ -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 */ }