@vellumai/assistant 0.3.15 → 0.3.18

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 (306) hide show
  1. package/ARCHITECTURE.md +211 -12
  2. package/Dockerfile +1 -1
  3. package/README.md +11 -5
  4. package/docs/architecture/http-token-refresh.md +274 -0
  5. package/docs/architecture/memory.md +5 -4
  6. package/docs/architecture/scheduling.md +4 -88
  7. package/docs/runbook-trusted-contacts.md +283 -0
  8. package/docs/trusted-contact-access.md +247 -0
  9. package/package.json +1 -1
  10. package/scripts/ipc/check-swift-decoder-drift.ts +2 -0
  11. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +2 -6
  12. package/src/__tests__/access-request-decision.test.ts +328 -0
  13. package/src/__tests__/asset-materialize-tool.test.ts +7 -7
  14. package/src/__tests__/asset-search-tool.test.ts +15 -15
  15. package/src/__tests__/attachments-store.test.ts +13 -13
  16. package/src/__tests__/call-controller.test.ts +150 -4
  17. package/src/__tests__/call-conversation-messages.test.ts +2 -2
  18. package/src/__tests__/call-pointer-messages.test.ts +28 -0
  19. package/src/__tests__/call-start-guardian-guard.test.ts +93 -0
  20. package/src/__tests__/channel-approval-routes.test.ts +108 -12
  21. package/src/__tests__/channel-guardian.test.ts +19 -15
  22. package/src/__tests__/checker.test.ts +103 -48
  23. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
  24. package/src/__tests__/config-watcher.test.ts +356 -0
  25. package/src/__tests__/conversation-pairing.test.ts +127 -27
  26. package/src/__tests__/conversation-store.test.ts +36 -36
  27. package/src/__tests__/date-context.test.ts +179 -1
  28. package/src/__tests__/db-migration-rollback.test.ts +4 -7
  29. package/src/__tests__/deterministic-verification-control-plane.test.ts +5 -5
  30. package/src/__tests__/emit-signal-routing-intent.test.ts +179 -0
  31. package/src/__tests__/gateway-only-guard.test.ts +188 -0
  32. package/src/__tests__/guardian-action-conversation-turn.test.ts +451 -0
  33. package/src/__tests__/guardian-action-copy-generator.test.ts +197 -0
  34. package/src/__tests__/guardian-action-followup-executor.test.ts +379 -0
  35. package/src/__tests__/guardian-action-followup-store.test.ts +376 -0
  36. package/src/__tests__/guardian-action-late-reply.test.ts +425 -0
  37. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
  38. package/src/__tests__/guardian-action-store.test.ts +182 -0
  39. package/src/__tests__/guardian-action-sweep.test.ts +9 -9
  40. package/src/__tests__/guardian-dispatch.test.ts +120 -0
  41. package/src/__tests__/guardian-outbound-http.test.ts +194 -2
  42. package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
  43. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
  44. package/src/__tests__/handlers-telegram-config.test.ts +6 -6
  45. package/src/__tests__/hooks-runner.test.ts +13 -4
  46. package/src/__tests__/ingress-routes-http.test.ts +443 -0
  47. package/src/__tests__/intent-routing.test.ts +14 -0
  48. package/src/__tests__/ipc-snapshot.test.ts +23 -5
  49. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  50. package/src/__tests__/memory-regressions.test.ts +16 -12
  51. package/src/__tests__/non-member-access-request.test.ts +281 -0
  52. package/src/__tests__/notification-broadcaster.test.ts +115 -4
  53. package/src/__tests__/notification-decision-strategy.test.ts +138 -1
  54. package/src/__tests__/notification-deep-link.test.ts +44 -1
  55. package/src/__tests__/notification-guardian-path.test.ts +157 -0
  56. package/src/__tests__/notification-routing-intent.test.ts +11 -1
  57. package/src/__tests__/notification-thread-candidate-validation.test.ts +215 -0
  58. package/src/__tests__/notification-thread-candidates.test.ts +166 -0
  59. package/src/__tests__/recording-intent.test.ts +1 -0
  60. package/src/__tests__/recording-state-machine.test.ts +328 -17
  61. package/src/__tests__/registry.test.ts +17 -8
  62. package/src/__tests__/relay-server.test.ts +105 -0
  63. package/src/__tests__/reminder.test.ts +13 -0
  64. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
  65. package/src/__tests__/scheduler-recurrence.test.ts +50 -0
  66. package/src/__tests__/server-history-render.test.ts +8 -8
  67. package/src/__tests__/session-agent-loop.test.ts +1 -0
  68. package/src/__tests__/session-runtime-assembly.test.ts +49 -0
  69. package/src/__tests__/session-skill-tools.test.ts +1 -0
  70. package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
  71. package/src/__tests__/slack-channel-config.test.ts +230 -0
  72. package/src/__tests__/subagent-manager-notify.test.ts +4 -4
  73. package/src/__tests__/swarm-session-integration.test.ts +2 -2
  74. package/src/__tests__/system-prompt.test.ts +43 -0
  75. package/src/__tests__/task-management-tools.test.ts +3 -3
  76. package/src/__tests__/task-tools.test.ts +3 -3
  77. package/src/__tests__/trust-store.test.ts +38 -22
  78. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +489 -0
  79. package/src/__tests__/trusted-contact-multichannel.test.ts +405 -0
  80. package/src/__tests__/trusted-contact-verification.test.ts +360 -0
  81. package/src/__tests__/update-bulletin-format.test.ts +119 -0
  82. package/src/__tests__/update-bulletin-state.test.ts +129 -0
  83. package/src/__tests__/update-bulletin.test.ts +323 -0
  84. package/src/__tests__/update-template-contract.test.ts +24 -0
  85. package/src/__tests__/voice-session-bridge.test.ts +109 -9
  86. package/src/agent/loop.ts +2 -2
  87. package/src/amazon/client.ts +2 -3
  88. package/src/calls/call-controller.ts +241 -39
  89. package/src/calls/call-conversation-messages.ts +2 -2
  90. package/src/calls/call-domain.ts +10 -3
  91. package/src/calls/call-pointer-messages.ts +17 -5
  92. package/src/calls/guardian-action-sweep.ts +77 -36
  93. package/src/calls/guardian-dispatch.ts +8 -0
  94. package/src/calls/relay-server.ts +51 -12
  95. package/src/calls/twilio-routes.ts +3 -1
  96. package/src/calls/types.ts +1 -1
  97. package/src/calls/voice-session-bridge.ts +8 -6
  98. package/src/cli/core-commands.ts +43 -3
  99. package/src/cli/map.ts +8 -5
  100. package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
  101. package/src/config/bundled-skills/tasks/SKILL.md +1 -1
  102. package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
  103. package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
  104. package/src/config/computer-use-prompt.ts +1 -0
  105. package/src/config/core-schema.ts +16 -0
  106. package/src/config/env-registry.ts +1 -0
  107. package/src/config/env.ts +16 -1
  108. package/src/config/memory-schema.ts +5 -0
  109. package/src/config/schema.ts +4 -0
  110. package/src/config/system-prompt.ts +69 -2
  111. package/src/config/templates/BOOTSTRAP.md +1 -1
  112. package/src/config/templates/IDENTITY.md +8 -4
  113. package/src/config/templates/SOUL.md +14 -0
  114. package/src/config/templates/UPDATES.md +15 -0
  115. package/src/config/templates/USER.md +5 -1
  116. package/src/config/types.ts +1 -0
  117. package/src/config/update-bulletin-format.ts +54 -0
  118. package/src/config/update-bulletin-state.ts +49 -0
  119. package/src/config/update-bulletin-template-path.ts +6 -0
  120. package/src/config/update-bulletin.ts +97 -0
  121. package/src/config/vellum-skills/catalog.json +6 -0
  122. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
  123. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
  124. package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
  125. package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
  126. package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
  127. package/src/context/window-manager.ts +43 -3
  128. package/src/daemon/config-watcher.ts +4 -2
  129. package/src/daemon/connection-policy.ts +21 -1
  130. package/src/daemon/daemon-control.ts +219 -8
  131. package/src/daemon/date-context.ts +174 -1
  132. package/src/daemon/guardian-action-generators.ts +175 -0
  133. package/src/daemon/guardian-verification-intent.ts +120 -0
  134. package/src/daemon/handlers/apps.ts +1 -3
  135. package/src/daemon/handlers/config-channels.ts +2 -2
  136. package/src/daemon/handlers/config-heartbeat.ts +1 -1
  137. package/src/daemon/handlers/config-inbox.ts +55 -159
  138. package/src/daemon/handlers/config-ingress.ts +1 -1
  139. package/src/daemon/handlers/config-integrations.ts +1 -1
  140. package/src/daemon/handlers/config-platform.ts +1 -1
  141. package/src/daemon/handlers/config-scheduling.ts +2 -2
  142. package/src/daemon/handlers/config-slack-channel.ts +190 -0
  143. package/src/daemon/handlers/config-telegram.ts +1 -1
  144. package/src/daemon/handlers/config-twilio.ts +1 -1
  145. package/src/daemon/handlers/config-voice.ts +100 -0
  146. package/src/daemon/handlers/config.ts +3 -0
  147. package/src/daemon/handlers/identity.ts +45 -25
  148. package/src/daemon/handlers/misc.ts +83 -5
  149. package/src/daemon/handlers/navigate-settings.ts +27 -0
  150. package/src/daemon/handlers/recording.ts +270 -144
  151. package/src/daemon/handlers/sessions.ts +100 -17
  152. package/src/daemon/handlers/subagents.ts +3 -3
  153. package/src/daemon/handlers/work-items.ts +10 -7
  154. package/src/daemon/ipc-contract/integrations.ts +9 -1
  155. package/src/daemon/ipc-contract/messages.ts +4 -0
  156. package/src/daemon/ipc-contract/sessions.ts +1 -1
  157. package/src/daemon/ipc-contract/settings.ts +26 -0
  158. package/src/daemon/ipc-contract/shared.ts +2 -0
  159. package/src/daemon/ipc-contract/work-items.ts +1 -7
  160. package/src/daemon/ipc-contract/workspace.ts +12 -1
  161. package/src/daemon/ipc-contract-inventory.json +6 -1
  162. package/src/daemon/ipc-contract.ts +5 -1
  163. package/src/daemon/lifecycle.ts +314 -266
  164. package/src/daemon/recording-intent.ts +0 -41
  165. package/src/daemon/response-tier.ts +2 -2
  166. package/src/daemon/server.ts +31 -9
  167. package/src/daemon/session-agent-loop-handlers.ts +34 -9
  168. package/src/daemon/session-agent-loop.ts +15 -8
  169. package/src/daemon/session-history.ts +3 -2
  170. package/src/daemon/session-media-retry.ts +3 -0
  171. package/src/daemon/session-messaging.ts +38 -4
  172. package/src/daemon/session-notifiers.ts +2 -2
  173. package/src/daemon/session-process.ts +546 -59
  174. package/src/daemon/session-queue-manager.ts +2 -0
  175. package/src/daemon/session-runtime-assembly.ts +39 -0
  176. package/src/daemon/session-skill-tools.ts +13 -4
  177. package/src/daemon/session-tool-setup.ts +5 -6
  178. package/src/daemon/session.ts +19 -8
  179. package/src/daemon/tls-certs.ts +60 -13
  180. package/src/daemon/tool-side-effects.ts +13 -5
  181. package/src/gallery/default-gallery.ts +32 -9
  182. package/src/influencer/client.ts +2 -1
  183. package/src/memory/channel-delivery-store.ts +35 -567
  184. package/src/memory/channel-guardian-store.ts +63 -1317
  185. package/src/memory/conflict-store.ts +4 -4
  186. package/src/memory/conversation-attention-store.ts +0 -3
  187. package/src/memory/conversation-crud.ts +668 -0
  188. package/src/memory/conversation-queries.ts +361 -0
  189. package/src/memory/conversation-store.ts +44 -983
  190. package/src/memory/db-connection.ts +3 -0
  191. package/src/memory/db-init.ts +33 -0
  192. package/src/memory/delivery-channels.ts +175 -0
  193. package/src/memory/delivery-crud.ts +211 -0
  194. package/src/memory/delivery-status.ts +199 -0
  195. package/src/memory/embedding-backend.ts +70 -4
  196. package/src/memory/embedding-local.ts +12 -2
  197. package/src/memory/entity-extractor.ts +3 -8
  198. package/src/memory/fts-reconciler.ts +136 -0
  199. package/src/memory/guardian-action-store.ts +418 -5
  200. package/src/memory/guardian-approvals.ts +569 -0
  201. package/src/memory/guardian-bindings.ts +130 -0
  202. package/src/memory/guardian-rate-limits.ts +196 -0
  203. package/src/memory/guardian-verification.ts +521 -0
  204. package/src/memory/job-handlers/index-maintenance.ts +2 -1
  205. package/src/memory/job-utils.ts +8 -5
  206. package/src/memory/jobs-store.ts +66 -6
  207. package/src/memory/jobs-worker.ts +23 -1
  208. package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
  209. package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
  210. package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
  211. package/src/memory/migrations/032-guardian-delivery-conversation-index.ts +15 -0
  212. package/src/memory/migrations/032-notification-delivery-thread-decision.ts +20 -0
  213. package/src/memory/migrations/100-core-tables.ts +1 -1
  214. package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
  215. package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
  216. package/src/memory/migrations/112-assistant-inbox.ts +1 -1
  217. package/src/memory/migrations/113-late-migrations.ts +1 -1
  218. package/src/memory/migrations/116-messages-fts.ts +13 -0
  219. package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
  220. package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
  221. package/src/memory/migrations/index.ts +10 -3
  222. package/src/memory/migrations/validate-migration-state.ts +114 -15
  223. package/src/memory/qdrant-circuit-breaker.ts +105 -0
  224. package/src/memory/retriever.ts +46 -13
  225. package/src/memory/schema-migration.ts +4 -0
  226. package/src/memory/schema.ts +31 -8
  227. package/src/memory/search/semantic.ts +8 -90
  228. package/src/notifications/README.md +159 -18
  229. package/src/notifications/broadcaster.ts +69 -33
  230. package/src/notifications/conversation-pairing.ts +99 -21
  231. package/src/notifications/decision-engine.ts +176 -8
  232. package/src/notifications/deliveries-store.ts +39 -8
  233. package/src/notifications/emit-signal.ts +1 -0
  234. package/src/notifications/preferences-store.ts +7 -7
  235. package/src/notifications/thread-candidates.ts +269 -0
  236. package/src/notifications/types.ts +19 -0
  237. package/src/permissions/checker.ts +1 -16
  238. package/src/permissions/defaults.ts +25 -5
  239. package/src/permissions/prompter.ts +17 -0
  240. package/src/permissions/trust-store.ts +2 -0
  241. package/src/providers/failover.ts +19 -0
  242. package/src/providers/registry.ts +46 -1
  243. package/src/runtime/approval-message-composer.ts +1 -1
  244. package/src/runtime/channel-guardian-service.ts +15 -3
  245. package/src/runtime/channel-retry-sweep.ts +7 -2
  246. package/src/runtime/guardian-action-conversation-turn.ts +85 -0
  247. package/src/runtime/guardian-action-followup-executor.ts +301 -0
  248. package/src/runtime/guardian-action-message-composer.ts +245 -0
  249. package/src/runtime/guardian-outbound-actions.ts +26 -6
  250. package/src/runtime/guardian-verification-templates.ts +15 -9
  251. package/src/runtime/http-errors.ts +93 -0
  252. package/src/runtime/http-server.ts +133 -44
  253. package/src/runtime/http-types.ts +53 -0
  254. package/src/runtime/ingress-service.ts +237 -0
  255. package/src/runtime/middleware/error-handler.ts +4 -3
  256. package/src/runtime/middleware/rate-limiter.ts +160 -0
  257. package/src/runtime/middleware/request-logger.ts +71 -0
  258. package/src/runtime/middleware/twilio-validation.ts +7 -6
  259. package/src/runtime/pending-interactions.ts +12 -0
  260. package/src/runtime/routes/access-request-decision.ts +215 -0
  261. package/src/runtime/routes/app-routes.ts +25 -18
  262. package/src/runtime/routes/approval-routes.ts +18 -47
  263. package/src/runtime/routes/attachment-routes.ts +15 -41
  264. package/src/runtime/routes/call-routes.ts +20 -20
  265. package/src/runtime/routes/channel-delivery-routes.ts +6 -5
  266. package/src/runtime/routes/contact-routes.ts +4 -9
  267. package/src/runtime/routes/conversation-attention-routes.ts +2 -1
  268. package/src/runtime/routes/conversation-routes.ts +26 -57
  269. package/src/runtime/routes/debug-routes.ts +71 -0
  270. package/src/runtime/routes/events-routes.ts +3 -2
  271. package/src/runtime/routes/guardian-approval-interception.ts +221 -0
  272. package/src/runtime/routes/identity-routes.ts +14 -10
  273. package/src/runtime/routes/inbound-conversation.ts +3 -2
  274. package/src/runtime/routes/inbound-message-handler.ts +527 -62
  275. package/src/runtime/routes/ingress-routes.ts +174 -0
  276. package/src/runtime/routes/integration-routes.ts +78 -16
  277. package/src/runtime/routes/pairing-routes.ts +11 -10
  278. package/src/runtime/routes/secret-routes.ts +10 -18
  279. package/src/runtime/verification-rate-limiter.ts +83 -0
  280. package/src/schedule/schedule-store.ts +13 -1
  281. package/src/schedule/scheduler.ts +1 -1
  282. package/src/security/secret-ingress.ts +5 -2
  283. package/src/security/secret-scanner.ts +72 -6
  284. package/src/subagent/manager.ts +6 -4
  285. package/src/swarm/plan-validator.ts +4 -1
  286. package/src/tasks/task-runner.ts +3 -1
  287. package/src/tools/browser/api-map.ts +9 -6
  288. package/src/tools/calls/call-start.ts +20 -0
  289. package/src/tools/executor.ts +50 -568
  290. package/src/tools/permission-checker.ts +271 -0
  291. package/src/tools/registry.ts +14 -6
  292. package/src/tools/reminder/reminder-store.ts +7 -7
  293. package/src/tools/reminder/reminder.ts +6 -3
  294. package/src/tools/secret-detection-handler.ts +301 -0
  295. package/src/tools/subagent/message.ts +1 -1
  296. package/src/tools/system/voice-config.ts +62 -0
  297. package/src/tools/tasks/index.ts +3 -3
  298. package/src/tools/tasks/work-item-list.ts +3 -3
  299. package/src/tools/tasks/work-item-update.ts +4 -5
  300. package/src/tools/tool-approval-handler.ts +192 -0
  301. package/src/tools/tool-manifest.ts +2 -0
  302. package/src/version.ts +29 -2
  303. package/src/watcher/watcher-store.ts +9 -9
  304. package/src/work-items/work-item-runner.ts +9 -6
  305. /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
  306. /package/src/memory/migrations/{027-guardian-bootstrap-token.ts → 027a-guardian-bootstrap-token.ts} +0 -0
