@vellumai/assistant 0.3.15 → 0.3.16

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 (290) hide show
  1. package/ARCHITECTURE.md +142 -0
  2. package/Dockerfile +1 -1
  3. package/README.md +5 -5
  4. package/docs/architecture/http-token-refresh.md +252 -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 +331 -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 +16 -14
  22. package/src/__tests__/checker.test.ts +24 -0
  23. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
  24. package/src/__tests__/config-watcher.test.ts +358 -0
  25. package/src/__tests__/conversation-pairing.test.ts +24 -24
  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 +294 -0
  37. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
  38. package/src/__tests__/guardian-action-sweep.test.ts +9 -9
  39. package/src/__tests__/guardian-outbound-http.test.ts +194 -2
  40. package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
  41. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
  42. package/src/__tests__/handlers-telegram-config.test.ts +6 -6
  43. package/src/__tests__/hooks-runner.test.ts +13 -4
  44. package/src/__tests__/ingress-routes-http.test.ts +443 -0
  45. package/src/__tests__/intent-routing.test.ts +14 -0
  46. package/src/__tests__/ipc-snapshot.test.ts +2 -5
  47. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  48. package/src/__tests__/memory-regressions.test.ts +16 -12
  49. package/src/__tests__/non-member-access-request.test.ts +282 -0
  50. package/src/__tests__/notification-decision-strategy.test.ts +136 -0
  51. package/src/__tests__/notification-routing-intent.test.ts +11 -1
  52. package/src/__tests__/notification-thread-candidates.test.ts +166 -0
  53. package/src/__tests__/recording-intent.test.ts +1 -0
  54. package/src/__tests__/recording-state-machine.test.ts +328 -17
  55. package/src/__tests__/registry.test.ts +17 -8
  56. package/src/__tests__/relay-server.test.ts +105 -0
  57. package/src/__tests__/reminder.test.ts +13 -0
  58. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
  59. package/src/__tests__/scheduler-recurrence.test.ts +50 -0
  60. package/src/__tests__/server-history-render.test.ts +8 -8
  61. package/src/__tests__/session-agent-loop.test.ts +1 -0
  62. package/src/__tests__/session-runtime-assembly.test.ts +49 -0
  63. package/src/__tests__/session-skill-tools.test.ts +1 -0
  64. package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
  65. package/src/__tests__/slack-channel-config.test.ts +230 -0
  66. package/src/__tests__/subagent-manager-notify.test.ts +4 -4
  67. package/src/__tests__/swarm-session-integration.test.ts +2 -2
  68. package/src/__tests__/system-prompt.test.ts +43 -0
  69. package/src/__tests__/task-management-tools.test.ts +3 -3
  70. package/src/__tests__/task-tools.test.ts +3 -3
  71. package/src/__tests__/trust-store.test.ts +17 -1
  72. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +491 -0
  73. package/src/__tests__/trusted-contact-multichannel.test.ts +409 -0
  74. package/src/__tests__/trusted-contact-verification.test.ts +360 -0
  75. package/src/__tests__/update-bulletin-format.test.ts +119 -0
  76. package/src/__tests__/update-bulletin-state.test.ts +129 -0
  77. package/src/__tests__/update-bulletin.test.ts +260 -0
  78. package/src/__tests__/update-template-contract.test.ts +29 -0
  79. package/src/agent/loop.ts +2 -2
  80. package/src/amazon/client.ts +2 -3
  81. package/src/calls/call-controller.ts +115 -34
  82. package/src/calls/call-conversation-messages.ts +2 -2
  83. package/src/calls/call-domain.ts +10 -3
  84. package/src/calls/call-pointer-messages.ts +17 -5
  85. package/src/calls/guardian-action-sweep.ts +77 -36
  86. package/src/calls/relay-server.ts +51 -12
  87. package/src/calls/twilio-routes.ts +3 -1
  88. package/src/calls/types.ts +1 -1
  89. package/src/calls/voice-session-bridge.ts +4 -4
  90. package/src/cli/core-commands.ts +3 -3
  91. package/src/cli/map.ts +8 -5
  92. package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
  93. package/src/config/bundled-skills/tasks/SKILL.md +1 -1
  94. package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
  95. package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
  96. package/src/config/computer-use-prompt.ts +1 -0
  97. package/src/config/core-schema.ts +16 -0
  98. package/src/config/env-registry.ts +1 -0
  99. package/src/config/env.ts +16 -1
  100. package/src/config/memory-schema.ts +5 -0
  101. package/src/config/schema.ts +4 -0
  102. package/src/config/system-prompt.ts +69 -2
  103. package/src/config/templates/BOOTSTRAP.md +1 -1
  104. package/src/config/templates/IDENTITY.md +8 -4
  105. package/src/config/templates/SOUL.md +14 -0
  106. package/src/config/templates/UPDATES.md +16 -0
  107. package/src/config/templates/USER.md +5 -1
  108. package/src/config/types.ts +1 -0
  109. package/src/config/update-bulletin-format.ts +52 -0
  110. package/src/config/update-bulletin-state.ts +49 -0
  111. package/src/config/update-bulletin.ts +82 -0
  112. package/src/config/vellum-skills/catalog.json +6 -0
  113. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
  114. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
  115. package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
  116. package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
  117. package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
  118. package/src/context/window-manager.ts +43 -3
  119. package/src/daemon/config-watcher.ts +1 -0
  120. package/src/daemon/connection-policy.ts +21 -1
  121. package/src/daemon/daemon-control.ts +164 -7
  122. package/src/daemon/date-context.ts +174 -1
  123. package/src/daemon/guardian-action-generators.ts +175 -0
  124. package/src/daemon/guardian-verification-intent.ts +120 -0
  125. package/src/daemon/handlers/apps.ts +1 -3
  126. package/src/daemon/handlers/config-channels.ts +2 -2
  127. package/src/daemon/handlers/config-heartbeat.ts +1 -1
  128. package/src/daemon/handlers/config-inbox.ts +55 -159
  129. package/src/daemon/handlers/config-ingress.ts +1 -1
  130. package/src/daemon/handlers/config-integrations.ts +1 -1
  131. package/src/daemon/handlers/config-platform.ts +1 -1
  132. package/src/daemon/handlers/config-scheduling.ts +2 -2
  133. package/src/daemon/handlers/config-slack-channel.ts +190 -0
  134. package/src/daemon/handlers/config-telegram.ts +1 -1
  135. package/src/daemon/handlers/config-twilio.ts +1 -1
  136. package/src/daemon/handlers/config-voice.ts +100 -0
  137. package/src/daemon/handlers/config.ts +3 -0
  138. package/src/daemon/handlers/misc.ts +83 -5
  139. package/src/daemon/handlers/navigate-settings.ts +27 -0
  140. package/src/daemon/handlers/recording.ts +270 -144
  141. package/src/daemon/handlers/sessions.ts +100 -17
  142. package/src/daemon/handlers/subagents.ts +3 -3
  143. package/src/daemon/handlers/work-items.ts +10 -7
  144. package/src/daemon/ipc-contract/integrations.ts +9 -1
  145. package/src/daemon/ipc-contract/messages.ts +4 -0
  146. package/src/daemon/ipc-contract/sessions.ts +1 -1
  147. package/src/daemon/ipc-contract/settings.ts +26 -0
  148. package/src/daemon/ipc-contract/shared.ts +2 -0
  149. package/src/daemon/ipc-contract/work-items.ts +1 -7
  150. package/src/daemon/ipc-contract-inventory.json +5 -1
  151. package/src/daemon/ipc-contract.ts +5 -1
  152. package/src/daemon/lifecycle.ts +306 -266
  153. package/src/daemon/recording-intent.ts +0 -41
  154. package/src/daemon/response-tier.ts +2 -2
  155. package/src/daemon/server.ts +6 -6
  156. package/src/daemon/session-agent-loop-handlers.ts +34 -9
  157. package/src/daemon/session-agent-loop.ts +15 -8
  158. package/src/daemon/session-history.ts +3 -2
  159. package/src/daemon/session-media-retry.ts +3 -0
  160. package/src/daemon/session-messaging.ts +38 -4
  161. package/src/daemon/session-notifiers.ts +2 -2
  162. package/src/daemon/session-process.ts +256 -23
  163. package/src/daemon/session-queue-manager.ts +2 -0
  164. package/src/daemon/session-runtime-assembly.ts +39 -0
  165. package/src/daemon/session-skill-tools.ts +13 -4
  166. package/src/daemon/session-tool-setup.ts +5 -6
  167. package/src/daemon/session.ts +19 -8
  168. package/src/daemon/tls-certs.ts +55 -13
  169. package/src/daemon/tool-side-effects.ts +13 -5
  170. package/src/gallery/default-gallery.ts +32 -9
  171. package/src/influencer/client.ts +2 -1
  172. package/src/memory/channel-delivery-store.ts +37 -567
  173. package/src/memory/channel-guardian-store.ts +66 -1317
  174. package/src/memory/conflict-store.ts +4 -4
  175. package/src/memory/conversation-attention-store.ts +0 -3
  176. package/src/memory/conversation-crud.ts +668 -0
  177. package/src/memory/conversation-queries.ts +361 -0
  178. package/src/memory/conversation-store.ts +45 -983
  179. package/src/memory/db-connection.ts +3 -0
  180. package/src/memory/db-init.ts +25 -0
  181. package/src/memory/delivery-channels.ts +175 -0
  182. package/src/memory/delivery-crud.ts +211 -0
  183. package/src/memory/delivery-status.ts +199 -0
  184. package/src/memory/embedding-backend.ts +70 -4
  185. package/src/memory/embedding-local.ts +12 -2
  186. package/src/memory/entity-extractor.ts +3 -8
  187. package/src/memory/fts-reconciler.ts +121 -0
  188. package/src/memory/guardian-action-store.ts +366 -3
  189. package/src/memory/guardian-approvals.ts +569 -0
  190. package/src/memory/guardian-bindings.ts +130 -0
  191. package/src/memory/guardian-rate-limits.ts +196 -0
  192. package/src/memory/guardian-verification.ts +520 -0
  193. package/src/memory/job-handlers/index-maintenance.ts +2 -1
  194. package/src/memory/job-utils.ts +8 -5
  195. package/src/memory/jobs-store.ts +66 -6
  196. package/src/memory/jobs-worker.ts +23 -1
  197. package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
  198. package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
  199. package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
  200. package/src/memory/migrations/100-core-tables.ts +1 -1
  201. package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
  202. package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
  203. package/src/memory/migrations/112-assistant-inbox.ts +1 -1
  204. package/src/memory/migrations/113-late-migrations.ts +1 -1
  205. package/src/memory/migrations/116-messages-fts.ts +13 -0
  206. package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
  207. package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
  208. package/src/memory/migrations/index.ts +8 -3
  209. package/src/memory/migrations/validate-migration-state.ts +114 -15
  210. package/src/memory/qdrant-circuit-breaker.ts +105 -0
  211. package/src/memory/retriever.ts +46 -13
  212. package/src/memory/schema-migration.ts +3 -0
  213. package/src/memory/schema.ts +25 -7
  214. package/src/memory/search/semantic.ts +8 -90
  215. package/src/notifications/README.md +1 -1
  216. package/src/notifications/broadcaster.ts +20 -2
  217. package/src/notifications/conversation-pairing.ts +3 -3
  218. package/src/notifications/decision-engine.ts +173 -8
  219. package/src/notifications/deliveries-store.ts +27 -8
  220. package/src/notifications/preferences-store.ts +7 -7
  221. package/src/notifications/thread-candidates.ts +234 -0
  222. package/src/notifications/types.ts +18 -0
  223. package/src/permissions/defaults.ts +11 -1
  224. package/src/permissions/prompter.ts +17 -0
  225. package/src/permissions/trust-store.ts +2 -0
  226. package/src/providers/failover.ts +19 -0
  227. package/src/providers/registry.ts +46 -1
  228. package/src/runtime/approval-message-composer.ts +1 -1
  229. package/src/runtime/channel-guardian-service.ts +15 -3
  230. package/src/runtime/channel-retry-sweep.ts +7 -2
  231. package/src/runtime/guardian-action-conversation-turn.ts +85 -0
  232. package/src/runtime/guardian-action-followup-executor.ts +301 -0
  233. package/src/runtime/guardian-action-message-composer.ts +245 -0
  234. package/src/runtime/guardian-outbound-actions.ts +26 -6
  235. package/src/runtime/guardian-verification-templates.ts +15 -9
  236. package/src/runtime/http-errors.ts +93 -0
  237. package/src/runtime/http-server.ts +133 -44
  238. package/src/runtime/http-types.ts +53 -0
  239. package/src/runtime/ingress-service.ts +237 -0
  240. package/src/runtime/middleware/error-handler.ts +4 -3
  241. package/src/runtime/middleware/rate-limiter.ts +160 -0
  242. package/src/runtime/middleware/request-logger.ts +71 -0
  243. package/src/runtime/middleware/twilio-validation.ts +7 -6
  244. package/src/runtime/pending-interactions.ts +12 -0
  245. package/src/runtime/routes/access-request-decision.ts +215 -0
  246. package/src/runtime/routes/app-routes.ts +25 -18
  247. package/src/runtime/routes/approval-routes.ts +18 -47
  248. package/src/runtime/routes/attachment-routes.ts +15 -41
  249. package/src/runtime/routes/call-routes.ts +20 -20
  250. package/src/runtime/routes/channel-delivery-routes.ts +6 -5
  251. package/src/runtime/routes/contact-routes.ts +4 -9
  252. package/src/runtime/routes/conversation-attention-routes.ts +2 -1
  253. package/src/runtime/routes/conversation-routes.ts +26 -57
  254. package/src/runtime/routes/debug-routes.ts +71 -0
  255. package/src/runtime/routes/events-routes.ts +3 -2
  256. package/src/runtime/routes/guardian-approval-interception.ts +221 -0
  257. package/src/runtime/routes/identity-routes.ts +14 -10
  258. package/src/runtime/routes/inbound-conversation.ts +3 -2
  259. package/src/runtime/routes/inbound-message-handler.ts +527 -62
  260. package/src/runtime/routes/ingress-routes.ts +174 -0
  261. package/src/runtime/routes/integration-routes.ts +78 -16
  262. package/src/runtime/routes/pairing-routes.ts +11 -10
  263. package/src/runtime/routes/secret-routes.ts +10 -18
  264. package/src/runtime/verification-rate-limiter.ts +83 -0
  265. package/src/schedule/schedule-store.ts +13 -1
  266. package/src/schedule/scheduler.ts +1 -1
  267. package/src/security/secret-ingress.ts +5 -2
  268. package/src/security/secret-scanner.ts +72 -6
  269. package/src/subagent/manager.ts +6 -4
  270. package/src/swarm/plan-validator.ts +4 -1
  271. package/src/tasks/task-runner.ts +3 -1
  272. package/src/tools/browser/api-map.ts +9 -6
  273. package/src/tools/calls/call-start.ts +20 -0
  274. package/src/tools/executor.ts +50 -568
  275. package/src/tools/permission-checker.ts +272 -0
  276. package/src/tools/registry.ts +14 -6
  277. package/src/tools/reminder/reminder-store.ts +7 -7
  278. package/src/tools/reminder/reminder.ts +6 -3
  279. package/src/tools/secret-detection-handler.ts +301 -0
  280. package/src/tools/subagent/message.ts +1 -1
  281. package/src/tools/system/voice-config.ts +62 -0
  282. package/src/tools/tasks/index.ts +3 -3
  283. package/src/tools/tasks/work-item-list.ts +3 -3
  284. package/src/tools/tasks/work-item-update.ts +4 -5
  285. package/src/tools/tool-approval-handler.ts +192 -0
  286. package/src/tools/tool-manifest.ts +2 -0
  287. package/src/watcher/watcher-store.ts +9 -9
  288. package/src/work-items/work-item-runner.ts +9 -6
  289. /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
  290. /package/src/memory/migrations/{027-guardian-bootstrap-token.ts → 027a-guardian-bootstrap-token.ts} +0 -0
