@vellumai/assistant 0.6.0 → 0.6.2

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 (358) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +68 -15
  3. package/Dockerfile +2 -2
  4. package/bun.lock +6 -2
  5. package/docker-entrypoint.sh +42 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
  9. package/openapi.yaml +539 -4
  10. package/package.json +5 -1
  11. package/src/__tests__/anthropic-provider.test.ts +160 -95
  12. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  13. package/src/__tests__/app-executors.test.ts +47 -1
  14. package/src/__tests__/app-source-watcher.test.ts +159 -0
  15. package/src/__tests__/assistant-event-hub.test.ts +30 -0
  16. package/src/__tests__/checker.test.ts +138 -172
  17. package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
  18. package/src/__tests__/config-schema.test.ts +5 -0
  19. package/src/__tests__/context-overflow-approval.test.ts +5 -5
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +4 -51
  22. package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
  23. package/src/__tests__/conversation-directories-parse.test.ts +105 -0
  24. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  25. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  26. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  27. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  28. package/src/__tests__/conversation-wipe.test.ts +2 -6
  29. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  30. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  31. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  32. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  33. package/src/__tests__/credential-execution-approval-bridge.test.ts +0 -2
  34. package/src/__tests__/date-context.test.ts +76 -210
  35. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  36. package/src/__tests__/file-list-tool.test.ts +219 -0
  37. package/src/__tests__/first-greeting.test.ts +1 -1
  38. package/src/__tests__/heartbeat-service.test.ts +180 -3
  39. package/src/__tests__/identity-routes.test.ts +328 -0
  40. package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
  41. package/src/__tests__/injection-block.test.ts +24 -0
  42. package/src/__tests__/inline-command-runner.test.ts +7 -5
  43. package/src/__tests__/install-skill-routing.test.ts +7 -6
  44. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
  45. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  46. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  47. package/src/__tests__/llm-context-route-provider.test.ts +101 -0
  48. package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
  49. package/src/__tests__/log-export-workspace.test.ts +257 -100
  50. package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
  51. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  52. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  53. package/src/__tests__/memory-recall-log-store.test.ts +132 -0
  54. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  55. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  56. package/src/__tests__/mock-fetch.ts +87 -0
  57. package/src/__tests__/navigate-settings-tab.test.ts +14 -1
  58. package/src/__tests__/notification-broadcaster.test.ts +65 -0
  59. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  60. package/src/__tests__/onboarding-template-contract.test.ts +63 -14
  61. package/src/__tests__/parser.test.ts +32 -0
  62. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  63. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  64. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  65. package/src/__tests__/permission-mode-store.test.ts +277 -0
  66. package/src/__tests__/permission-mode.test.ts +101 -0
  67. package/src/__tests__/pkb-autoinject.test.ts +96 -0
  68. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  69. package/src/__tests__/profiler-routes.test.ts +502 -0
  70. package/src/__tests__/profiler-run-store.test.ts +441 -0
  71. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  72. package/src/__tests__/registry.test.ts +1 -1
  73. package/src/__tests__/require-fresh-approval.test.ts +0 -2
  74. package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
  75. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  76. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  77. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  78. package/src/__tests__/search-skills-unified.test.ts +4 -3
  79. package/src/__tests__/send-endpoint-busy.test.ts +42 -3
  80. package/src/__tests__/set-permission-mode.test.ts +274 -0
  81. package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
  82. package/src/__tests__/skill-memory.test.ts +2 -783
  83. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  84. package/src/__tests__/subagent-detail.test.ts +84 -0
  85. package/src/__tests__/subagent-disposal.test.ts +308 -0
  86. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  87. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  88. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  89. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  90. package/src/__tests__/subagent-tools.test.ts +464 -4
  91. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  92. package/src/__tests__/task-memory-cleanup.test.ts +12 -12
  93. package/src/__tests__/terminal-sandbox.test.ts +1 -1
  94. package/src/__tests__/terminal-tools.test.ts +16 -29
  95. package/src/__tests__/test-preload.ts +18 -0
  96. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
  97. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
  98. package/src/__tests__/tool-executor.test.ts +4 -27
  99. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  100. package/src/__tests__/top-level-renderer.test.ts +10 -13
  101. package/src/__tests__/transport-hints-queue.test.ts +77 -0
  102. package/src/__tests__/trust-store.test.ts +4 -4
  103. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
  104. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  105. package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
  106. package/src/__tests__/workspace-policy.test.ts +2 -7
  107. package/src/agent/loop.ts +6 -29
  108. package/src/approvals/guardian-request-resolvers.ts +24 -0
  109. package/src/avatar/traits-png-sync.ts +3 -3
  110. package/src/channels/types.ts +5 -0
  111. package/src/cli/__tests__/run-assistant-command.ts +56 -0
  112. package/src/cli/__tests__/unknown-command.test.ts +33 -0
  113. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  114. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  115. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  116. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  117. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  118. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  119. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  120. package/src/cli/commands/conversations.ts +1 -8
  121. package/src/cli/commands/default-action.ts +68 -1
  122. package/src/cli/commands/email.ts +584 -835
  123. package/src/cli/commands/memory.ts +1 -34
  124. package/src/cli/commands/notifications.ts +7 -2
  125. package/src/cli/commands/oauth/__tests__/connect.test.ts +27 -0
  126. package/src/cli/commands/oauth/connect.ts +25 -5
  127. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  128. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  129. package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
  130. package/src/cli/commands/routes.ts +396 -0
  131. package/src/cli/commands/skills.ts +130 -20
  132. package/src/cli/program.ts +11 -2
  133. package/src/cli.ts +1 -120
  134. package/src/config/assistant-feature-flags.ts +59 -55
  135. package/src/config/bundled-skills/app-builder/SKILL.md +91 -5
  136. package/src/config/bundled-skills/gmail/SKILL.md +13 -8
  137. package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
  138. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
  139. package/src/config/bundled-skills/messaging/SKILL.md +7 -0
  140. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  141. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  142. package/src/config/bundled-skills/settings/TOOLS.json +1 -1
  143. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  144. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  145. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  146. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
  147. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  148. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  149. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  150. package/src/config/env-registry.ts +63 -0
  151. package/src/config/feature-flag-registry.json +17 -1
  152. package/src/config/schema.ts +8 -0
  153. package/src/config/schemas/filing.ts +51 -0
  154. package/src/config/schemas/heartbeat.ts +15 -12
  155. package/src/config/schemas/memory-lifecycle.ts +12 -0
  156. package/src/config/schemas/security.ts +14 -0
  157. package/src/config/schemas/services.ts +8 -0
  158. package/src/credential-execution/approval-bridge.ts +0 -1
  159. package/src/credential-execution/managed-catalog.ts +3 -7
  160. package/src/daemon/app-source-watcher.ts +93 -0
  161. package/src/daemon/config-watcher.ts +85 -3
  162. package/src/daemon/context-overflow-approval.ts +0 -1
  163. package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
  164. package/src/daemon/conversation-agent-loop.ts +179 -65
  165. package/src/daemon/conversation-attachments.ts +0 -1
  166. package/src/daemon/conversation-history.ts +4 -19
  167. package/src/daemon/conversation-lifecycle.ts +8 -14
  168. package/src/daemon/conversation-messaging.ts +3 -0
  169. package/src/daemon/conversation-process.ts +30 -8
  170. package/src/daemon/conversation-queue-manager.ts +8 -0
  171. package/src/daemon/conversation-runtime-assembly.ts +359 -308
  172. package/src/daemon/conversation-surfaces.ts +65 -0
  173. package/src/daemon/conversation-tool-setup.ts +44 -17
  174. package/src/daemon/conversation-workspace.ts +1 -2
  175. package/src/daemon/conversation.ts +19 -3
  176. package/src/daemon/date-context.ts +26 -53
  177. package/src/daemon/first-greeting.ts +1 -1
  178. package/src/daemon/handlers/conversations.ts +5 -7
  179. package/src/daemon/handlers/shared.test.ts +143 -0
  180. package/src/daemon/handlers/shared.ts +70 -5
  181. package/src/daemon/handlers/skills.ts +11 -18
  182. package/src/daemon/lifecycle.ts +220 -158
  183. package/src/daemon/message-types/conversations.ts +29 -6
  184. package/src/daemon/message-types/messages.ts +9 -2
  185. package/src/daemon/message-types/notifications.ts +12 -0
  186. package/src/daemon/message-types/schedules.ts +1 -0
  187. package/src/daemon/message-types/settings.ts +18 -0
  188. package/src/daemon/profiler-run-store.ts +557 -0
  189. package/src/daemon/server.ts +87 -10
  190. package/src/daemon/shutdown-handlers.ts +5 -0
  191. package/src/daemon/tool-side-effects.ts +23 -3
  192. package/src/daemon/transport-hints.ts +33 -0
  193. package/src/export/transcript-formatter.ts +148 -0
  194. package/src/filing/filing-service.ts +228 -0
  195. package/src/heartbeat/heartbeat-service.ts +96 -7
  196. package/src/index.ts +1 -1
  197. package/src/mcp/client.ts +6 -0
  198. package/src/mcp/mcp-oauth-provider.ts +149 -27
  199. package/src/memory/admin.ts +33 -32
  200. package/src/memory/app-store.ts +69 -0
  201. package/src/memory/conversation-bootstrap.ts +1 -1
  202. package/src/memory/conversation-crud.ts +151 -117
  203. package/src/memory/conversation-directories.ts +39 -0
  204. package/src/memory/conversation-group-migration.ts +66 -6
  205. package/src/memory/conversation-queries.ts +58 -12
  206. package/src/memory/conversation-title-service.ts +1 -0
  207. package/src/memory/db-init.ts +182 -376
  208. package/src/memory/embedding-local.ts +1 -1
  209. package/src/memory/graph/bootstrap.ts +75 -66
  210. package/src/memory/graph/capability-seed.ts +167 -17
  211. package/src/memory/graph/consolidation.ts +38 -4
  212. package/src/memory/graph/conversation-graph-memory.ts +133 -104
  213. package/src/memory/graph/extraction-job.ts +9 -4
  214. package/src/memory/graph/extraction.ts +66 -23
  215. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  216. package/src/memory/graph/graph-search.ts +29 -15
  217. package/src/memory/graph/injection.ts +38 -8
  218. package/src/memory/graph/inspect.ts +12 -3
  219. package/src/memory/graph/retriever.ts +365 -262
  220. package/src/memory/graph/store.test.ts +48 -0
  221. package/src/memory/graph/store.ts +150 -11
  222. package/src/memory/graph/tool-handlers.ts +84 -209
  223. package/src/memory/graph/tools.ts +8 -52
  224. package/src/memory/graph/types.ts +24 -0
  225. package/src/memory/group-crud.ts +25 -9
  226. package/src/memory/job-handlers/cleanup.ts +44 -1
  227. package/src/memory/jobs-store.ts +70 -60
  228. package/src/memory/jobs-worker.ts +44 -28
  229. package/src/memory/llm-request-log-store.ts +96 -12
  230. package/src/memory/memory-recall-log-store.ts +49 -5
  231. package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
  232. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  233. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  234. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  235. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  236. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  237. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  238. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  239. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  240. package/src/memory/migrations/index.ts +8 -0
  241. package/src/memory/migrations/registry.ts +8 -0
  242. package/src/memory/schema/conversations.ts +14 -0
  243. package/src/memory/schema/infrastructure.ts +8 -1
  244. package/src/memory/schema/memory-core.ts +0 -51
  245. package/src/memory/schema/memory-graph.ts +15 -0
  246. package/src/memory/task-memory-cleanup.ts +30 -11
  247. package/src/messaging/provider.ts +1 -1
  248. package/src/notifications/broadcaster.ts +6 -0
  249. package/src/notifications/conversation-pairing.ts +12 -4
  250. package/src/notifications/copy-composer.ts +86 -0
  251. package/src/notifications/decision-engine.ts +35 -0
  252. package/src/notifications/emit-signal.ts +14 -0
  253. package/src/notifications/signal.ts +11 -0
  254. package/src/oauth/platform-connection.test.ts +2 -2
  255. package/src/oauth/seed-providers.ts +1 -0
  256. package/src/permissions/checker.ts +15 -4
  257. package/src/permissions/defaults.ts +7 -8
  258. package/src/permissions/permission-mode-store.ts +180 -0
  259. package/src/permissions/permission-mode.ts +31 -0
  260. package/src/permissions/prompter.ts +0 -2
  261. package/src/permissions/workspace-policy.ts +9 -0
  262. package/src/platform/client.ts +1 -1
  263. package/src/prompts/system-prompt.ts +59 -7
  264. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  265. package/src/prompts/templates/BOOTSTRAP.md +76 -162
  266. package/src/prompts/templates/HEARTBEAT.md +3 -1
  267. package/src/prompts/templates/SOUL.md +30 -9
  268. package/src/prompts/templates/UPDATES.md +8 -0
  269. package/src/providers/anthropic/client.ts +107 -219
  270. package/src/runtime/assistant-event-hub.ts +22 -0
  271. package/src/runtime/auth/route-policy.ts +23 -0
  272. package/src/runtime/auth/token-service.ts +8 -0
  273. package/src/runtime/http-server.ts +32 -2
  274. package/src/runtime/http-types.ts +12 -1
  275. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  276. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  277. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  278. package/src/runtime/routes/app-management-routes.ts +1 -11
  279. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  280. package/src/runtime/routes/archive-utils.ts +29 -0
  281. package/src/runtime/routes/avatar-routes.ts +2 -9
  282. package/src/runtime/routes/btw-routes.ts +14 -1
  283. package/src/runtime/routes/conversation-analysis-routes.ts +185 -0
  284. package/src/runtime/routes/conversation-management-routes.ts +1 -14
  285. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  286. package/src/runtime/routes/conversation-routes.ts +270 -44
  287. package/src/runtime/routes/group-routes.ts +22 -8
  288. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  289. package/src/runtime/routes/identity-routes.ts +53 -18
  290. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  291. package/src/runtime/routes/log-export/AGENTS.md +104 -0
  292. package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
  293. package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
  294. package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
  295. package/src/runtime/routes/log-export-routes.ts +41 -278
  296. package/src/runtime/routes/memory-item-routes.test.ts +168 -233
  297. package/src/runtime/routes/migration-routes.ts +18 -7
  298. package/src/runtime/routes/profiler-routes.ts +350 -0
  299. package/src/runtime/routes/schedule-routes.ts +27 -12
  300. package/src/runtime/routes/settings-routes.ts +95 -8
  301. package/src/runtime/routes/subagents-routes.ts +28 -7
  302. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  303. package/src/runtime/routes/user-routes.ts +41 -0
  304. package/src/runtime/routes/workspace-routes.ts +0 -1
  305. package/src/schedule/schedule-store.ts +30 -0
  306. package/src/schedule/scheduler.ts +45 -18
  307. package/src/skills/catalog-install.ts +10 -2
  308. package/src/skills/inline-command-runner.ts +12 -14
  309. package/src/skills/managed-store.ts +2 -2
  310. package/src/skills/skill-memory.ts +1 -293
  311. package/src/subagent/index.ts +13 -3
  312. package/src/subagent/manager.ts +308 -29
  313. package/src/subagent/types.ts +68 -0
  314. package/src/tasks/task-runner.ts +4 -4
  315. package/src/tools/apps/executors.ts +29 -4
  316. package/src/tools/filesystem/list.ts +93 -0
  317. package/src/tools/permission-checker.ts +78 -18
  318. package/src/tools/registry.ts +4 -0
  319. package/src/tools/schedule/create.ts +3 -0
  320. package/src/tools/schedule/list.ts +1 -0
  321. package/src/tools/schedule/update.ts +6 -0
  322. package/src/tools/secret-detection-handler.ts +0 -1
  323. package/src/tools/shared/filesystem/errors.ts +5 -0
  324. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  325. package/src/tools/shared/filesystem/types.ts +17 -0
  326. package/src/tools/shared/shell-output.ts +31 -2
  327. package/src/tools/skills/sandbox-runner.ts +3 -6
  328. package/src/tools/subagent/abort.ts +12 -2
  329. package/src/tools/subagent/message.ts +9 -2
  330. package/src/tools/subagent/notify-parent.ts +79 -0
  331. package/src/tools/subagent/read.ts +29 -8
  332. package/src/tools/subagent/resolve.ts +21 -0
  333. package/src/tools/subagent/spawn.ts +2 -0
  334. package/src/tools/subagent/status.ts +11 -1
  335. package/src/tools/system/avatar-generator.ts +3 -3
  336. package/src/tools/system/register.ts +23 -0
  337. package/src/tools/system/set-permission-mode.ts +103 -0
  338. package/src/tools/terminal/parser.ts +30 -5
  339. package/src/tools/terminal/safe-env.ts +16 -1
  340. package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
  341. package/src/tools/terminal/sandbox.ts +4 -1
  342. package/src/tools/terminal/shell.ts +3 -5
  343. package/src/tools/tool-manifest.ts +6 -0
  344. package/src/tools/types.ts +2 -3
  345. package/src/util/logger.ts +1 -1
  346. package/src/util/platform.ts +50 -17
  347. package/src/watcher/provider-types.ts +1 -1
  348. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  349. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  350. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  351. package/src/workspace/migrations/029-seed-pkb.ts +85 -0
  352. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
  353. package/src/workspace/migrations/registry.ts +6 -0
  354. package/src/workspace/top-level-renderer.ts +5 -9
  355. package/src/__tests__/cli-memory.test.ts +0 -377
  356. package/src/__tests__/clipboard.test.ts +0 -88
  357. package/src/cli/cli-memory.ts +0 -179
  358. package/src/util/clipboard.ts +0 -34
