@vellumai/assistant 0.3.14 → 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 (295) hide show
  1. package/ARCHITECTURE.md +142 -0
  2. package/Dockerfile +2 -2
  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-control-plane-policy.test.ts +1 -3
  40. package/src/__tests__/guardian-outbound-http.test.ts +202 -10
  41. package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
  42. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
  43. package/src/__tests__/handlers-telegram-config.test.ts +6 -6
  44. package/src/__tests__/hooks-runner.test.ts +13 -4
  45. package/src/__tests__/ingress-routes-http.test.ts +443 -0
  46. package/src/__tests__/intent-routing.test.ts +14 -0
  47. package/src/__tests__/ipc-snapshot.test.ts +2 -5
  48. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  49. package/src/__tests__/memory-regressions.test.ts +16 -12
  50. package/src/__tests__/non-member-access-request.test.ts +282 -0
  51. package/src/__tests__/notification-decision-strategy.test.ts +136 -0
  52. package/src/__tests__/notification-routing-intent.test.ts +11 -2
  53. package/src/__tests__/notification-thread-candidates.test.ts +166 -0
  54. package/src/__tests__/recording-intent-fallback.test.ts +0 -1
  55. package/src/__tests__/recording-intent-handler.test.ts +6 -3
  56. package/src/__tests__/recording-intent.test.ts +3 -2
  57. package/src/__tests__/recording-state-machine.test.ts +337 -26
  58. package/src/__tests__/registry.test.ts +17 -8
  59. package/src/__tests__/relay-server.test.ts +105 -0
  60. package/src/__tests__/reminder.test.ts +13 -0
  61. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
  62. package/src/__tests__/scheduler-recurrence.test.ts +50 -0
  63. package/src/__tests__/server-history-render.test.ts +8 -8
  64. package/src/__tests__/session-agent-loop.test.ts +1 -0
  65. package/src/__tests__/session-runtime-assembly.test.ts +49 -0
  66. package/src/__tests__/session-skill-tools.test.ts +1 -0
  67. package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
  68. package/src/__tests__/slack-channel-config.test.ts +230 -0
  69. package/src/__tests__/subagent-manager-notify.test.ts +4 -4
  70. package/src/__tests__/swarm-session-integration.test.ts +2 -2
  71. package/src/__tests__/system-prompt.test.ts +43 -0
  72. package/src/__tests__/task-management-tools.test.ts +3 -3
  73. package/src/__tests__/task-tools.test.ts +3 -3
  74. package/src/__tests__/trust-store.test.ts +17 -1
  75. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +491 -0
  76. package/src/__tests__/trusted-contact-multichannel.test.ts +409 -0
  77. package/src/__tests__/trusted-contact-verification.test.ts +360 -0
  78. package/src/__tests__/update-bulletin-format.test.ts +119 -0
  79. package/src/__tests__/update-bulletin-state.test.ts +129 -0
  80. package/src/__tests__/update-bulletin.test.ts +260 -0
  81. package/src/__tests__/update-template-contract.test.ts +29 -0
  82. package/src/agent/loop.ts +2 -2
  83. package/src/amazon/client.ts +2 -3
  84. package/src/calls/call-controller.ts +115 -34
  85. package/src/calls/call-conversation-messages.ts +2 -2
  86. package/src/calls/call-domain.ts +10 -3
  87. package/src/calls/call-pointer-messages.ts +17 -5
  88. package/src/calls/guardian-action-sweep.ts +77 -36
  89. package/src/calls/relay-server.ts +51 -12
  90. package/src/calls/twilio-routes.ts +3 -1
  91. package/src/calls/types.ts +1 -1
  92. package/src/calls/voice-session-bridge.ts +4 -4
  93. package/src/cli/core-commands.ts +3 -3
  94. package/src/cli/map.ts +8 -5
  95. package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
  96. package/src/config/bundled-skills/tasks/SKILL.md +1 -1
  97. package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
  98. package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
  99. package/src/config/computer-use-prompt.ts +1 -0
  100. package/src/config/core-schema.ts +16 -0
  101. package/src/config/env-registry.ts +1 -0
  102. package/src/config/env.ts +16 -1
  103. package/src/config/memory-schema.ts +5 -0
  104. package/src/config/schema.ts +4 -0
  105. package/src/config/system-prompt.ts +69 -2
  106. package/src/config/templates/BOOTSTRAP.md +1 -1
  107. package/src/config/templates/IDENTITY.md +8 -4
  108. package/src/config/templates/SOUL.md +14 -0
  109. package/src/config/templates/UPDATES.md +16 -0
  110. package/src/config/templates/USER.md +5 -1
  111. package/src/config/types.ts +1 -0
  112. package/src/config/update-bulletin-format.ts +52 -0
  113. package/src/config/update-bulletin-state.ts +49 -0
  114. package/src/config/update-bulletin.ts +82 -0
  115. package/src/config/vellum-skills/catalog.json +6 -0
  116. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
  117. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
  118. package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
  119. package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
  120. package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
  121. package/src/context/window-manager.ts +43 -3
  122. package/src/daemon/config-watcher.ts +1 -0
  123. package/src/daemon/connection-policy.ts +21 -1
  124. package/src/daemon/daemon-control.ts +164 -7
  125. package/src/daemon/date-context.ts +174 -1
  126. package/src/daemon/guardian-action-generators.ts +175 -0
  127. package/src/daemon/guardian-verification-intent.ts +120 -0
  128. package/src/daemon/handlers/apps.ts +1 -3
  129. package/src/daemon/handlers/config-channels.ts +8 -8
  130. package/src/daemon/handlers/config-heartbeat.ts +1 -1
  131. package/src/daemon/handlers/config-inbox.ts +55 -159
  132. package/src/daemon/handlers/config-ingress.ts +1 -1
  133. package/src/daemon/handlers/config-integrations.ts +1 -1
  134. package/src/daemon/handlers/config-platform.ts +1 -1
  135. package/src/daemon/handlers/config-scheduling.ts +2 -2
  136. package/src/daemon/handlers/config-slack-channel.ts +190 -0
  137. package/src/daemon/handlers/config-telegram.ts +1 -1
  138. package/src/daemon/handlers/config-twilio.ts +1 -1
  139. package/src/daemon/handlers/config-voice.ts +100 -0
  140. package/src/daemon/handlers/config.ts +3 -0
  141. package/src/daemon/handlers/index.ts +1 -1
  142. package/src/daemon/handlers/misc.ts +84 -6
  143. package/src/daemon/handlers/navigate-settings.ts +27 -0
  144. package/src/daemon/handlers/recording.ts +270 -144
  145. package/src/daemon/handlers/sessions.ts +107 -24
  146. package/src/daemon/handlers/subagents.ts +3 -3
  147. package/src/daemon/handlers/work-items.ts +10 -7
  148. package/src/daemon/ipc-contract/integrations.ts +9 -1
  149. package/src/daemon/ipc-contract/messages.ts +4 -0
  150. package/src/daemon/ipc-contract/sessions.ts +1 -1
  151. package/src/daemon/ipc-contract/settings.ts +26 -0
  152. package/src/daemon/ipc-contract/shared.ts +2 -0
  153. package/src/daemon/ipc-contract/work-items.ts +1 -7
  154. package/src/daemon/ipc-contract-inventory.json +5 -1
  155. package/src/daemon/ipc-contract.ts +5 -1
  156. package/src/daemon/lifecycle.ts +306 -266
  157. package/src/daemon/recording-executor.ts +1 -1
  158. package/src/daemon/recording-intent.ts +0 -41
  159. package/src/daemon/response-tier.ts +2 -2
  160. package/src/daemon/server.ts +6 -6
  161. package/src/daemon/session-agent-loop-handlers.ts +34 -9
  162. package/src/daemon/session-agent-loop.ts +15 -8
  163. package/src/daemon/session-history.ts +3 -2
  164. package/src/daemon/session-media-retry.ts +3 -0
  165. package/src/daemon/session-messaging.ts +38 -4
  166. package/src/daemon/session-notifiers.ts +2 -2
  167. package/src/daemon/session-process.ts +256 -23
  168. package/src/daemon/session-queue-manager.ts +2 -0
  169. package/src/daemon/session-runtime-assembly.ts +39 -0
  170. package/src/daemon/session-skill-tools.ts +13 -4
  171. package/src/daemon/session-tool-setup.ts +6 -7
  172. package/src/daemon/session.ts +19 -8
  173. package/src/daemon/tls-certs.ts +55 -13
  174. package/src/daemon/tool-side-effects.ts +13 -5
  175. package/src/gallery/default-gallery.ts +32 -9
  176. package/src/influencer/client.ts +2 -1
  177. package/src/memory/channel-delivery-store.ts +37 -567
  178. package/src/memory/channel-guardian-store.ts +66 -1317
  179. package/src/memory/conflict-store.ts +4 -4
  180. package/src/memory/conversation-attention-store.ts +4 -7
  181. package/src/memory/conversation-crud.ts +668 -0
  182. package/src/memory/conversation-queries.ts +361 -0
  183. package/src/memory/conversation-store.ts +45 -983
  184. package/src/memory/db-connection.ts +3 -0
  185. package/src/memory/db-init.ts +25 -0
  186. package/src/memory/delivery-channels.ts +175 -0
  187. package/src/memory/delivery-crud.ts +211 -0
  188. package/src/memory/delivery-status.ts +199 -0
  189. package/src/memory/embedding-backend.ts +70 -4
  190. package/src/memory/embedding-local.ts +12 -2
  191. package/src/memory/entity-extractor.ts +3 -8
  192. package/src/memory/fts-reconciler.ts +121 -0
  193. package/src/memory/guardian-action-store.ts +366 -3
  194. package/src/memory/guardian-approvals.ts +569 -0
  195. package/src/memory/guardian-bindings.ts +130 -0
  196. package/src/memory/guardian-rate-limits.ts +196 -0
  197. package/src/memory/guardian-verification.ts +520 -0
  198. package/src/memory/job-handlers/index-maintenance.ts +2 -1
  199. package/src/memory/job-utils.ts +8 -5
  200. package/src/memory/jobs-store.ts +66 -6
  201. package/src/memory/jobs-worker.ts +23 -1
  202. package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
  203. package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
  204. package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
  205. package/src/memory/migrations/100-core-tables.ts +1 -1
  206. package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
  207. package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
  208. package/src/memory/migrations/112-assistant-inbox.ts +1 -1
  209. package/src/memory/migrations/113-late-migrations.ts +1 -1
  210. package/src/memory/migrations/116-messages-fts.ts +13 -0
  211. package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
  212. package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
  213. package/src/memory/migrations/index.ts +8 -3
  214. package/src/memory/migrations/validate-migration-state.ts +114 -15
  215. package/src/memory/qdrant-circuit-breaker.ts +105 -0
  216. package/src/memory/retriever.ts +46 -13
  217. package/src/memory/schema-migration.ts +3 -0
  218. package/src/memory/schema.ts +25 -7
  219. package/src/memory/search/semantic.ts +8 -90
  220. package/src/notifications/README.md +1 -1
  221. package/src/notifications/broadcaster.ts +20 -2
  222. package/src/notifications/conversation-pairing.ts +3 -3
  223. package/src/notifications/decision-engine.ts +173 -8
  224. package/src/notifications/deliveries-store.ts +27 -8
  225. package/src/notifications/preferences-store.ts +7 -7
  226. package/src/notifications/thread-candidates.ts +234 -0
  227. package/src/notifications/types.ts +18 -0
  228. package/src/permissions/defaults.ts +11 -1
  229. package/src/permissions/prompter.ts +17 -0
  230. package/src/permissions/trust-store.ts +2 -0
  231. package/src/providers/failover.ts +19 -0
  232. package/src/providers/registry.ts +46 -1
  233. package/src/runtime/approval-message-composer.ts +1 -1
  234. package/src/runtime/channel-guardian-service.ts +15 -3
  235. package/src/runtime/channel-retry-sweep.ts +7 -2
  236. package/src/runtime/guardian-action-conversation-turn.ts +85 -0
  237. package/src/runtime/guardian-action-followup-executor.ts +301 -0
  238. package/src/runtime/guardian-action-message-composer.ts +245 -0
  239. package/src/runtime/guardian-outbound-actions.ts +35 -15
  240. package/src/runtime/guardian-verification-templates.ts +15 -9
  241. package/src/runtime/http-errors.ts +93 -0
  242. package/src/runtime/http-server.ts +140 -51
  243. package/src/runtime/http-types.ts +53 -0
  244. package/src/runtime/ingress-service.ts +237 -0
  245. package/src/runtime/middleware/error-handler.ts +4 -3
  246. package/src/runtime/middleware/rate-limiter.ts +160 -0
  247. package/src/runtime/middleware/request-logger.ts +71 -0
  248. package/src/runtime/middleware/twilio-validation.ts +7 -6
  249. package/src/runtime/pending-interactions.ts +12 -0
  250. package/src/runtime/routes/access-request-decision.ts +215 -0
  251. package/src/runtime/routes/app-routes.ts +25 -18
  252. package/src/runtime/routes/approval-routes.ts +18 -47
  253. package/src/runtime/routes/attachment-routes.ts +15 -41
  254. package/src/runtime/routes/call-routes.ts +20 -20
  255. package/src/runtime/routes/channel-delivery-routes.ts +6 -5
  256. package/src/runtime/routes/contact-routes.ts +4 -9
  257. package/src/runtime/routes/conversation-attention-routes.ts +5 -4
  258. package/src/runtime/routes/conversation-routes.ts +26 -57
  259. package/src/runtime/routes/debug-routes.ts +71 -0
  260. package/src/runtime/routes/events-routes.ts +3 -2
  261. package/src/runtime/routes/guardian-approval-interception.ts +221 -0
  262. package/src/runtime/routes/identity-routes.ts +14 -10
  263. package/src/runtime/routes/inbound-conversation.ts +3 -2
  264. package/src/runtime/routes/inbound-message-handler.ts +527 -62
  265. package/src/runtime/routes/ingress-routes.ts +174 -0
  266. package/src/runtime/routes/integration-routes.ts +82 -20
  267. package/src/runtime/routes/pairing-routes.ts +11 -10
  268. package/src/runtime/routes/secret-routes.ts +10 -18
  269. package/src/runtime/verification-rate-limiter.ts +83 -0
  270. package/src/schedule/schedule-store.ts +13 -1
  271. package/src/schedule/scheduler.ts +2 -2
  272. package/src/security/secret-ingress.ts +5 -2
  273. package/src/security/secret-scanner.ts +72 -6
  274. package/src/subagent/manager.ts +6 -4
  275. package/src/swarm/plan-validator.ts +4 -1
  276. package/src/tasks/task-runner.ts +3 -1
  277. package/src/tools/browser/api-map.ts +9 -6
  278. package/src/tools/calls/call-start.ts +20 -0
  279. package/src/tools/executor.ts +50 -568
  280. package/src/tools/permission-checker.ts +272 -0
  281. package/src/tools/registry.ts +14 -6
  282. package/src/tools/reminder/reminder-store.ts +7 -7
  283. package/src/tools/reminder/reminder.ts +6 -3
  284. package/src/tools/secret-detection-handler.ts +301 -0
  285. package/src/tools/subagent/message.ts +1 -1
  286. package/src/tools/system/voice-config.ts +62 -0
  287. package/src/tools/tasks/index.ts +3 -3
  288. package/src/tools/tasks/work-item-list.ts +3 -3
  289. package/src/tools/tasks/work-item-update.ts +4 -5
  290. package/src/tools/tool-approval-handler.ts +192 -0
  291. package/src/tools/tool-manifest.ts +2 -0
  292. package/src/watcher/watcher-store.ts +9 -9
  293. package/src/work-items/work-item-runner.ts +9 -6
  294. /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
  295. /package/src/memory/migrations/{027-guardian-bootstrap-token.ts → 027a-guardian-bootstrap-token.ts} +0 -0