@@ -1519,4 +1519,109 @@ describe('relay-server', () => {
1519
1519
 
1520
1520
  relay.destroy();
1521
1521
  });
1522
+
1523
+ // ── Outbound guardian verification pointer messages ─────────────────
1524
+
1525
+ test('outbound guardian verification success emits pointer to origin conversation', async () => {
1526
+ ensureConversation('conv-gv-pointer-success');
1527
+ ensureConversation('conv-gv-pointer-success-origin');
1528
+ const session = createCallSession({
1529
+ conversationId: 'conv-gv-pointer-success',
1530
+ provider: 'twilio',
1531
+ fromNumber: '+15551111111',
1532
+ toNumber: '+15559999999',
1533
+ assistantId: 'test-assistant',
1534
+ callMode: 'guardian_verification',
1535
+ guardianVerificationSessionId: 'gv-session-ptr-success',
1536
+ initiatedFromConversationId: 'conv-gv-pointer-success-origin',
1537
+ });
1538
+
1539
+ const challenge = createVerificationChallenge('test-assistant', 'voice');
1540
+ const secret = challenge.secret;
1541
+
1542
+ const { relay } = createMockWs(session.id);
1543
+
1544
+ await relay.handleMessage(JSON.stringify({
1545
+ type: 'setup',
1546
+ callSid: 'CA_gv_pointer_success',
1547
+ from: '+15551111111',
1548
+ to: '+15559999999',
1549
+ customParameters: { guardianVerificationSessionId: 'gv-session-ptr-success' },
1550
+ }));
1551
+
1552
+ expect(relay.isGuardianVerificationActive()).toBe(true);
1553
+
1554
+ // Enter the correct code via DTMF
1555
+ for (const digit of secret) {
1556
+ await relay.handleMessage(JSON.stringify({ type: 'dtmf', digit }));
1557
+ }
1558
+
1559
+ // Verification should have succeeded
1560
+ expect(relay.isGuardianVerificationActive()).toBe(false);
1561
+
1562
+ // Origin conversation should have a pointer message
1563
+ const originText = getLatestAssistantText('conv-gv-pointer-success-origin');
1564
+ expect(originText).not.toBeNull();
1565
+ expect(originText).toContain('Guardian verification');
1566
+ expect(originText).toContain('+15559999999');
1567
+ expect(originText).toContain('succeeded');
1568
+
1569
+ // Let the delayed endSession callback flush
1570
+ await new Promise((resolve) => setTimeout(resolve, 3100));
1571
+
1572
+ relay.destroy();
1573
+ });
1574
+
1575
+ test('outbound guardian verification failure emits pointer to origin conversation', async () => {
1576
+ ensureConversation('conv-gv-pointer-fail');
1577
+ ensureConversation('conv-gv-pointer-fail-origin');
1578
+ const session = createCallSession({
1579
+ conversationId: 'conv-gv-pointer-fail',
1580
+ provider: 'twilio',
1581
+ fromNumber: '+15551111111',
1582
+ toNumber: '+15559999999',
1583
+ assistantId: 'test-assistant',
1584
+ callMode: 'guardian_verification',
1585
+ guardianVerificationSessionId: 'gv-session-ptr-fail',
1586
+ initiatedFromConversationId: 'conv-gv-pointer-fail-origin',
1587
+ });
1588
+
1589
+ createVerificationChallenge('test-assistant', 'voice');
1590
+
1591
+ const { relay } = createMockWs(session.id);
1592
+
1593
+ await relay.handleMessage(JSON.stringify({
1594
+ type: 'setup',
1595
+ callSid: 'CA_gv_pointer_fail',
1596
+ from: '+15551111111',
1597
+ to: '+15559999999',
1598
+ customParameters: { guardianVerificationSessionId: 'gv-session-ptr-fail' },
1599
+ }));
1600
+
1601
+ expect(relay.isGuardianVerificationActive()).toBe(true);
1602
+
1603
+ // Enter wrong codes 3 times (max attempts = 3)
1604
+ for (let attempt = 0; attempt < 3; attempt++) {
1605
+ for (const digit of '000000') {
1606
+ await relay.handleMessage(JSON.stringify({ type: 'dtmf', digit }));
1607
+ }
1608
+ }
1609
+
1610
+ // Call should be marked as failed
1611
+ const updated = getCallSession(session.id);
1612
+ expect(updated).not.toBeNull();
1613
+ expect(updated!.status).toBe('failed');
1614
+
1615
+ // Origin conversation should have a failure pointer message
1616
+ const originText = getLatestAssistantText('conv-gv-pointer-fail-origin');
1617
+ expect(originText).not.toBeNull();
1618
+ expect(originText).toContain('Guardian verification');
1619
+ expect(originText).toContain('+15559999999');
1620
+ expect(originText).toContain('failed');
1621
+
1622
+ // Let the delayed endSession callback flush
1623
+ await new Promise((resolve) => setTimeout(resolve, 2100));
1624
+
1625
+ relay.destroy();
1626
+ });
1522
1627
  });