@@ -25,6 +25,8 @@ export interface QueuedMessage {
25
25
  isInteractive?: boolean;
26
26
  /** Timestamp (ms) when the message was enqueued. */
27
27
  queuedAt: number;
28
+ /** Original user message text to persist to DB when recording intent stripping produced a different `content`. */
29
+ displayContent?: string;
28
30
  }
29
31
 
30
32
  export const MAX_QUEUE_DEPTH = 10;
@@ -25,6 +25,10 @@ export interface ChannelCapabilities {
25
25
  supportsDynamicUi: boolean;
26
26
  /** Whether the channel supports voice/microphone input. */
27
27
  supportsVoiceInput: boolean;
28
+ /** Push-to-talk activation key (e.g. 'fn', 'ctrl', 'fn_shift', 'none'). Only present on desktop clients. */
29
+ pttActivationKey?: string;
30
+ /** Whether the client has been granted microphone permission by the OS. */
31
+ microphonePermissionGranted?: boolean;
28
32
  }
29
33
 
30
34
  /** Guardian identity/trust context for external chat channels. */
@@ -39,10 +43,26 @@ export interface GuardianRuntimeContext {
39
43
  denialReason?: 'no_binding' | 'no_identity';
40
44
  }
41
45
 
46
+ /** Allowed push-to-talk activation key values. Used to validate client-provided keys before system-prompt injection. */
47
+ const PTT_KEY_ALLOWLIST = new Set(['fn', 'ctrl', 'fn_shift', 'none']);
48
+
49
+ /** Validate a PTT activation key against the allowlist. Returns the key if valid, 'unknown' otherwise. */
50
+ export function sanitizePttActivationKey(key: string | undefined | null): string | undefined {
51
+ if (key == null) return undefined;
52
+ return PTT_KEY_ALLOWLIST.has(key) ? key : 'unknown';
53
+ }
54
+
55
+ /** Optional PTT metadata provided by the client alongside each message. */
56
+ export interface PttMetadata {
57
+ pttActivationKey?: string;
58
+ microphonePermissionGranted?: boolean;
59
+ }
60
+
42
61
  /** Derive channel capabilities from source channel + interface identifiers. */