@@ -334,14 +334,18 @@ describe('dynamic skill tool registry', () => {
334
334
  expect(getTool('sk_tool_b')?.origin).toBe('skill');
335
335
  });
336
336
 
337
- test('rejects skill tool that collides with a core tool', async () => {
337
+ test('skips skill tool that collides with a core tool without throwing', async () => {
338
338
  await initializeTools();
339
339
 
340
340
  // host_file_read is a core tool registered during init
341
341
  const colliding = makeSkillTool('host_file_read', 'rogue-skill');
342
- expect(() => registerSkillTools([colliding])).toThrow(
343
- 'collides with core tool',
344
- );
342
+ const accepted = registerSkillTools([colliding]);
343
+
344
+ // The colliding tool should be silently skipped
345
+ expect(accepted).toHaveLength(0);
346
+ // The core tool should still be in place (not overwritten)
347
+ const retrieved = getTool('host_file_read');
348
+ expect(retrieved?.origin).toBeUndefined(); // core tools have no origin
345
349
  });
346
350
 
347
351
  test('allows replacement within the same owning skill', () => {
@@ -410,7 +414,7 @@ describe('dynamic skill tool registry', () => {
410
414
  expect(skillNames).not.toContain('bash');
411
415
  });
412
416
 
413
- test('registerSkillTools is atomic no partial registration on collision', async () => {
417
+ test('registerSkillTools skips core-colliding tools but registers the rest', async () => {
414
418
  await initializeTools();
415
419
 
416
420
  const tools = [
@@ -418,9 +422,14 @@ describe('dynamic skill tool registry', () => {
418
422
  makeSkillTool('host_file_read', 'atomic-skill'), // collides with core
419
423
  ];
420
424
 
421
- expect(() => registerSkillTools(tools)).toThrow('collides with core tool');
422
- // The first tool should NOT have been registered either
423
- expect(getTool('sk_atomic_ok')).toBeUndefined();
425
+ const accepted = registerSkillTools(tools);
426
+ // Only the non-colliding tool should be accepted
427
+ expect(accepted).toHaveLength(1);
428
+ expect(accepted[0].name).toBe('sk_atomic_ok');
429
+ // The non-colliding tool should be registered
430
+ expect(getTool('sk_atomic_ok')).toBeDefined();
431
+ // The core tool should be untouched
432
+ expect(getTool('host_file_read')?.origin).toBeUndefined();
424
433
  });
425
434
  });
426
435
 
@@ -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
  });