@@ -736,24 +736,24 @@ describe("invalidateAssistantInferredItemsForConversation", () => {
736
736
  expect(isConversationFailed(otherConvId)).toBe(false);
737
737
  });
738
738
 
739
- test("cancels pending extract_items jobs for the failed conversation", () => {
739
+ test("cancels pending graph_extract jobs for the failed conversation", () => {
740
740
  seedConversations();
741
741
  seedMessages();
742
742
 
743
743
  const db = getDb();
744
744
 
745
- // Enqueue extract_items jobs for messages in the target conversation
746
- enqueueMemoryJob("extract_items", {
747
- messageId: "msg-task-1",
745
+ // Enqueue graph_extract jobs for the target conversation
746
+ enqueueMemoryJob("graph_extract", {
747
+ conversationId: convId,
748
748
  scopeId: "default",
749
749
  });
750
- enqueueMemoryJob("extract_items", {
751
- messageId: "msg-task-2",
750
+ enqueueMemoryJob("graph_extract", {
751
+ conversationId: convId,
752
752
  scopeId: "default",
753
753
  });
754
- // Enqueue an extract_items job for a message in a different conversation
755
- enqueueMemoryJob("extract_items", {
756
- messageId: "msg-other",
754
+ // Enqueue a graph_extract job for a different conversation
755
+ enqueueMemoryJob("graph_extract", {
756
+ conversationId: otherConvId,
757
757
  scopeId: "default",
758
758
  });
759
759
 
@@ -761,7 +761,7 @@ describe("invalidateAssistantInferredItemsForConversation", () => {
761
761
  const pendingBefore = db
762
762
  .select()
763
763
  .from(memoryJobs)
764
- .where(eq(memoryJobs.type, "extract_items"))
764
+ .where(eq(memoryJobs.type, "graph_extract"))
765
765
  .all();
766
766
  expect(pendingBefore.filter((j) => j.status === "pending")).toHaveLength(3);
767
767
 
@@ -771,7 +771,7 @@ describe("invalidateAssistantInferredItemsForConversation", () => {
771
771
  const allJobs = db
772
772
  .select()
773
773
  .from(memoryJobs)
774
- .where(eq(memoryJobs.type, "extract_items"))
774
+ .where(eq(memoryJobs.type, "graph_extract"))
775
775
  .all();
776
776
  const failedJobs = allJobs.filter((j) => j.status === "failed");
777
777
  const pendingJobs = allJobs.filter((j) => j.status === "pending");
@@ -785,6 +785,6 @@ describe("invalidateAssistantInferredItemsForConversation", () => {
785
785
  // The job for the other conversation should remain pending
786
786
  expect(pendingJobs).toHaveLength(1);
787
787
  const payload = JSON.parse(pendingJobs[0].payload);
788
- expect(payload.messageId).toBe("msg-other");
788
+ expect(payload.conversationId).toBe(otherConvId);
789
789
  });
790
790
  });
@@ -2,7 +2,7 @@ import * as realChildProcess from "node:child_process";
2
2
  import * as realFs from "node:fs";
3
3
  import { beforeEach, describe, expect, mock, test } from "bun:test";
4
4
 
5
- import type { SandboxConfig } from "../config/schema.js";
5
+ import type { SandboxConfig } from "../tools/terminal/sandbox.js";
6
6
 
7
7
  let platform = "linux";
8
8
 
@@ -1,3 +1,4 @@
1
+ import { existsSync, readFileSync } from "node:fs";
1
2
  import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
3
 
3
4
  import type { ShellOutputResult } from "../tools/shared/shell-output.js";
@@ -64,9 +65,13 @@ mock.module("../tools/network/script-proxy/index.js", () => ({
64
65
 
65
66
  // ── Imports (after mocks) ───────────────────────────────────────────────────
66
67
 
67
- import type { SandboxConfig } from "../config/schema.js";
68
68
  import { parse } from "../tools/terminal/parser.js";
69
- import { buildSanitizedEnv } from "../tools/terminal/safe-env.js";
69
+ import {
70
+ ALWAYS_INJECTED_ENV_VARS,
71
+ buildSanitizedEnv,
72
+ SAFE_ENV_VARS,
73
+ } from "../tools/terminal/safe-env.js";
74
+ import type { SandboxConfig } from "../tools/terminal/sandbox.js";
70
75
  import { wrapCommand } from "../tools/terminal/sandbox.js";
71
76
  import { ToolError } from "../util/errors.js";
72
77
 
@@ -450,30 +455,7 @@ describe("buildSanitizedEnv", () => {
450
455
  test("result is a plain object with no prototype-inherited secrets", () => {
451
456
  const env = buildSanitizedEnv();
452
457
  const keys = Object.keys(env);
453
- const safeKeys = [
454
- "PATH",
455
- "HOME",
456
- "TERM",
457
- "LANG",
458
- "EDITOR",
459
- "SHELL",
460
- "USER",
461
- "TMPDIR",
462
- "LC_ALL",
463
- "LC_CTYPE",
464
- "XDG_RUNTIME_DIR",
465
- "DISPLAY",
466
- "COLORTERM",
467
- "TERM_PROGRAM",
468
- "SSH_AUTH_SOCK",
469
- "SSH_AGENT_PID",
470
- "GPG_TTY",
471
- "GNUPGHOME",
472
- "VELLUM_DEV",
473
- "INTERNAL_GATEWAY_BASE_URL",
474
- "VELLUM_DATA_DIR",
475
- "VELLUM_WORKSPACE_DIR",
476
- ];
458
+ const safeKeys: string[] = [...SAFE_ENV_VARS, ...ALWAYS_INJECTED_ENV_VARS];
477
459
  for (const key of keys) {
478
460
  expect(safeKeys).toContain(key);
479
461
  }
@@ -722,9 +704,14 @@ describe("formatShellOutput", () => {
722
704
  });
723
705
 
724
706
  test("truncates very long output", () => {
725
- const longOutput = "x".repeat(60_000);
707
+ const longOutput = "x".repeat(30_000);
726
708
  const result = formatShellOutput(longOutput, "", 0, false, 120);
727
- expect(result.content).toContain('<output_truncated limit="50K" />');
728
- expect(result.content.length).toBeLessThan(60_000);
709
+ expect(result.content).toContain('limit="20K"');
710
+ // Extract the file="..." attribute from the truncation tag
711
+ const fileMatch = result.content.match(/file="([^"]+)"/);
712
+ expect(fileMatch).not.toBeNull();
713
+ const filePath = fileMatch![1];
714
+ expect(existsSync(filePath)).toBe(true);
715
+ expect(readFileSync(filePath, "utf-8")).toBe(longOutput);
729
716
  });
730
717
  });
@@ -22,10 +22,28 @@ const testDir = realpathSync(
22
22
  mkdtempSync(join(tmpdir(), "vellum-test-workspace-")),
23
23
  );
24
24
  process.env.VELLUM_WORKSPACE_DIR = testDir;
25
+ process.env.VELLUM_PLATFORM_URL = "https://test-platform.vellum.ai";
26
+ process.exitCode = 0;
27
+
28
+ // Prevent tests from routing credential writes through the real CES
29
+ // (Credential Execution Service). Without this, setSecureKeyAsync() in
30
+ // containerized environments writes to the live credential store.
31
+ const savedIsContainerized = process.env.IS_CONTAINERIZED;
32
+ const savedCesCredentialUrl = process.env.CES_CREDENTIAL_URL;
33
+ delete process.env.IS_CONTAINERIZED;
34
+ delete process.env.CES_CREDENTIAL_URL;
25
35
 
26
36
  afterAll(() => {
27
37
  resetDb();
38
+ process.exitCode = 0;
28
39
  delete process.env.VELLUM_WORKSPACE_DIR;
40
+ delete process.env.VELLUM_PLATFORM_URL;
41
+ if (savedIsContainerized !== undefined) {
42
+ process.env.IS_CONTAINERIZED = savedIsContainerized;
43
+ }
44
+ if (savedCesCredentialUrl !== undefined) {
45
+ process.env.CES_CREDENTIAL_URL = savedCesCredentialUrl;
46
+ }
29
47
  try {
30
48
  rmSync(testDir, { recursive: true, force: true });
31
49
  } catch {
@@ -37,7 +37,6 @@ describe("createToolDomainEventPublisher", () => {
37
37
  reason: "needs approval",
38
38
  allowlistOptions: [],
39
39
  scopeOptions: [],
40
- sandboxed: true,
41
40
  });
42
41
 
43
42
  await publish({
@@ -41,7 +41,6 @@ let checkerDecision: "allow" | "prompt" | "deny" = "allow";
41
41
  let checkerReason = "allowed";
42
42
  let checkerRisk = "low";
43
43
  let promptDecision: "allow" | "always_allow" | "deny" | "always_deny" = "allow";
44
- let sandboxed = false;
45
44
  let fakeToolResult: ToolExecutionResult = { content: "ok", isError: false };
46
45
  let toolThrow: Error | null = null;
47
46
 
@@ -151,7 +150,7 @@ mock.module("../tools/shared/filesystem/path-policy.js", () => ({
151
150
  }));
152
151
 
153
152
  mock.module("../tools/terminal/sandbox.js", () => ({
154
- wrapCommand: () => ({ command: "", sandboxed }),
153
+ wrapCommand: () => ({ command: "", sandboxed: false }),
155
154
  }));
156
155
 
157
156
  import { PermissionPrompter } from "../permissions/prompter.js";
@@ -193,7 +192,6 @@ describe("ToolExecutor lifecycle events", () => {
193
192
  checkerReason = "allowed";
194
193
  checkerRisk = "low";
195
194
  promptDecision = "allow";
196
- sandboxed = false;
197
195
  fakeToolResult = { content: "ok", isError: false };
198
196
  toolThrow = null;
199
197
  });
@@ -231,7 +229,6 @@ describe("ToolExecutor lifecycle events", () => {
231
229
  checkerReason = "medium risk: requires approval";
232
230
  checkerRisk = "medium";
233
231
  promptDecision = "deny";
234
- sandboxed = true;
235
232
 
236
233
  const events: ToolLifecycleEvent[] = [];
237
234
  const executor = new ToolExecutor(makePrompter());
@@ -256,7 +253,6 @@ describe("ToolExecutor lifecycle events", () => {
256
253
  expect(promptEvent.executionTarget).toBe("sandbox");
257
254
  expect(promptEvent.riskLevel).toBe("medium");
258
255
  expect(promptEvent.reason).toBe("medium risk: requires approval");
259
- expect(promptEvent.sandboxed).toBe(true);
260
256
  expect(promptEvent.allowlistOptions).toEqual([
261
257
  { label: "exact", description: "exact", pattern: "exact" },
262
258
  ]);
@@ -276,7 +272,6 @@ describe("ToolExecutor lifecycle events", () => {
276
272
  checkerDecision = "prompt";
277
273
  checkerReason = "guardrail prompt";
278
274
  checkerRisk = "high";
279
- sandboxed = true;
280
275
 
281
276
  const events: ToolLifecycleEvent[] = [];
282
277
  const executor = new ToolExecutor(
@@ -580,7 +575,6 @@ describe("ToolExecutor lifecycle events", () => {
580
575
  checkerReason = "Matched trust rule";
581
576
  checkerRisk = "low";
582
577
  promptDecision = "allow";
583
- sandboxed = true;
584
578
 
585
579
  const events: ToolLifecycleEvent[] = [];
586
580
  const executor = new ToolExecutor(makePrompter());
@@ -602,7 +596,6 @@ describe("ToolExecutor lifecycle events", () => {
602
596
  expect(promptEvent.reason).toBe(
603
597
  "Private conversation: side-effect tools require explicit approval",
604
598
  );
605
- expect(promptEvent.sandboxed).toBe(true);
606
599
  });
607
600
 
608
601
  test("no permission_prompt event for read-only tool even with forcePromptSideEffects", async () => {
@@ -1949,7 +1949,6 @@ describe("ToolExecutor persistentDecisionsAllowed contract", () => {
1949
1949
  _allowlistOptions: AllowlistOption[],
1950
1950
  _scopeOptions: ScopeOption[],
1951
1951
  _diff: unknown,
1952
- _sandboxed: unknown,
1953
1952
  _conversationId: unknown,
1954
1953
  _executionTarget: unknown,
1955
1954
  persistentDecisionsAllowed: boolean | undefined,
@@ -2072,7 +2071,8 @@ describe("ToolExecutor persistentDecisionsAllowed contract", () => {
2072
2071
  // ---------------------------------------------------------------------------
2073
2072
 
2074
2073
  // Import the real buildSanitizedEnv (not mocked) for baseline credential tests
2075
- const { buildSanitizedEnv } = await import("../tools/terminal/safe-env.js");
2074
+ const { buildSanitizedEnv, SAFE_ENV_VARS, ALWAYS_INJECTED_ENV_VARS } =
2075
+ await import("../tools/terminal/safe-env.js");
2076
2076
 
2077
2077
  describe("buildSanitizedEnv — baseline: credential exclusion", () => {
2078
2078
  // Credential-like env vars that must never appear in the sanitized env.
@@ -2130,33 +2130,10 @@ describe("buildSanitizedEnv — baseline: credential exclusion", () => {
2130
2130
  });
2131
2131
 
2132
2132
  test("sanitized env only contains keys from the allowlist", () => {
2133
- const SAFE_ENV_VARS = [
2134
- "PATH",
2135
- "HOME",
2136
- "TERM",
2137
- "LANG",
2138
- "EDITOR",
2139
- "SHELL",
2140
- "USER",
2141
- "TMPDIR",
2142
- "LC_ALL",
2143
- "LC_CTYPE",
2144
- "XDG_RUNTIME_DIR",
2145
- "DISPLAY",
2146
- "COLORTERM",
2147
- "TERM_PROGRAM",
2148
- "SSH_AUTH_SOCK",
2149
- "SSH_AGENT_PID",
2150
- "GPG_TTY",
2151
- "GNUPGHOME",
2152
- "INTERNAL_GATEWAY_BASE_URL",
2153
- "VELLUM_DATA_DIR",
2154
- "VELLUM_WORKSPACE_DIR",
2155
- ];
2156
-
2133
+ const allowed: string[] = [...SAFE_ENV_VARS, ...ALWAYS_INJECTED_ENV_VARS];
2157
2134
  const env = buildSanitizedEnv();
2158
2135
  for (const key of Object.keys(env)) {
2159
- expect(SAFE_ENV_VARS).toContain(key);
2136
+ expect(allowed).toContain(key);
2160
2137
  }
2161
2138
  });
2162
2139
  });
@@ -31,6 +31,7 @@ mock.module("../memory/app-store.js", () => ({
31
31
  getApp: mock(() => null),
32
32
  getAppDirPath: mock(() => ""),
33
33
  isMultifileApp: mock(() => false),
34
+ resolveAppIdFromPath: mock(() => null),
34
35
  }));
35
36
  mock.module("../services/published-app-updater.js", () => ({
36
37
  updatePublishedAppDeployment: mock(() => Promise.resolve()),
@@ -15,11 +15,11 @@ describe("renderWorkspaceTopLevelContext", () => {
15
15
  const result = renderWorkspaceTopLevelContext(snapshot);
16
16
  expect(result).toBe(
17
17
  [
18
- "<workspace_top_level>",
18
+ "<workspace>",
19
19
  "Root: /sandbox",
20
20
  "Directories: lib, src, tests",
21
21
  "Files: README.md, package.json",
22
- "</workspace_top_level>",
22
+ "</workspace>",
23
23
  ].join("\n"),
24
24
  );
25
25
  });
@@ -61,11 +61,11 @@ describe("renderWorkspaceTopLevelContext", () => {
61
61
  const result = renderWorkspaceTopLevelContext(snapshot);
62
62
  expect(result).toBe(
63
63
  [
64
- "<workspace_top_level>",
64
+ "<workspace>",
65
65
  "Root: /empty",
66
66
  "Directories: ",
67
67
  "Files: ",
68
- "</workspace_top_level>",
68
+ "</workspace>",
69
69
  ].join("\n"),
70
70
  );
71
71
  });
@@ -92,8 +92,8 @@ describe("renderWorkspaceTopLevelContext", () => {
92
92
  };
93
93
 
94
94
  const result = renderWorkspaceTopLevelContext(snapshot);
95
- expect(result.startsWith("<workspace_top_level>")).toBe(true);
96
- expect(result.endsWith("</workspace_top_level>")).toBe(true);
95
+ expect(result.startsWith("<workspace>")).toBe(true);
96
+ expect(result.endsWith("</workspace>")).toBe(true);
97
97
  });
98
98
 
99
99
  test("includes hidden directories", () => {
@@ -124,7 +124,7 @@ describe("renderWorkspaceTopLevelContext", () => {
124
124
  expect(result).toContain("Files: a.txt, b.txt");
125
125
  });
126
126
 
127
- test("renders current conversation and attachment paths when provided", () => {
127
+ test("renders attachment path when provided", () => {
128
128
  const snapshot: TopLevelSnapshot = {
129
129
  rootPath: "/sandbox",
130
130
  directories: ["src"],
@@ -133,16 +133,13 @@ describe("renderWorkspaceTopLevelContext", () => {
133
133
  };
134
134
 
135
135
  const result = renderWorkspaceTopLevelContext(snapshot, {
136
- currentConversationPath: "conversations/2026-03-19T12-00-00.000Z_conv-1/",
137
- currentConversationAttachmentsPath:
136
+ conversationAttachmentsPath:
138
137
  "conversations/2026-03-19T12-00-00.000Z_conv-1/attachments/",
139
138
  });
140
139
 
141
140
  expect(result).toContain(
142
- "Current conversation folder: conversations/2026-03-19T12-00-00.000Z_conv-1/",
143
- );
144
- expect(result).toContain(
145
- "Attachment files: conversations/2026-03-19T12-00-00.000Z_conv-1/attachments/",
141
+ "Current conversation attachments: conversations/2026-03-19T12-00-00.000Z_conv-1/attachments/",
146
142
  );
143
+ expect(result).not.toContain("Current conversation folder");
147
144
  });
148
145
  });
@@ -0,0 +1,77 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import type {
4
+ MacosTransportMetadata,
5
+ NonMacosTransportMetadata,
6
+ } from "../daemon/message-types/conversations.js";
7
+ import { buildTransportHints } from "../daemon/transport-hints.js";
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // buildTransportHints
11
+ // ---------------------------------------------------------------------------
12
+
13
+ describe("buildTransportHints", () => {
14
+ test("produces correct hints for macOS transport", () => {
15
+ const transport: MacosTransportMetadata = {
16
+ channelId: "vellum",
17
+ interfaceId: "macos",
18
+ hostHomeDir: "/Users/alice",
19
+ hostUsername: "alice",
20
+ };
21
+
22
+ const hints = buildTransportHints(transport);
23
+
24
+ expect(hints).toContain("User is messaging from interface: macos");
25
+ expect(hints).toContain("Host home directory: /Users/alice");
26
+ expect(hints).toContain("Host username: alice");
27
+ expect(hints).toHaveLength(3);
28
+ });
29
+
30
+ test("produces correct hints for non-macOS transport", () => {
31
+ const transport: NonMacosTransportMetadata = {
32
+ channelId: "vellum",
33
+ interfaceId: "ios",
34
+ };
35
+
36
+ const hints = buildTransportHints(transport);
37
+
38
+ expect(hints).toContain("User is messaging from interface: ios");
39
+ expect(hints).toHaveLength(1);
40
+ // Should not include host environment hints
41
+ expect(hints.some((h) => h.includes("Host home directory"))).toBe(false);
42
+ expect(hints.some((h) => h.includes("Host username"))).toBe(false);
43
+ });
44
+
45
+ test("includes client-provided hints", () => {
46
+ const transport: MacosTransportMetadata = {
47
+ channelId: "vellum",
48
+ interfaceId: "macos",
49
+ hostHomeDir: "/Users/bob",
50
+ hostUsername: "bob",
51
+ hints: ["custom hint"],
52
+ };
53
+
54
+ const hints = buildTransportHints(transport);
55
+
56
+ expect(hints).toContain("User is messaging from interface: macos");
57
+ expect(hints).toContain("Host home directory: /Users/bob");
58
+ expect(hints).toContain("Host username: bob");
59
+ expect(hints).toContain("custom hint");
60
+ expect(hints).toHaveLength(4);
61
+ });
62
+
63
+ test("handles missing optional fields on macOS transport", () => {
64
+ const transport: MacosTransportMetadata = {
65
+ channelId: "vellum",
66
+ interfaceId: "macos",
67
+ };
68
+
69
+ const hints = buildTransportHints(transport);
70
+
71
+ expect(hints).toContain("User is messaging from interface: macos");
72
+ // Without hostHomeDir and hostUsername, only the interface hint is present
73
+ expect(hints).toHaveLength(1);
74
+ expect(hints.some((h) => h.includes("Host home directory"))).toBe(false);
75
+ expect(hints.some((h) => h.includes("Host username"))).toBe(false);
76
+ });
77
+ });
@@ -909,8 +909,8 @@ describe("Trust Store", () => {
909
909
  expect(match!.id).toBe("default:allow-bash-rm-bootstrap");
910
910
  expect(match!.decision).toBe("allow");
911
911
  expect(match!.allowHighRisk).toBe(true);
912
- // Outside workspace, the bootstrap rule doesn't match — with sandbox
913
- // disabled (the default), there is no catch-all bash allow rule either.
912
+ // Outside workspace, the bootstrap rule doesn't match — without
913
+ // IS_CONTAINERIZED there is no catch-all bash allow rule either.
914
914
  const other = findHighestPriorityRule(
915
915
  "bash",
916
916
  ["rm BOOTSTRAP.md"],
@@ -930,8 +930,8 @@ describe("Trust Store", () => {
930
930
  expect(match!.id).toBe("default:allow-bash-rm-updates");
931
931
  expect(match!.decision).toBe("allow");
932
932
  expect(match!.allowHighRisk).toBe(true);
933
- // Outside workspace, should NOT match the updates rule — with sandbox
934
- // disabled (the default), there is no catch-all bash allow rule either.
933
+ // Outside workspace, should NOT match the updates rule — without
934
+ // IS_CONTAINERIZED there is no catch-all bash allow rule either.
935
935
  const other = findHighestPriorityRule(
936
936
  "bash",
937
937
  ["rm UPDATES.md"],
@@ -133,7 +133,7 @@ describe("trusted contact lifecycle notification signals", () => {
133
133
  resetState();
134
134
  });
135
135
 
136
- test("guardian deny emits guardian_decision and denied signals", async () => {
136
+ test("guardian deny emits guardian_decision and denied signals with display names", async () => {
137
137
  // Set up guardian binding and member record (guardians must pass ACL)
138
138
  createGuardianBinding({
139
139
  channel: "telegram",
@@ -148,6 +148,17 @@ describe("trusted contact lifecycle notification signals", () => {
148
148
  externalChatId: "guardian-chat-789",
149
149
  status: "active",
150
150
  policy: "allow",
151
+ displayName: "Guardian Bob",
152
+ });
153
+
154
+ // Set up requester contact with a display name so payloads are enriched
155
+ upsertContactChannel({
156
+ sourceChannel: "telegram",
157
+ externalUserId: "requester-user-456",
158
+ externalChatId: "requester-chat-456",
159
+ status: "pending",
160
+ policy: "ask",
161
+ displayName: "Alice Requester",
151
162
  });
152
163
 
153
164
  const testRequestId = `req-deny-${Date.now()}`;
@@ -199,11 +210,15 @@ describe("trusted contact lifecycle notification signals", () => {
199
210
  expect(gdPayload.decision).toBe("denied");
200
211
  expect(gdPayload.requesterExternalUserId).toBe("requester-user-456");
201
212
  expect(gdPayload.decidedByExternalUserId).toBe("guardian-user-789");
213
+ expect(gdPayload.requesterDisplayName).toBe("Alice Requester");
214
+ expect(gdPayload.decidedByDisplayName).toBe("Guardian Bob");
202
215
 
203
216
  // Verify denied payload
204
217
  const dPayload = deniedSignals[0].contextPayload as Record<string, unknown>;
205
218
  expect(dPayload.decision).toBe("denied");
206
219
  expect(dPayload.requesterExternalUserId).toBe("requester-user-456");
220
+ expect(dPayload.requesterDisplayName).toBe("Alice Requester");
221
+ expect(dPayload.decidedByDisplayName).toBe("Guardian Bob");
207
222
 
208
223
  // Verify deduplication keys are distinct
209
224
  expect(guardianDecisionSignals[0].dedupeKey).toContain(
@@ -212,7 +227,7 @@ describe("trusted contact lifecycle notification signals", () => {
212
227
  expect(deniedSignals[0].dedupeKey).toContain("trusted-contact:denied:");
213
228
  });
214
229
 
215
- test("guardian approve emits guardian_decision and verification_sent signals", async () => {
230
+ test("guardian approve emits guardian_decision and verification_sent signals with display names", async () => {
216
231
  // Set up guardian binding and member record (guardians must pass ACL)
217
232
  createGuardianBinding({
218
233
  channel: "telegram",
@@ -227,6 +242,17 @@ describe("trusted contact lifecycle notification signals", () => {
227
242
  externalChatId: "guardian-chat-789",
228
243
  status: "active",
229
244
  policy: "allow",
245
+ displayName: "Guardian Bob",
246
+ });
247
+
248
+ // Set up requester contact with a display name
249
+ upsertContactChannel({
250
+ sourceChannel: "telegram",
251
+ externalUserId: "requester-user-456",
252
+ externalChatId: "requester-chat-456",
253
+ status: "pending",
254
+ policy: "ask",
255
+ displayName: "Alice Requester",
230
256
  });
231
257
 
232
258
  const testRequestId = `req-approve-${Date.now()}`;
@@ -276,6 +302,8 @@ describe("trusted contact lifecycle notification signals", () => {
276
302
  const vsPayload = vsSignal.contextPayload as Record<string, unknown>;
277
303
  expect(vsPayload.requesterExternalUserId).toBe("requester-user-456");
278
304
  expect(vsPayload.verificationSessionId).toBeDefined();
305
+ expect(vsPayload.requesterDisplayName).toBe("Alice Requester");
306
+ expect(vsPayload.decidedByDisplayName).toBe("Guardian Bob");
279
307
  expect(
280
308
  (vsSignal.attentionHints as Record<string, unknown>).visibleInSourceNow,
281
309
  ).toBe(true);
@@ -340,6 +368,92 @@ describe("trusted contact lifecycle notification signals", () => {
340
368
  // guardian_decision and denied — both keyed on approval.id
341
369
  expect(signals.length).toBe(2);
342
370
  });
371
+
372
+ test("display name fields fall back to null when contacts have no display name", async () => {
373
+ // Set up guardian binding — createGuardianBinding creates a contact with
374
+ // displayName defaulting to the externalUserId. We need to verify the
375
+ // null-fallback path for display name resolution, so we use a guardian
376
+ // whose external user ID does NOT have a matching contact_channels row.
377
+ //
378
+ // Strategy: create the guardian binding normally (so the sender is
379
+ // classified as trustClass="guardian"), then use a DIFFERENT external
380
+ // user ID in the approval request's guardianExternalUserId field. The
381
+ // decidedByExternalUserId comes from actorExternalId (the real guardian),
382
+ // which DOES have a contact. To truly get null, we clear the guardian
383
+ // contact's displayName to empty string after creation — the signal
384
+ // enrichment code treats empty string the same as null for display
385
+ // purposes.
386
+ createGuardianBinding({
387
+ channel: "telegram",
388
+ guardianExternalUserId: "guardian-noname-111",
389
+ guardianDeliveryChatId: "guardian-chat-111",
390
+ guardianPrincipalId: "guardian-noname-111",
391
+ verifiedVia: "test",
392
+ });
393
+
394
+ // Clear the guardian contact's displayName to empty string so the
395
+ // display name resolution returns a falsy value. createGuardianBinding
396
+ // defaults displayName to the externalUserId, which would be a non-empty
397
+ // string and defeat the purpose of this test.
398
+ const db = getDb();
399
+ db.run("UPDATE contacts SET display_name = '' WHERE role = 'guardian'");
400
+
401
+ // Do NOT create a requester contact — display name should resolve to null
402
+
403
+ const testRequestId = `req-noname-${Date.now()}`;
404
+
405
+ createApprovalRequest({
406
+ runId: `ingress-access-request-${Date.now()}`,
407
+ requestId: testRequestId,
408
+ conversationId: "access-req-telegram-requester-noname-222",
409
+ channel: "telegram",
410
+ requesterExternalUserId: "requester-noname-222",
411
+ requesterChatId: "requester-chat-222",
412
+ guardianExternalUserId: "guardian-noname-111",
413
+ guardianChatId: "guardian-chat-111",
414
+ toolName: "ingress_access_request",
415
+ riskLevel: "access_request",
416
+ reason: "Unknown user requesting access",
417
+ expiresAt: Date.now() + GUARDIAN_APPROVAL_TTL_MS,
418
+ });
419
+
420
+ // Guardian denies via callback button
421
+ const guardianReq = buildInboundRequest({
422
+ conversationExternalId: "guardian-chat-111",
423
+ actorExternalId: "guardian-noname-111",
424
+ actorDisplayName: "Guardian",
425
+ content: "",
426
+ callbackData: `apr:${testRequestId}:reject`,
427
+ });
428
+
429
+ await handleChannelInbound(guardianReq, undefined, TEST_BEARER_TOKEN);
430
+
431
+ const guardianDecisionSignals = emitSignalCalls.filter(
432
+ (c) => c.sourceEventName === "ingress.trusted_contact.guardian_decision",
433
+ );
434
+ const deniedSignals = emitSignalCalls.filter(
435
+ (c) => c.sourceEventName === "ingress.trusted_contact.denied",
436
+ );
437
+
438
+ expect(guardianDecisionSignals.length).toBe(1);
439
+ expect(deniedSignals.length).toBe(1);
440
+
441
+ // Verify display names fall back to null/empty when contacts have no
442
+ // display name set.
443
+ const gdPayload = guardianDecisionSignals[0].contextPayload as Record<
444
+ string,
445
+ unknown
446
+ >;
447
+ expect(gdPayload.requesterDisplayName).toBeNull();
448
+ // Guardian contact exists but displayName was cleared to empty string.
449
+ // The signal enrichment resolves displayName from the contact record,
450
+ // so decidedByDisplayName will be "" (empty string) rather than null.
451
+ expect(gdPayload.decidedByDisplayName).toBe("");
452
+
453
+ const dPayload = deniedSignals[0].contextPayload as Record<string, unknown>;
454
+ expect(dPayload.requesterDisplayName).toBeNull();
455
+ expect(dPayload.decidedByDisplayName).toBe("");
456
+ });
343
457
  });
344
458
 
345
459
  // ---------------------------------------------------------------------------