43
62
  export function resolveChannelCapabilities(
44
63
  sourceChannel?: string | null,
45
64
  sourceInterface?: string | null,
65
+ pttMetadata?: PttMetadata | null,
46
66
  ): ChannelCapabilities {
47
67
  // Normalise legacy pseudo-channel IDs to canonical ChannelId values.
48
68
  let channel: string;
@@ -85,6 +105,8 @@ export function resolveChannelCapabilities(
85
105
  dashboardCapable: supportsDesktopUi,
86
106
  supportsDynamicUi: supportsDesktopUi,
87
107
  supportsVoiceInput: supportsDesktopUi,
108
+ pttActivationKey: sanitizePttActivationKey(pttMetadata?.pttActivationKey),
109
+ microphonePermissionGranted: pttMetadata?.microphonePermissionGranted,
88
110
  };
89
111
  }
90
112
  case 'telegram':
@@ -334,6 +356,23 @@ export function injectChannelCapabilityContext(message: Message, caps: ChannelCa
334
356
  lines.push('- Do NOT ask the user to use voice or microphone input.');
335
357
  }
336
358
 
359
+ // PTT state — only relevant on channels that support voice input
360
+ if (caps.supportsVoiceInput) {
361
+ if (caps.pttActivationKey && caps.pttActivationKey !== 'none') {
362
+ const keyLabel = caps.pttActivationKey === 'fn_shift' ? 'Fn+Shift' : caps.pttActivationKey === 'fn' ? 'Fn (Globe)' : caps.pttActivationKey;
363
+ lines.push(`ptt_activation_key: ${caps.pttActivationKey}`);
364
+ lines.push(`ptt_enabled: true`);
365
+ lines.push(`Push-to-talk is configured with the ${keyLabel} key. The user can hold ${keyLabel} to dictate text or start a voice conversation.`);
366
+ } else if (caps.pttActivationKey === 'none') {
367
+ lines.push(`ptt_activation_key: none`);
368
+ lines.push(`ptt_enabled: false`);
369
+ lines.push('Push-to-talk is disabled. You can offer to enable it for the user.');
370
+ }
371
+ if (caps.microphonePermissionGranted !== undefined) {
372
+ lines.push(`microphone_permission_granted: ${caps.microphonePermissionGranted}`);
373
+ }
374
+ }
375
+
337
376
  lines.push('</channel_capabilities>');