@@ -219,6 +219,19 @@ describe('reminder tool', () => {
219
219
  expect(result.content).toContain('Routing: multi_channel');
220
220
  });
221
221
 
222
+ test('create with routing_hints null returns error', async () => {
223
+ const future = new Date(Date.now() + 60_000).toISOString();
224
+ const result = executeReminderCreate({
225
+ fire_at: future,
226
+ label: 'Bad hints',
227
+ message: 'Null hints should fail',
228
+ routing_hints: null,
229
+ });
230
+
231
+ expect(result.isError).toBe(true);
232
+ expect(result.content).toContain('routing_hints must be a JSON object');
233
+ });
234
+
222
235
  test('create without routing fields still works (backward compat)', async () => {
223
236
  const future = new Date(Date.now() + 60_000).toISOString();
224
237
  const result = executeReminderCreate({
@@ -85,8 +85,8 @@ describe('Runtime attachment metadata', () => {
85
85
 
86
86
  // Set up conversation and messages using "self" as the assistantId
87
87
  const mapping = getOrCreateConversation(conversationKey);
88
- conversationStore.addMessage(mapping.conversationId, 'user', 'Hello');
89
- const assistantMsg = conversationStore.addMessage(
88
+ await conversationStore.addMessage(mapping.conversationId, 'user', 'Hello');
89
+ const assistantMsg = await conversationStore.addMessage(
90
90
  mapping.conversationId,
91
91
  'assistant',
92
92
  JSON.stringify([{ type: 'text', text: 'Here is a chart' }]),
@@ -124,8 +124,8 @@ describe('Runtime attachment metadata', () => {
124
124
  const conversationKey = 'test-conv-2';
125
125
 
126
126
  const mapping = getOrCreateConversation(conversationKey);
127
- conversationStore.addMessage(mapping.conversationId, 'user', 'Hello');
128
- conversationStore.addMessage(
127
+ await conversationStore.addMessage(mapping.conversationId, 'user', 'Hello');
128
+ await conversationStore.addMessage(
129
129
  mapping.conversationId,
130
130
  'assistant',
131
131
  JSON.stringify([{ type: 'text', text: 'No attachments here' }]),
@@ -36,6 +36,7 @@ import {
36
36
  } from '../schedule/schedule-store.js';
37
37
  import { startScheduler } from '../schedule/scheduler.js';
38
38
  import { createTask } from '../tasks/task-store.js';
39
+ import { getReminder, insertReminder } from '../tools/reminder/reminder-store.js';
39
40
 
40
41
  initializeDb();
41
42
 
@@ -79,6 +80,7 @@ describe('scheduler RRULE execution', () => {
79
80
  const db = getDb();
80
81
  db.run('DELETE FROM cron_runs');
81
82
  db.run('DELETE FROM cron_jobs');
83
+ db.run('DELETE FROM reminders');
82
84
  db.run('DELETE FROM task_runs');
83
85
  db.run('DELETE FROM tasks');
84
86
  db.run('DELETE FROM messages');
@@ -437,4 +439,52 @@ describe('scheduler RRULE execution', () => {
437
439
  expect(Math.abs(after!.nextRunAt - originalNextRunAt)).toBeLessThan(65000);
438
440
  expect(after!.lastRunAt).not.toBeNull();
439
441
  });
442
+
443
+ test('notify reminder passes routing metadata to notifyReminder callback', async () => {
444
+ const reminder = insertReminder({
445
+ label: 'Route this reminder',
446
+ message: 'Reminder body',
447
+ fireAt: Date.now() - 1000,
448
+ mode: 'notify',
449
+ routingIntent: 'multi_channel',
450
+ routingHints: {
451
+ requestedByUser: true,
452
+ channelMentions: ['telegram', 'sms'],
453
+ },
454
+ });
455
+
456
+ const notifyCalls: Array<{
457
+ id: string;
458
+ label: string;
459
+ message: string;
460
+ routingIntent: 'single_channel' | 'multi_channel' | 'all_channels';
461
+ routingHints: Record<string, unknown>;
462
+ }> = [];
463
+ const notifyReminder = (payload: {
464
+ id: string;
465
+ label: string;
466
+ message: string;
467
+ routingIntent: 'single_channel' | 'multi_channel' | 'all_channels';
468
+ routingHints: Record<string, unknown>;
469
+ }) => {
470
+ notifyCalls.push(payload);
471
+ };
472
+
473
+ const scheduler = startScheduler(async () => {}, notifyReminder, () => {});
474
+ await new Promise(resolve => setTimeout(resolve, 500));
475
+ scheduler.stop();
476
+
477
+ expect(notifyCalls).toHaveLength(1);
478
+ expect(notifyCalls[0]).toEqual({
479
+ id: reminder.id,
480
+ label: reminder.label,
481
+ message: reminder.message,
482
+ routingIntent: 'multi_channel',
483
+ routingHints: {
484
+ requestedByUser: true,
485
+ channelMentions: ['telegram', 'sms'],
486
+ },
487
+ });
488
+ expect(getReminder(reminder.id)?.status).toBe('fired');
489
+ });
440
490
  });
@@ -381,14 +381,14 @@ describe('getAttachmentsForMessage', () => {
381
381
  db.run('DELETE FROM conversations');
382
382
  });
383
383
 
384
- function createMessage(role: string, content: string): string {
384
+ async function createMessage(role: string, content: string): Promise<string> {
385
385
  const conv = createConversation('test');
386
- const msg = addMessage(conv.id, role, content);
386
+ const msg = await addMessage(conv.id, role, content);
387
387
  return msg.id;
388
388
  }
389
389
 
390
- test('returns attachments linked to a message', () => {
391
- const msgId = createMessage('assistant', 'Here is a chart');
390
+ test('returns attachments linked to a message', async () => {
391
+ const msgId = await createMessage('assistant', 'Here is a chart');
392
392
  const stored = uploadAttachment('chart.png', 'image/png', 'iVBOR');
393
393
  linkAttachmentToMessage(msgId, stored.id, 0);
394
394
 
@@ -404,8 +404,8 @@ describe('getAttachmentsForMessage', () => {
404
404
  expect(getAttachmentsForMessage('msg-nonexistent')).toEqual([]);
405
405
  });
406
406
 
407
- test('returns multiple attachments in position order', () => {
408
- const msgId = createMessage('assistant', 'Two files');
407
+ test('returns multiple attachments in position order', async () => {
408
+ const msgId = await createMessage('assistant', 'Two files');
409
409
  const a1 = uploadAttachment('first.txt', 'text/plain', 'AAAA');
410
410
  const a2 = uploadAttachment('second.txt', 'text/plain', 'BBBB');
411
411
 
@@ -418,8 +418,8 @@ describe('getAttachmentsForMessage', () => {
418
418
  expect(result[1].originalFilename).toBe('second.txt');
419
419
  });
420
420
 
421
- test('returns all attachments linked to a message', () => {
422
- const msgId = createMessage('assistant', 'Mixed');
421
+ test('returns all attachments linked to a message', async () => {
422
+ const msgId = await createMessage('assistant', 'Mixed');
423
423
  const a1 = uploadAttachment('a.png', 'image/png', 'AAAA');
424
424
  const a2 = uploadAttachment('b.png', 'image/png', 'BBBB');
425
425
 
@@ -30,6 +30,7 @@ mock.module('../config/loader.js', () => ({
30
30
  rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
31
31
  apiKeys: {},
32
32
  workspaceGit: { turnCommitMaxWaitMs: 10 },
33
+ ui: {},
33
34
  }),
34
35
  loadRawConfig: () => ({}),
35
36
  saveRawConfig: () => {},
@@ -10,6 +10,7 @@ import {
10
10
  injectGuardianContext,
11
11
  injectTemporalContext,
12
12
  resolveChannelCapabilities,
13
+ sanitizePttActivationKey,
13
14
  stripChannelCapabilityContext,
14
15
  stripChannelTurnContext,
15
16
  stripGuardianContext,
@@ -787,3 +788,51 @@ describe('applyRuntimeInjections with channelTurnContext', () => {
787
788
  expect(result[0].content.length).toBe(1);
788
789
  });
789
790
  });
791
+
792
+ // ---------------------------------------------------------------------------
793
+ // sanitizePttActivationKey
794
+ // ---------------------------------------------------------------------------
795
+
796
+ describe('sanitizePttActivationKey', () => {
797
+ test('returns undefined for null/undefined input', () => {
798
+ expect(sanitizePttActivationKey(null)).toBeUndefined();
799
+ expect(sanitizePttActivationKey(undefined)).toBeUndefined();
800
+ });
801
+
802
+ test('passes through valid keys', () => {
803
+ expect(sanitizePttActivationKey('fn')).toBe('fn');
804
+ expect(sanitizePttActivationKey('ctrl')).toBe('ctrl');
805
+ expect(sanitizePttActivationKey('fn_shift')).toBe('fn_shift');
806
+ expect(sanitizePttActivationKey('none')).toBe('none');
807
+ });
808
+
809
+ test('returns "unknown" for invalid keys', () => {
810
+ expect(sanitizePttActivationKey('malicious\nprompt injection')).toBe('unknown');
811
+ expect(sanitizePttActivationKey('arbitrary_value')).toBe('unknown');
812
+ expect(sanitizePttActivationKey('')).toBe('unknown');
813
+ });
814
+ });
815
+
816
+ // ---------------------------------------------------------------------------
817
+ // resolveChannelCapabilities sanitizes pttActivationKey
818
+ // ---------------------------------------------------------------------------
819
+
820
+ describe('resolveChannelCapabilities with PTT metadata', () => {
821
+ test('sanitizes valid pttActivationKey', () => {
822
+ const caps = resolveChannelCapabilities('macos', 'macos', { pttActivationKey: 'fn' });
823
+ expect(caps.pttActivationKey).toBe('fn');
824
+ });
825
+
826
+ test('sanitizes invalid pttActivationKey to unknown', () => {
827
+ const caps = resolveChannelCapabilities('macos', 'macos', { pttActivationKey: 'evil\nprompt' });
828
+ expect(caps.pttActivationKey).toBe('unknown');
829
+ });
830
+
831
+ test('passes through microphonePermissionGranted', () => {
832
+ const caps = resolveChannelCapabilities('macos', 'macos', {
833
+ pttActivationKey: 'fn',
834
+ microphonePermissionGranted: true,
835
+ });
836
+ expect(caps.microphonePermissionGranted).toBe(true);
837
+ });
838
+ });
@@ -134,6 +134,7 @@ mock.module('../tools/registry.js', () => ({
134
134
  for (const id of skillIds) {
135
135
  mockSkillRefCount.set(id, (mockSkillRefCount.get(id) ?? 0) + 1);
136
136
  }
137
+ return tools;
137
138
  },
138
139
  unregisterSkillTools: (skillId: string) => {
139
140
  mockUnregisteredSkillIds.push(skillId);
@@ -96,10 +96,18 @@ mock.module('node:fs', () => ({
96
96
  existsSync: () => true,
97
97
  }));
98
98
 
99
+ const benchmarkRegistry = new Map<string, unknown>();
99
100
  mock.module('../tools/registry.js', () => ({
100
- registerSkillTools: () => {},
101
- unregisterSkillTools: () => {},
102
- getTool: () => undefined,
101
+ registerSkillTools: (tools: Array<{ name: string; [k: string]: unknown }>) => {
102
+ for (const t of tools) benchmarkRegistry.set(t.name, t);
103
+ return tools;
104
+ },
105
+ unregisterSkillTools: (skillId: string) => {
106
+ for (const [name, t] of benchmarkRegistry) {
107
+ if ((t as { ownerSkillId?: string }).ownerSkillId === skillId) benchmarkRegistry.delete(name);
108
+ }
109
+ },
110
+ getTool: (name: string) => benchmarkRegistry.get(name),
103
111
  }));
104
112
 
105
113
  // ---------------------------------------------------------------------------
@@ -0,0 +1,230 @@
1
+ import { mkdtempSync, rmSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test';
6
+
7
+ const testDir = mkdtempSync(join(tmpdir(), 'slack-channel-cfg-test-'));
8
+
9
+ mock.module('../config/loader.js', () => ({
10
+ getConfig: () => ({}),
11
+ loadConfig: () => ({}),
12
+ loadRawConfig: () => ({}),
13
+ saveRawConfig: () => {},
14
+ saveConfig: () => {},
15
+ invalidateConfigCache: () => {},
16
+ }));
17
+
18
+ mock.module('../util/platform.js', () => ({
19
+ getRootDir: () => testDir,
20
+ getDataDir: () => testDir,
21
+ getIpcBlobDir: () => join(testDir, 'ipc-blobs'),
22
+ isMacOS: () => process.platform === 'darwin',
23
+ isLinux: () => process.platform === 'linux',
24
+ isWindows: () => process.platform === 'win32',
25
+ getSocketPath: () => join(testDir, 'test.sock'),
26
+ getPidPath: () => join(testDir, 'test.pid'),
27
+ getDbPath: () => join(testDir, 'test.db'),
28
+ getLogPath: () => join(testDir, 'test.log'),
29
+ ensureDataDir: () => {},
30
+ readHttpToken: () => undefined,
31
+ }));
32
+
33
+ mock.module('../util/logger.js', () => ({
34
+ getLogger: () => ({
35
+ info: () => {},
36
+ warn: () => {},
37
+ error: () => {},
38
+ debug: () => {},
39
+ trace: () => {},
40
+ fatal: () => {},
41
+ isDebug: () => false,
42
+ child: () => ({
43
+ info: () => {},
44
+ warn: () => {},
45
+ error: () => {},
46
+ debug: () => {},
47
+ isDebug: () => false,
48
+ }),
49
+ }),
50
+ }));
51
+
52
+ // Mock secure key storage
53
+ let secureKeyStore: Record<string, string> = {};
54
+
55
+ mock.module('../security/secure-keys.js', () => ({
56
+ getSecureKey: (account: string) => secureKeyStore[account] ?? undefined,
57
+ setSecureKey: (account: string, value: string) => {
58
+ secureKeyStore[account] = value;
59
+ return true;
60
+ },
61
+ deleteSecureKey: (account: string) => {
62
+ if (account in secureKeyStore) {
63
+ delete secureKeyStore[account];
64
+ return true;
65
+ }
66
+ return false;
67
+ },
68
+ listSecureKeys: () => Object.keys(secureKeyStore),
69
+ getBackendType: () => 'encrypted',
70
+ isDowngradedFromKeychain: () => false,
71
+ _resetBackend: () => {},
72
+ _setBackend: () => {},
73
+ }));
74
+
75
+ // Mock credential metadata store
76
+ let credentialMetadataStore: Array<{ service: string; field: string; accountInfo?: string }> = [];
77
+
78
+ mock.module('../tools/credentials/metadata-store.js', () => ({
79
+ getCredentialMetadata: (service: string, field: string) =>
80
+ credentialMetadataStore.find((m) => m.service === service && m.field === field) ?? undefined,
81
+ upsertCredentialMetadata: (service: string, field: string, policy?: Record<string, unknown>) => {
82
+ const existing = credentialMetadataStore.find((m) => m.service === service && m.field === field);
83
+ if (existing) {
84
+ if (policy?.accountInfo !== undefined) existing.accountInfo = policy.accountInfo as string;
85
+ return existing;
86
+ }
87
+ const record = { service, field, accountInfo: policy?.accountInfo as string | undefined };
88
+ credentialMetadataStore.push(record);
89
+ return record;
90
+ },
91
+ deleteCredentialMetadata: (service: string, field: string) => {
92
+ const idx = credentialMetadataStore.findIndex((m) => m.service === service && m.field === field);
93
+ if (idx !== -1) {
94
+ credentialMetadataStore.splice(idx, 1);
95
+ return true;
96
+ }
97
+ return false;
98
+ },
99
+ listCredentialMetadata: () => credentialMetadataStore,
100
+ assertMetadataWritable: () => {},
101
+ _setMetadataPath: () => {},
102
+ }));
103
+
104
+ // Mock fetch for Slack API validation
105
+ const originalFetch = globalThis.fetch;
106
+
107
+ import {
108
+ getSlackChannelConfig,
109
+ setSlackChannelConfig,
110
+ clearSlackChannelConfig,
111
+ } from '../daemon/handlers/config-slack-channel.js';
112
+
113
+ afterAll(() => {
114
+ globalThis.fetch = originalFetch;
115
+ try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
116
+ });
117
+
118
+ describe('Slack channel config handler', () => {
119
+ beforeEach(() => {
120
+ secureKeyStore = {};
121
+ credentialMetadataStore = [];
122
+ globalThis.fetch = originalFetch;
123
+ });
124
+
125
+ test('GET returns correct shape when not configured', () => {
126
+ const result = getSlackChannelConfig();
127
+ expect(result.success).toBe(true);
128
+ expect(result.hasBotToken).toBe(false);
129
+ expect(result.hasAppToken).toBe(false);
130
+ expect(result.connected).toBe(false);
131
+ });
132
+
133
+ test('GET returns connected: true when both tokens are set', () => {
134
+ secureKeyStore['credential:slack_channel:bot_token'] = 'xoxb-test';
135
+ secureKeyStore['credential:slack_channel:app_token'] = 'xapp-test';
136
+
137
+ const result = getSlackChannelConfig();
138
+ expect(result.success).toBe(true);
139
+ expect(result.hasBotToken).toBe(true);
140
+ expect(result.hasAppToken).toBe(true);
141
+ expect(result.connected).toBe(true);
142
+ });
143
+
144
+ test('GET returns metadata when available', () => {
145
+ secureKeyStore['credential:slack_channel:bot_token'] = 'xoxb-test';
146
+ credentialMetadataStore.push({
147
+ service: 'slack_channel',
148
+ field: 'bot_token',
149
+ accountInfo: JSON.stringify({
150
+ teamId: 'T123',
151
+ teamName: 'TestTeam',
152
+ botUserId: 'U_BOT',
153
+ botUsername: 'testbot',
154
+ }),
155
+ });
156
+
157
+ const result = getSlackChannelConfig();
158
+ expect(result.teamId).toBe('T123');
159
+ expect(result.teamName).toBe('TestTeam');
160
+ expect(result.botUserId).toBe('U_BOT');
161
+ expect(result.botUsername).toBe('testbot');
162
+ });
163
+
164
+ test('POST validates app token shape (xapp- prefix required)', async () => {
165
+ const result = await setSlackChannelConfig(undefined, 'invalid-token');
166
+ expect(result.success).toBe(false);
167
+ expect(result.error).toContain('xapp-');
168
+ });
169
+
170
+ test('POST accepts valid app token with xapp- prefix', async () => {
171
+ const result = await setSlackChannelConfig(undefined, 'xapp-valid-token-123');
172
+ expect(result.success).toBe(true);
173
+ expect(result.hasAppToken).toBe(true);
174
+ expect(secureKeyStore['credential:slack_channel:app_token']).toBe('xapp-valid-token-123');
175
+ });
176
+
177
+ test('POST validates bot token via Slack auth.test API', async () => {
178
+ globalThis.fetch = (async () => {
179
+ return new Response(JSON.stringify({
180
+ ok: true,
181
+ team_id: 'T_TEAM',
182
+ team: 'MyTeam',
183
+ user_id: 'U_BOT',
184
+ user: 'mybot',
185
+ }), {
186
+ status: 200,
187
+ headers: { 'content-type': 'application/json' },
188
+ });
189
+ }) as typeof globalThis.fetch;
190
+
191
+ const result = await setSlackChannelConfig('xoxb-valid-bot-token');
192
+ expect(result.success).toBe(true);
193
+ expect(result.hasBotToken).toBe(true);
194
+ expect(result.teamId).toBe('T_TEAM');
195
+ expect(result.teamName).toBe('MyTeam');
196
+ });
197
+
198
+ test('POST returns error when Slack auth.test rejects bot token', async () => {
199
+ globalThis.fetch = (async () => {
200
+ return new Response(JSON.stringify({
201
+ ok: false,
202
+ error: 'invalid_auth',
203
+ }), {
204
+ status: 200,
205
+ headers: { 'content-type': 'application/json' },
206
+ });
207
+ }) as typeof globalThis.fetch;
208
+
209
+ const result = await setSlackChannelConfig('xoxb-bad-token');
210
+ expect(result.success).toBe(false);
211
+ expect(result.error).toContain('invalid_auth');
212
+ });
213
+
214
+ test('DELETE clears credentials', () => {
215
+ secureKeyStore['credential:slack_channel:bot_token'] = 'xoxb-test';
216
+ secureKeyStore['credential:slack_channel:app_token'] = 'xapp-test';
217
+ credentialMetadataStore.push({ service: 'slack_channel', field: 'bot_token' });
218
+ credentialMetadataStore.push({ service: 'slack_channel', field: 'app_token' });
219
+
220
+ const result = clearSlackChannelConfig();
221
+ expect(result.success).toBe(true);
222
+ expect(result.hasBotToken).toBe(false);
223
+ expect(result.hasAppToken).toBe(false);
224
+ expect(result.connected).toBe(false);
225
+
226
+ expect(secureKeyStore['credential:slack_channel:bot_token']).toBeUndefined();
227
+ expect(secureKeyStore['credential:slack_channel:app_token']).toBeUndefined();
228
+ expect(credentialMetadataStore).toHaveLength(0);
229
+ });
230
+ });
@@ -393,13 +393,13 @@ describe('SubagentManager abort race guard', () => {
393
393
  });
394
394
 
395
395
  describe('SubagentManager sendMessage validation', () => {
396
- test('rejects empty content without throwing', () => {
396
+ test('rejects empty content without throwing', async () => {
397
397
  const manager = new SubagentManager();
398
398
  const subagentId = 'sub-1';
399
399
  injectFakeSubagent(manager, subagentId, makeState(subagentId));
400
400
 
401
- expect(manager.sendMessage(subagentId, '')).toBe('empty');
402
- expect(manager.sendMessage(subagentId, ' ')).toBe('empty');
403
- expect(manager.sendMessage(subagentId, '\n\t')).toBe('empty');
401
+ expect(await manager.sendMessage(subagentId, '')).toBe('empty');
402
+ expect(await manager.sendMessage(subagentId, ' ')).toBe('empty');
403
+ expect(await manager.sendMessage(subagentId, '\n\t')).toBe('empty');
404
404
  });
405
405
  });
@@ -134,7 +134,7 @@ describe('swarm through AgentLoop', () => {
134
134
  const loop = new AgentLoop(mockProvider, 'system prompt', {}, tools, toolExecutor);
135
135
  const messages: Message[] = [{ role: 'user', content: [{ type: 'text', text: 'Build a feature' }] }];
136
136
 
137
- const history = await loop.run(messages, (e) => events.push(e));
137
+ const history = await loop.run(messages, (e) => { events.push(e); });
138
138
 
139
139
  // Should have tool_use event
140
140
  const toolUseEvents = events.filter(e => e.type === 'tool_use');
@@ -186,7 +186,7 @@ describe('swarm through AgentLoop', () => {
186
186
  const messages: Message[] = [{ role: 'user', content: [{ type: 'text', text: 'go' }] }];
187
187
 
188
188
  // Should not hang or throw
189
- const history = await loop.run(messages, (e) => events.push(e), controller.signal);
189
+ const history = await loop.run(messages, (e) => { events.push(e); }, controller.signal);
190
190
  expect(history.length).toBeGreaterThanOrEqual(1);
191
191
  });
192
192
  });