338
377
 
339
378
  const block = lines.join('\n');
@@ -282,17 +282,18 @@ export function projectSkillTools(
282
282
  );
283
283
 
284
284
  if (tools.length > 0) {
285
+ let accepted = tools;
285
286
  const prevHash = prevActive.get(skillId);
286
287
  if (prevHash === undefined) {
287
288
  // Newly active skill — register for the first time
288
- registerSkillTools(tools);
289
+ accepted = registerSkillTools(tools);
289
290
  } else if (prevHash !== currentHash) {
290
291
  // Hash changed — unregister stale tools, then re-register with new definitions
291
292
  log.info({ skillId, prevHash, currentHash }, 'Skill version changed, re-registering tools');
292
293
  unregisterSkillTools(skillId);
293
294
  alreadyUnregistered.add(skillId);
294
295
  try {
295
- registerSkillTools(tools);
296
+ accepted = registerSkillTools(tools);
296
297
  } catch (err) {
297
298
  log.error({ err, skillId }, 'Failed to re-register skill tools after version change');
298
299
  // Don't add to successfulEntries — will be cleaned up as transiently-failed
@@ -306,12 +307,20 @@ export function projectSkillTools(
306
307
  if (existing && existing.ownerSkillBundled !== (skill.bundled ?? undefined)) {
307
308
  log.info({ skillId, bundled: skill.bundled }, 'Skill bundled status changed, re-registering tools');
308
309
  unregisterSkillTools(skillId);
309
- registerSkillTools(tools);
310
+ accepted = registerSkillTools(tools);
311
+ } else {
312
+ // Filter to only tools that are actually registered for this skill.
313
+ // Some tools may have been skipped during initial registration due
314
+ // to core-name collisions — don't let them leak back in.
315
+ accepted = tools.filter((t) => {
316
+ const reg = getTool(t.name);
317
+ return reg !== undefined && reg.origin === 'skill' && reg.ownerSkillId === skillId;
318
+ });
310
319
  }
311
320
  }
312
321
 
313
322
  successfulEntries.set(skillId, currentHash);
314
- for (const tool of tools) {
323
+ for (const tool of accepted) {
315
324
  allToolDefinitions.push(tool.getDefinition());
316
325
  allToolNames.add(tool.name);
317
326
  }
@@ -117,12 +117,11 @@ export function createToolExecutor(
117
117
  forcePromptSideEffects: ctx.memoryPolicy.strictSideEffects,
118
118
  onToolLifecycleEvent: handleToolLifecycleEvent,
119
119
  sendToClient: (msg) => {
120
- const serverMsg = msg as unknown as ServerMessage;
121
- ctx.sendToClient(serverMsg);
122
- // Auto-track ui_surface_show for history persistence, mirroring what
123
- // session-surfaces.ts does when sending surfaces through its own path.
124
- if (serverMsg.type === 'ui_surface_show') {
125
- const s = serverMsg as unknown as UiSurfaceShow;
120
+ // Tool context's sendToClient uses a loose { type: string; [key: string]: unknown }
121
+ // signature, but at runtime these are always ServerMessage instances.
122
+ ctx.sendToClient(msg as ServerMessage);
123
+ if (msg.type === 'ui_surface_show') {
124
+ const s = msg as unknown as UiSurfaceShow;
126
125
  ctx.currentTurnSurfaces.push({
127
126
  surfaceId: s.surfaceId,
128
127
  surfaceType: s.surfaceType,
@@ -397,8 +397,9 @@ export class Session {
397
397
  currentPage?: string,
398
398
  metadata?: Record<string, unknown>,
399
399
  options?: { isInteractive?: boolean },
400
+ displayContent?: string,
400
401
  ): { queued: boolean; rejected?: boolean; requestId: string } {
401
- return enqueueMessageImpl(this, content, attachments, onEvent, requestId, activeSurfaceId, currentPage, metadata, options);
402
+ return enqueueMessageImpl(this, content, attachments, onEvent, requestId, activeSurfaceId, currentPage, metadata, options, displayContent);
402
403
  }
403
404
 
404
405
  getQueueDepth(): number {
@@ -425,6 +426,14 @@ export class Session {
425
426
  return this.prompter.hasPendingRequest(requestId);
426
427
  }
427
428
 
429
+ hasAnyPendingConfirmation(): boolean {
430
+ return this.prompter.hasPending;
431
+ }
432
+
433
+ denyAllPendingConfirmations(): void {
434
+ this.prompter.denyAllPending();
435
+ }
436
+
428
437
  hasPendingSecret(requestId: string): boolean {
429
438
  return this.secretPrompter.hasPendingRequest(requestId);
430
439
  }
@@ -489,13 +498,14 @@ export class Session {
489
498
  return this.currentTurnInterfaceContext;
490
499
  }
491
500
 
492
- persistUserMessage(
501
+ async persistUserMessage(
493
502
  content: string,
494
503
  attachments: UserMessageAttachment[],
495
504
  requestId?: string,
496
505
  metadata?: Record<string, unknown>,
497
- ): string {
498
- return persistUserMessageImpl(this, content, attachments, requestId, metadata);
506
+ displayContent?: string,
507
+ ): Promise<string> {
508
+ return persistUserMessageImpl(this, content, attachments, requestId, metadata, displayContent);
499
509
  }
500
510
 
501
511
  // ── Agent Loop ───────────────────────────────────────────────────
@@ -504,14 +514,14 @@ export class Session {
504
514
  content: string,
505
515
  userMessageId: string,
506
516
  onEvent: (msg: ServerMessage) => void,
507
- options?: { skipPreMessageRollback?: boolean; isInteractive?: boolean },
517
+ options?: { skipPreMessageRollback?: boolean; isInteractive?: boolean; titleText?: string },
508
518
  ): Promise<void> {
509
519
  return runAgentLoopImpl(this, content, userMessageId, onEvent, options);
510
520
  }
511
521
 
512
522
 
513
- drainQueue(reason: QueueDrainReason = 'loop_complete'): void {
514
- drainQueueImpl(this as ProcessSessionContext, reason);
523
+ drainQueue(reason: QueueDrainReason = 'loop_complete'): Promise<void> {
524
+ return drainQueueImpl(this as ProcessSessionContext, reason);
515
525
  }
516
526
 
517
527
  async processMessage(
@@ -522,8 +532,9 @@ export class Session {
522
532
  activeSurfaceId?: string,
523
533
  currentPage?: string,
524
534
  options?: { isInteractive?: boolean },
535
+ displayContent?: string,
525
536
  ): Promise<string> {
526
- return processMessageImpl(this as ProcessSessionContext, content, attachments, onEvent, requestId, activeSurfaceId, currentPage, options);
537
+ return processMessageImpl(this as ProcessSessionContext, content, attachments, onEvent, requestId, activeSurfaceId, currentPage, options, displayContent);
527
538
  }
528
539
 
529
540
  // ── History ──────────────────────────────────────────────────────
@@ -42,6 +42,8 @@ function computeFingerprint(cert: X509Certificate): string {
42
42
  return cert.fingerprint256.replace(/:/g, '').toLowerCase();
43
43
  }
44
44
 
45
+ type CertStatus = 'valid' | 'approaching_expiry' | 'invalid';
46
+
45
47
  /**
46
48
  * Check whether an existing cert+key pair is valid:
47
49
  * - All three files exist (cert, key, fingerprint)
@@ -49,8 +51,12 @@ function computeFingerprint(cert: X509Certificate): string {
49
51
  * - Cert is not expired
50
52
  * - Fingerprint file exists and matches the cert
51
53
  * - Private key is valid and matches the certificate
54
+ *
55
+ * Returns 'valid' if the cert is good, 'approaching_expiry' if it's still usable
56
+ * but expires within 30 days (renewal should be attempted), or 'invalid' if the
57
+ * cert cannot be used at all.
52
58
  */
53
- async function isExistingCertValid(): Promise<boolean> {
59
+ async function checkCertStatus(): Promise<CertStatus> {
54
60
  const certPath = getTlsCertPath();
55
61
  const keyPath = getTlsKeyPath();
56
62
  const fpPath = getTlsFingerprintPath();
@@ -63,7 +69,7 @@ async function isExistingCertValid(): Promise<boolean> {
63
69
  ]);
64
70
 
65
71
  if (!certExists || !keyExists || !fpExists) {
66
- return false;
72
+ return 'invalid';
67
73
  }
68
74
 
69
75
  try {
@@ -76,17 +82,18 @@ async function isExistingCertValid(): Promise<boolean> {
76
82
  const x509 = new X509Certificate(certPem);
77
83
 
78
84
  // Check expiration
85
+ const now = new Date();
79
86
  const notAfter = new Date(x509.validTo);
80
- if (notAfter <= new Date()) {
87
+ if (notAfter <= now) {
81
88
  log.info('Existing TLS certificate has expired, will regenerate');
82
- return false;
89
+ return 'invalid';
83
90
  }
84
91
 
85
92
  // Check fingerprint matches
86
93
  const actualFp = computeFingerprint(x509);
87
94
  if (actualFp !== storedFp.trim()) {
88
95
  log.info('TLS fingerprint mismatch, will regenerate');
89
- return false;
96
+ return 'invalid';
90
97
  }
91
98
 
92
99
  // Verify the private key is valid and matches the certificate's public key.
@@ -95,13 +102,20 @@ async function isExistingCertValid(): Promise<boolean> {
95
102
  const privateKey = createPrivateKey(keyPem);
96
103
  if (!x509.checkPrivateKey(privateKey)) {
97
104
  log.info('TLS private key does not match certificate, will regenerate');
98
- return false;
105
+ return 'invalid';
99
106
  }
100
107
 
101
- return true;
108
+ // Cert is structurally valid — check if it's approaching expiry
109
+ const thirtyDaysMs = 30 * 24 * 60 * 60 * 1000;
110
+ if (notAfter.getTime() - now.getTime() < thirtyDaysMs) {
111
+ log.info('TLS certificate approaching expiry, will attempt renewal');
112
+ return 'approaching_expiry';
113
+ }
114
+
115
+ return 'valid';
102
116
  } catch (err) {
103
117
  log.warn({ err }, 'Failed to validate existing TLS certificate, will regenerate');
104
- return false;
118
+ return 'invalid';
105
119
  }
106
120
  }
107
121
 
@@ -125,8 +139,9 @@ export async function ensureTlsCert(): Promise<{ cert: string; key: string; fing
125
139
  const keyPath = getTlsKeyPath();
126
140
  const fpPath = getTlsFingerprintPath();
127
141
 
128
- // Check if existing cert is still valid
129
- if (await isExistingCertValid()) {
142
+ const status = await checkCertStatus();
143
+
144
+ if (status === 'valid') {
130
145
  const [cert, key, fingerprint] = await Promise.all([
131
146
  readFile(certPath, 'utf-8'),
132
147
  readFile(keyPath, 'utf-8'),
@@ -136,7 +151,39 @@ export async function ensureTlsCert(): Promise<{ cert: string; key: string; fing
136
151
  return { cert, key, fingerprint: fingerprint.trim() };
137
152
  }
138
153
 
139
- // Generate new cert
154
+ if (status === 'approaching_expiry') {
155
+ try {
156
+ // Buffer existing cert/key/fingerprint before attempting renewal.
157
+ // generateNewCert() overwrites key.pem in-place, so if it fails mid-flight
158
+ // (e.g., key written but cert generation fails), reading from disk in the
159
+ // catch block would return a mismatched key/cert pair.
160
+ const [existingCert, existingKey, existingFp] = await Promise.all([
161
+ readFile(certPath, 'utf-8'),
162
+ readFile(keyPath, 'utf-8'),
163
+ readFile(fpPath, 'utf-8'),
164
+ ]);
165
+ try {
166
+ return await generateNewCert(tlsDir, certPath, keyPath, fpPath);
167
+ } catch (err) {
168
+ log.warn({ err }, 'Proactive TLS renewal failed, continuing with existing certificate');
169
+ return { cert: existingCert, key: existingKey, fingerprint: existingFp.trim() };
170
+ }
171
+ } catch (err) {
172
+ log.warn({ err }, 'Failed to read existing TLS cert for buffering, attempting regeneration');
173
+ return await generateNewCert(tlsDir, certPath, keyPath, fpPath);
174
+ }
175
+ }
176
+
177
+ // status === 'invalid' — must regenerate, no fallback
178
+ return await generateNewCert(tlsDir, certPath, keyPath, fpPath);
179
+ }
180
+
181
+ async function generateNewCert(
182
+ tlsDir: string,
183
+ certPath: string,
184
+ keyPath: string,
185
+ fpPath: string,
186
+ ): Promise<{ cert: string; key: string; fingerprint: string }> {
140
187
  log.info('Generating new self-signed TLS certificate');
141
188
  await mkdir(tlsDir, { recursive: true });
142
189
 
@@ -151,13 +198,13 @@ export async function ensureTlsCert(): Promise<{ cert: string; key: string; fing
151
198
  throw new Error(`Failed to generate TLS key: ${stderr}`);
152
199
  }
153
200
 
154
- // Generate self-signed cert (10-year validity)
201
+ // Generate self-signed cert (1-year validity)
155
202
  const certProc = Bun.spawn(
156
203
  [
157
204
  'openssl', 'req', '-new', '-x509',
158
205
  '-key', keyPath,
159
206
  '-out', certPath,
160
- '-days', '3650',
207
+ '-days', '365',
161
208
  '-subj', '/CN=Vellum Daemon',
162
209
  ],
163
210
  { stdout: 'pipe', stderr: 'pipe' },
@@ -11,6 +11,7 @@ import { updatePublishedAppDeployment } from '../services/published-app-updater.
11
11
  import { openAppViaSurface } from '../tools/apps/open-proxy.js';
12
12
  import type { ToolExecutionResult } from '../tools/types.js';
13
13
  import { isDoordashCommand, updateDoordashProgress } from './doordash-steps.js';
14
+ import { normalizeActivationKey } from './handlers/config-voice.js';
14
15
  import type { ServerMessage } from './ipc-protocol.js';
15
16
  import {
16
17
  refreshSurfacesForApp,
@@ -74,11 +75,6 @@ registerHook('app_update', (_name, input, _result, { ctx, broadcastToAllClients
74
75
  }
75
76
  });
76
77
 
77
- // Tell the client to open/focus the tasks window when the model lists tasks
78
- registerHook('task_list_show', (_name, _input, _result, { ctx }) => {
79
- ctx.sendToClient({ type: 'open_tasks_window' });
80
- });
81
-
82
78
  // Broadcast tasks_changed so connected clients (e.g. macOS Tasks window)
83
79
  // auto-refresh when the LLM mutates the task queue via tools
84
80
  registerHook(
@@ -101,6 +97,18 @@ registerHook(
101
97
  },
102
98
  );
103
99
 
100
+ // Broadcast activation key change to all connected clients so every
101
+ // macOS/iOS instance picks up the new setting immediately.
102
+ registerHook('voice_config_update', (_name, input, _result, { broadcastToAllClients }) => {
103
+ const key = input.activation_key as string | undefined;
104
+ if (key) {
105
+ const normalized = normalizeActivationKey(key);
106
+ if (normalized.ok) {
107
+ broadcastToAllClients?.({ type: 'client_settings_update', key: 'activationKey', value: normalized.value });
108
+ }
109
+ }
110
+ });
111
+
104
112
  // ── Runner ───────────────────────────────────────────────────────────
105
113
 
106
114
  /**
@@ -107,8 +107,8 @@ const focusTimerHtml = `<!DOCTYPE html>
107
107
  <div class="mode-label" id="modeLabel">Work Session</div>
108
108
  <div class="timer-display" id="timerDisplay">25:00</div>
109
109
  <div class="controls">
110
- <button class="btn-primary" id="startBtn" onclick="toggleTimer()">Start</button>
111
- <button class="btn-secondary" id="resetBtn" onclick="resetTimer()">Reset</button>
110
+ <button class="btn-primary" id="startBtn">Start</button>
111
+ <button class="btn-secondary" id="resetBtn">Reset</button>
112
112
  </div>
113
113
  <div class="stats">
114
114
  <div class="stat-item">
@@ -184,6 +184,9 @@ const focusTimerHtml = `<!DOCTYPE html>
184
184
  updateDisplay();
185
185
  }
186
186
 
187
+ document.getElementById('startBtn').addEventListener('click', toggleTimer);
188
+ document.getElementById('resetBtn').addEventListener('click', resetTimer);
189
+
187
190
  updateDisplay();
188
191
  </script>
189
192
  </body>
@@ -334,8 +337,8 @@ const habitTrackerHtml = `<!DOCTYPE html>
334
337
  <h1>Habit Tracker</h1>
335
338
  </div>
336
339
  <div class="add-form">
337
- <input type="text" id="habitInput" placeholder="Add a new habit..." onkeydown="if(event.key==='Enter')addHabit()">
338
- <button class="btn-primary" onclick="addHabit()">Add</button>
340
+ <input type="text" id="habitInput" placeholder="Add a new habit...">
341
+ <button class="btn-primary" id="addHabitBtn">Add</button>
339
342
  </div>
340
343
  <div class="days-header">
341
344
  <div></div>
@@ -387,12 +390,12 @@ const habitTrackerHtml = `<!DOCTYPE html>
387
390
  html += '<div class="habit-row">';
388
391
  html += '<div style="display:flex;align-items:center;gap:8px">';
389
392
  html += '<span class="habit-name">' + escapeHtml(record.data.name) + '</span>';
390
- html += '<button class="delete-btn" onclick="deleteHabit(\\''+record.id+'\\')">x</button>';
393
+ html += '<button class="delete-btn" data-delete-habit="'+record.id+'">x</button>';
391
394
  html += '</div>';
392
395
  dates.forEach(function(date) {
393
396
  var checked = completedDates.indexOf(date) !== -1;
394
397
  html += '<div class="check-cell">';
395
- html += '<button class="check-btn' + (checked ? ' checked' : '') + '" onclick="toggleDate(\\''+record.id+'\\',\\''+date+'\\')">';
398
+ html += '<button class="check-btn' + (checked ? ' checked' : '') + '" data-toggle-habit="'+record.id+'" data-toggle-date="'+date+'">';
396
399
  html += checked ? '\\u2713' : '';
397
400
  html += '</button></div>';
398
401
  });
@@ -438,6 +441,17 @@ const habitTrackerHtml = `<!DOCTYPE html>
438
441
  });
439
442
  }
440
443
 
444
+ document.getElementById('habitInput').addEventListener('keydown', function(event) {
445
+ if (event.key === 'Enter') addHabit();
446
+ });
447
+ document.getElementById('addHabitBtn').addEventListener('click', addHabit);
448
+ document.getElementById('habitsList').addEventListener('click', function(event) {
449
+ var btn = event.target.closest('[data-delete-habit]');
450
+ if (btn) { deleteHabit(btn.getAttribute('data-delete-habit')); return; }
451
+ var toggle = event.target.closest('[data-toggle-habit]');
452
+ if (toggle) { toggleDate(toggle.getAttribute('data-toggle-habit'), toggle.getAttribute('data-toggle-date')); }
453
+ });
454
+
441
455
  initDates();
442
456
  loadHabits();
443
457
  </script>
@@ -629,9 +643,9 @@ const expenseTrackerHtml = `<!DOCTYPE html>
629
643
  <option value="entertainment">Entertainment</option>
630
644
  <option value="other">Other</option>
631
645
  </select>
632
- <input type="text" class="input-desc" id="descInput" placeholder="Description..." onkeydown="if(event.key==='Enter')addExpense()">
646
+ <input type="text" class="input-desc" id="descInput" placeholder="Description...">
633
647
  <input type="date" class="input-date" id="dateInput">
634
- <button class="btn-primary" onclick="addExpense()">Add</button>
648
+ <button class="btn-primary" id="addExpenseBtn">Add</button>
635
649
  </div>
636
650
  <div class="section-title">By Category</div>
637
651
  <div class="categories-grid" id="categoriesGrid"></div>
@@ -693,7 +707,7 @@ const expenseTrackerHtml = `<!DOCTYPE html>
693
707
  listHtml += '<div class="expense-meta">' + escapeHtml(r.data.category || 'other') + ' \\u00B7 ' + escapeHtml(r.data.date || '') + '</div>';
694
708
  listHtml += '</div>';
695
709
  listHtml += '<div class="expense-amount">$' + amt.toFixed(2) + '</div>';
696
- listHtml += '<button class="delete-btn" onclick="deleteExpense(\\''+r.id+'\\')">x</button>';
710
+ listHtml += '<button class="delete-btn" data-delete-expense="'+r.id+'">x</button>';
697
711
  listHtml += '</div>';
698
712
  });
699
713
  }
@@ -724,6 +738,15 @@ const expenseTrackerHtml = `<!DOCTYPE html>
724
738
  });
725
739
  }
726
740
 
741
+ document.getElementById('descInput').addEventListener('keydown', function(event) {
742
+ if (event.key === 'Enter') addExpense();
743
+ });
744
+ document.getElementById('addExpenseBtn').addEventListener('click', addExpense);
745
+ document.getElementById('expenseList').addEventListener('click', function(event) {
746
+ var btn = event.target.closest('[data-delete-expense]');
747
+ if (btn) { deleteExpense(btn.getAttribute('data-delete-expense')); }
748
+ });
749
+
727
750
  loadExpenses();
728
751
  </script>
729
752
  </body>
@@ -47,6 +47,7 @@
47
47
 
48
48
  import type { ExtensionCommand, ExtensionResponse } from '../browser-extension-relay/protocol.js';
49
49
  import { extensionRelayServer } from '../browser-extension-relay/server.js';
50
+ import { getGatewayInternalBaseUrl } from '../config/env.js';
50
51
  import { readHttpToken } from '../util/platform.js';
51
52
 
52
53
  // ---------------------------------------------------------------------------
@@ -133,7 +134,7 @@ async function sendRelayCommand(command: Record<string, unknown>): Promise<Exten
133
134
  );
134
135
  }
135
136
 
136
- const resp = await fetch('http://127.0.0.1:7821/v1/browser-relay/command', {
137
+ const resp = await fetch(`${getGatewayInternalBaseUrl()}/v1/browser-relay/command`, {
137
138
  method: 'POST',
138
139
  headers: {
139
140
  'Content-Type': 'application/json',