@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
@@ -0,0 +1,387 @@
1
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Mocks — must come before any imports that depend on them
7
+ // ---------------------------------------------------------------------------
8
+
9
+ const testDir = process.env.VELLUM_WORKSPACE_DIR!;
10
+ const workspaceDir = testDir;
11
+ const conversationsDir = join(workspaceDir, "conversations");
12
+ mkdirSync(conversationsDir, { recursive: true });
13
+
14
+ mock.module("../util/logger.js", () => ({
15
+ getLogger: () =>
16
+ new Proxy({} as Record<string, unknown>, {
17
+ get: () => () => {},
18
+ }),
19
+ }));
20
+
21
+ mock.module("../config/loader.js", () => ({
22
+ getConfig: () => ({
23
+ ui: {},
24
+ model: "test",
25
+ provider: "test",
26
+ memory: { enabled: false },
27
+ rateLimit: { maxRequestsPerMinute: 0 },
28
+ }),
29
+ }));
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Imports — after mocks
33
+ // ---------------------------------------------------------------------------
34
+
35
+ import { getDb, initializeDb } from "../memory/db.js";
36
+ import { conversations, messages } from "../memory/schema.js";
37
+ import { recoverConversationsFromDiskViewMigration } from "../workspace/migrations/028-recover-conversations-from-disk-view.js";
38
+
39
+ initializeDb();
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Helpers
43
+ // ---------------------------------------------------------------------------
44
+
45
+ function resetTables() {
46
+ const db = getDb();
47
+ db.run("DELETE FROM messages");
48
+ db.run("DELETE FROM conversations");
49
+ }
50
+
51
+ function resetConversationsDir() {
52
+ rmSync(conversationsDir, { recursive: true, force: true });
53
+ mkdirSync(conversationsDir, { recursive: true });
54
+ }
55
+
56
+ function createDiskViewDir(
57
+ id: string,
58
+ meta: Record<string, unknown>,
59
+ messagesJsonl?: string,
60
+ ): string {
61
+ const createdAt =
62
+ typeof meta.createdAt === "string" ? meta.createdAt : new Date().toISOString();
63
+ const timestamp = createdAt.replace(/:/g, "-");
64
+ const dirName = `${timestamp}_${id}`;
65
+ const dirPath = join(conversationsDir, dirName);
66
+ mkdirSync(dirPath, { recursive: true });
67
+ writeFileSync(join(dirPath, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
68
+ if (messagesJsonl !== undefined) {
69
+ writeFileSync(join(dirPath, "messages.jsonl"), messagesJsonl);
70
+ }
71
+ return dirPath;
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Tests
76
+ // ---------------------------------------------------------------------------
77
+
78
+ describe("028-recover-conversations-from-disk-view migration", () => {
79
+ beforeEach(() => {
80
+ resetTables();
81
+ resetConversationsDir();
82
+ });
83
+
84
+ test("recovers conversation with messages", () => {
85
+ const id = "conv-028-basic";
86
+ const createdAt = "2026-03-18T14:23:00.000Z";
87
+ const updatedAt = "2026-03-18T14:25:00.000Z";
88
+
89
+ const userLine = JSON.stringify({
90
+ role: "user",
91
+ ts: "2026-03-18T14:23:30.000Z",
92
+ content: "Hello, world",
93
+ });
94
+ const assistantLine = JSON.stringify({
95
+ role: "assistant",
96
+ ts: "2026-03-18T14:24:00.000Z",
97
+ content: "Hi there!",
98
+ });
99
+
100
+ createDiskViewDir(
101
+ id,
102
+ { id, title: "Basic Recovery", type: "standard", channel: "desktop", createdAt, updatedAt },
103
+ userLine + "\n" + assistantLine + "\n",
104
+ );
105
+
106
+ recoverConversationsFromDiskViewMigration.run(workspaceDir);
107
+
108
+ const db = getDb();
109
+ const convRows = db.select().from(conversations).all();
110
+ expect(convRows).toHaveLength(1);
111
+ expect(convRows[0].id).toBe(id);
112
+ expect(convRows[0].title).toBe("Basic Recovery");
113
+ expect(convRows[0].conversationType).toBe("standard");
114
+ expect(convRows[0].createdAt).toBe(Date.parse(createdAt));
115
+ expect(convRows[0].updatedAt).toBe(Date.parse(updatedAt));
116
+
117
+ const msgRows = db.select().from(messages).all();
118
+ expect(msgRows).toHaveLength(2);
119
+
120
+ const userMsg = msgRows.find((m) => m.role === "user")!;
121
+ expect(userMsg).toBeDefined();
122
+ const userContent = JSON.parse(userMsg.content);
123
+ expect(userContent).toEqual([{ type: "text", text: "Hello, world" }]);
124
+ expect(userMsg.createdAt).toBe(Date.parse("2026-03-18T14:23:30.000Z"));
125
+
126
+ const assistantMsg = msgRows.find((m) => m.role === "assistant")!;
127
+ expect(assistantMsg).toBeDefined();
128
+ const assistantContent = JSON.parse(assistantMsg.content);
129
+ expect(assistantContent).toEqual([{ type: "text", text: "Hi there!" }]);
130
+ expect(assistantMsg.createdAt).toBe(Date.parse("2026-03-18T14:24:00.000Z"));
131
+ });
132
+
133
+ test("handles toolCalls and toolResults", () => {
134
+ const id = "conv-028-tools";
135
+ const createdAt = "2026-03-18T15:00:00.000Z";
136
+
137
+ const toolCallLine = JSON.stringify({
138
+ role: "assistant",
139
+ ts: "2026-03-18T15:00:10.000Z",
140
+ toolCalls: [{ name: "bash", input: { command: "ls" } }],
141
+ });
142
+ const toolResultLine = JSON.stringify({
143
+ role: "user",
144
+ ts: "2026-03-18T15:00:20.000Z",
145
+ toolResults: [{ content: "file.txt" }],
146
+ });
147
+
148
+ createDiskViewDir(
149
+ id,
150
+ { id, title: "Tool Test", type: "standard", createdAt, updatedAt: createdAt },
151
+ toolCallLine + "\n" + toolResultLine + "\n",
152
+ );
153
+
154
+ recoverConversationsFromDiskViewMigration.run(workspaceDir);
155
+
156
+ const db = getDb();
157
+ const msgRows = db.select().from(messages).all();
158
+ expect(msgRows).toHaveLength(2);
159
+
160
+ const assistantMsg = msgRows.find((m) => m.role === "assistant")!;
161
+ const assistantContent = JSON.parse(assistantMsg.content);
162
+ expect(assistantContent).toHaveLength(1);
163
+ expect(assistantContent[0].type).toBe("tool_use");
164
+ expect(assistantContent[0].name).toBe("bash");
165
+ expect(assistantContent[0].input).toEqual({ command: "ls" });
166
+ // tool_use blocks get a random UUID id — just check it's a string
167
+ expect(typeof assistantContent[0].id).toBe("string");
168
+
169
+ const userMsg = msgRows.find((m) => m.role === "user")!;
170
+ const userContent = JSON.parse(userMsg.content);
171
+ expect(userContent).toHaveLength(1);
172
+ expect(userContent[0].type).toBe("tool_result");
173
+ expect(userContent[0].content).toBe("file.txt");
174
+ expect(userContent[0].tool_use_id).toBe("");
175
+ });
176
+
177
+ test("handles mixed content + toolCalls on the same message", () => {
178
+ const id = "conv-028-mixed";
179
+ const createdAt = "2026-03-18T15:30:00.000Z";
180
+
181
+ const mixedLine = JSON.stringify({
182
+ role: "assistant",
183
+ ts: "2026-03-18T15:30:10.000Z",
184
+ content: "Let me check that",
185
+ toolCalls: [{ name: "bash", input: { command: "ls" } }],
186
+ });
187
+
188
+ createDiskViewDir(
189
+ id,
190
+ { id, title: "Mixed Test", type: "standard", createdAt, updatedAt: createdAt },
191
+ mixedLine + "\n",
192
+ );
193
+
194
+ recoverConversationsFromDiskViewMigration.run(workspaceDir);
195
+
196
+ const db = getDb();
197
+ const msgRows = db.select().from(messages).all();
198
+ expect(msgRows).toHaveLength(1);
199
+
200
+ const assistantMsg = msgRows[0];
201
+ expect(assistantMsg.role).toBe("assistant");
202
+
203
+ const contentBlocks = JSON.parse(assistantMsg.content);
204
+ expect(contentBlocks).toHaveLength(2);
205
+
206
+ expect(contentBlocks[0].type).toBe("text");
207
+ expect(contentBlocks[0].text).toBe("Let me check that");
208
+
209
+ expect(contentBlocks[1].type).toBe("tool_use");
210
+ expect(contentBlocks[1].name).toBe("bash");
211
+ expect(contentBlocks[1].input).toEqual({ command: "ls" });
212
+ expect(typeof contentBlocks[1].id).toBe("string");
213
+ });
214
+
215
+ test("skips existing conversations", () => {
216
+ const id = "conv-028-existing";
217
+ const createdAt = "2026-03-18T16:00:00.000Z";
218
+ const createdAtMs = Date.parse(createdAt);
219
+
220
+ // Pre-insert the conversation in the DB
221
+ const db = getDb();
222
+ db.insert(conversations)
223
+ .values({
224
+ id,
225
+ title: "Already Here",
226
+ createdAt: createdAtMs,
227
+ updatedAt: createdAtMs,
228
+ conversationType: "standard",
229
+ source: "user",
230
+ memoryScopeId: "default",
231
+ })
232
+ .run();
233
+
234
+ // Create matching disk-view dir with a message
235
+ createDiskViewDir(
236
+ id,
237
+ { id, title: "Already Here", type: "standard", createdAt, updatedAt: createdAt },
238
+ JSON.stringify({ role: "user", ts: createdAt, content: "Should not be imported" }) + "\n",
239
+ );
240
+
241
+ recoverConversationsFromDiskViewMigration.run(workspaceDir);
242
+
243
+ // Verify no duplication: still 1 conversation, 0 messages (the disk-view message was not imported)
244
+ const convRows = db.select().from(conversations).all();
245
+ expect(convRows).toHaveLength(1);
246
+ expect(convRows[0].title).toBe("Already Here");
247
+
248
+ const msgRows = db.select().from(messages).all();
249
+ expect(msgRows).toHaveLength(0);
250
+ });
251
+
252
+ test("idempotent — running twice produces same result", () => {
253
+ const id = "conv-028-idem";
254
+ const createdAt = "2026-03-18T17:00:00.000Z";
255
+
256
+ createDiskViewDir(
257
+ id,
258
+ { id, title: "Idempotency Test", type: "standard", createdAt, updatedAt: createdAt },
259
+ JSON.stringify({ role: "user", ts: createdAt, content: "First message" }) + "\n" +
260
+ JSON.stringify({ role: "assistant", ts: "2026-03-18T17:01:00.000Z", content: "Reply" }) + "\n",
261
+ );
262
+
263
+ recoverConversationsFromDiskViewMigration.run(workspaceDir);
264
+
265
+ const db = getDb();
266
+ const convCountAfterFirst = db.select().from(conversations).all().length;
267
+ const msgCountAfterFirst = db.select().from(messages).all().length;
268
+ expect(convCountAfterFirst).toBe(1);
269
+ expect(msgCountAfterFirst).toBe(2);
270
+
271
+ // Run again
272
+ recoverConversationsFromDiskViewMigration.run(workspaceDir);
273
+
274
+ const convCountAfterSecond = db.select().from(conversations).all().length;
275
+ const msgCountAfterSecond = db.select().from(messages).all().length;
276
+ expect(convCountAfterSecond).toBe(convCountAfterFirst);
277
+ expect(msgCountAfterSecond).toBe(msgCountAfterFirst);
278
+ });
279
+
280
+ test("handles missing messages.jsonl", () => {
281
+ const id = "conv-028-no-messages";
282
+ const createdAt = "2026-03-18T18:00:00.000Z";
283
+
284
+ // Create dir with only meta.json — no messages.jsonl
285
+ createDiskViewDir(
286
+ id,
287
+ { id, title: "No Messages", type: "standard", createdAt, updatedAt: createdAt },
288
+ );
289
+
290
+ recoverConversationsFromDiskViewMigration.run(workspaceDir);
291
+
292
+ const db = getDb();
293
+ const convRows = db.select().from(conversations).all();
294
+ expect(convRows).toHaveLength(1);
295
+ expect(convRows[0].id).toBe(id);
296
+ expect(convRows[0].title).toBe("No Messages");
297
+
298
+ const msgRows = db.select().from(messages).all();
299
+ expect(msgRows).toHaveLength(0);
300
+ });
301
+
302
+ test("handles malformed JSONL lines", () => {
303
+ const id = "conv-028-malformed-jsonl";
304
+ const createdAt = "2026-03-18T19:00:00.000Z";
305
+
306
+ const validLine = JSON.stringify({ role: "user", ts: createdAt, content: "Valid" });
307
+ const invalidLine = "{ this is not valid json }}}";
308
+
309
+ createDiskViewDir(
310
+ id,
311
+ { id, title: "Malformed JSONL", type: "standard", createdAt, updatedAt: createdAt },
312
+ validLine + "\n" + invalidLine + "\n",
313
+ );
314
+
315
+ recoverConversationsFromDiskViewMigration.run(workspaceDir);
316
+
317
+ const db = getDb();
318
+ const convRows = db.select().from(conversations).all();
319
+ expect(convRows).toHaveLength(1);
320
+
321
+ // Only the valid line should produce a message row
322
+ const msgRows = db.select().from(messages).all();
323
+ expect(msgRows).toHaveLength(1);
324
+ expect(msgRows[0].role).toBe("user");
325
+ const content = JSON.parse(msgRows[0].content);
326
+ expect(content).toEqual([{ type: "text", text: "Valid" }]);
327
+ });
328
+
329
+ test("handles malformed meta.json", () => {
330
+ const id = "conv-028-malformed-meta";
331
+ const createdAt = "2026-03-18T20:00:00.000Z";
332
+ const timestamp = createdAt.replace(/:/g, "-");
333
+ const dirName = `${timestamp}_${id}`;
334
+ const dirPath = join(conversationsDir, dirName);
335
+ mkdirSync(dirPath, { recursive: true });
336
+
337
+ // Write broken JSON directly
338
+ writeFileSync(join(dirPath, "meta.json"), "{ broken json");
339
+
340
+ // Migration should complete without error
341
+ recoverConversationsFromDiskViewMigration.run(workspaceDir);
342
+
343
+ const db = getDb();
344
+ const convRows = db.select().from(conversations).all();
345
+ expect(convRows).toHaveLength(0);
346
+ });
347
+
348
+ test("no-op when conversations dir missing", () => {
349
+ // Remove the conversations dir entirely
350
+ rmSync(conversationsDir, { recursive: true, force: true });
351
+ expect(existsSync(conversationsDir)).toBe(false);
352
+
353
+ // Migration should complete without error
354
+ recoverConversationsFromDiskViewMigration.run(workspaceDir);
355
+
356
+ // No conversations should exist since we can't access the DB rows through a missing dir
357
+ const db = getDb();
358
+ const convRows = db.select().from(conversations).all();
359
+ expect(convRows).toHaveLength(0);
360
+ });
361
+
362
+ test("processes multiple directories", () => {
363
+ const ids = ["conv-028-multi-a", "conv-028-multi-b", "conv-028-multi-c"];
364
+ const baseTime = Date.parse("2026-03-18T21:00:00.000Z");
365
+
366
+ for (let i = 0; i < ids.length; i++) {
367
+ const ts = new Date(baseTime + i * 60_000).toISOString();
368
+ createDiskViewDir(
369
+ ids[i],
370
+ { id: ids[i], title: `Multi ${i + 1}`, type: "standard", createdAt: ts, updatedAt: ts },
371
+ JSON.stringify({ role: "user", ts, content: `Message ${i + 1}` }) + "\n",
372
+ );
373
+ }
374
+
375
+ recoverConversationsFromDiskViewMigration.run(workspaceDir);
376
+
377
+ const db = getDb();
378
+ const convRows = db.select().from(conversations).all();
379
+ expect(convRows).toHaveLength(3);
380
+
381
+ const recoveredIds = convRows.map((c) => c.id).sort();
382
+ expect(recoveredIds).toEqual([...ids].sort());
383
+
384
+ const msgRows = db.select().from(messages).all();
385
+ expect(msgRows).toHaveLength(3);
386
+ });
387
+ });
@@ -0,0 +1,168 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
11
+
12
+ import { seedPkbAutoinjectMigration } from "../workspace/migrations/030-seed-pkb-autoinject.js";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Helpers
16
+ // ---------------------------------------------------------------------------
17
+
18
+ let workspaceDir: string;
19
+ let pkbDir: string;
20
+
21
+ function freshWorkspace(): void {
22
+ workspaceDir = join(
23
+ tmpdir(),
24
+ `vellum-migration-030-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
25
+ );
26
+ pkbDir = join(workspaceDir, "pkb");
27
+ mkdirSync(pkbDir, { recursive: true });
28
+ }
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Setup / Teardown
32
+ // ---------------------------------------------------------------------------
33
+
34
+ const dirs: string[] = [];
35
+
36
+ beforeEach(() => {
37
+ freshWorkspace();
38
+ dirs.push(workspaceDir);
39
+ });
40
+
41
+ afterEach(() => {
42
+ for (const dir of dirs.splice(0)) {
43
+ rmSync(dir, { recursive: true, force: true });
44
+ }
45
+ });
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Tests
49
+ // ---------------------------------------------------------------------------
50
+
51
+ describe("030-seed-pkb-autoinject migration", () => {
52
+ test("has correct migration id", () => {
53
+ expect(seedPkbAutoinjectMigration.id).toBe("030-seed-pkb-autoinject");
54
+ });
55
+
56
+ // ─── run() ──────────────────────────────────────────────────────────────
57
+
58
+ test("creates _autoinject.md with default content", () => {
59
+ seedPkbAutoinjectMigration.run(workspaceDir);
60
+
61
+ const filePath = join(pkbDir, "_autoinject.md");
62
+ expect(existsSync(filePath)).toBe(true);
63
+
64
+ const content = readFileSync(filePath, "utf-8");
65
+ expect(content).toContain("INDEX.md");
66
+ expect(content).toContain("essentials.md");
67
+ expect(content).toContain("threads.md");
68
+ expect(content).toContain("buffer.md");
69
+ });
70
+
71
+ test("no-op when pkb/ does not exist", () => {
72
+ rmSync(pkbDir, { recursive: true, force: true });
73
+ seedPkbAutoinjectMigration.run(workspaceDir);
74
+ expect(existsSync(join(pkbDir, "_autoinject.md"))).toBe(false);
75
+ });
76
+
77
+ test("idempotent — does not overwrite existing _autoinject.md", () => {
78
+ const customContent = "INDEX.md\ncustom-topic.md\n";
79
+ writeFileSync(join(pkbDir, "_autoinject.md"), customContent, "utf-8");
80
+
81
+ seedPkbAutoinjectMigration.run(workspaceDir);
82
+
83
+ const content = readFileSync(join(pkbDir, "_autoinject.md"), "utf-8");
84
+ expect(content).toBe(customContent);
85
+ });
86
+
87
+ test("appends _autoinject.md entry to INDEX.md", () => {
88
+ const indexContent =
89
+ "# Knowledge Base\n\n## Always Loaded\n" +
90
+ "- essentials.md — Core facts\n" +
91
+ "- threads.md — Active threads\n" +
92
+ "- buffer.md — Inbox\n\n" +
93
+ "## Topics\n";
94
+ writeFileSync(join(pkbDir, "INDEX.md"), indexContent, "utf-8");
95
+
96
+ seedPkbAutoinjectMigration.run(workspaceDir);
97
+
98
+ const updated = readFileSync(join(pkbDir, "INDEX.md"), "utf-8");
99
+ expect(updated).toContain("_autoinject.md");
100
+ // Should appear after buffer.md
101
+ const bufferIdx = updated.indexOf("buffer.md");
102
+ const autoinjectIdx = updated.indexOf("_autoinject.md");
103
+ expect(autoinjectIdx).toBeGreaterThan(bufferIdx);
104
+ });
105
+
106
+ test("does not duplicate _autoinject.md entry in INDEX.md", () => {
107
+ const indexContent =
108
+ "# Knowledge Base\n\n## Always Loaded\n" +
109
+ "- buffer.md — Inbox\n" +
110
+ "- _autoinject.md — Controls autoinjection\n\n" +
111
+ "## Topics\n";
112
+ writeFileSync(join(pkbDir, "INDEX.md"), indexContent, "utf-8");
113
+
114
+ seedPkbAutoinjectMigration.run(workspaceDir);
115
+
116
+ const updated = readFileSync(join(pkbDir, "INDEX.md"), "utf-8");
117
+ const matches = updated.match(/_autoinject\.md/g);
118
+ expect(matches?.length).toBe(1);
119
+ });
120
+
121
+ test("handles missing INDEX.md gracefully", () => {
122
+ // No INDEX.md — should still create _autoinject.md without error
123
+ seedPkbAutoinjectMigration.run(workspaceDir);
124
+ expect(existsSync(join(pkbDir, "_autoinject.md"))).toBe(true);
125
+ });
126
+
127
+ // ─── down() ─────────────────────────────────────────────────────────────
128
+
129
+ describe("down()", () => {
130
+ test("removes _autoinject.md when content matches template", () => {
131
+ seedPkbAutoinjectMigration.run(workspaceDir);
132
+ expect(existsSync(join(pkbDir, "_autoinject.md"))).toBe(true);
133
+
134
+ seedPkbAutoinjectMigration.down(workspaceDir);
135
+ expect(existsSync(join(pkbDir, "_autoinject.md"))).toBe(false);
136
+ });
137
+
138
+ test("preserves _autoinject.md when user has customized it", () => {
139
+ const customContent = "INDEX.md\nmy-custom-file.md\n";
140
+ writeFileSync(join(pkbDir, "_autoinject.md"), customContent, "utf-8");
141
+
142
+ seedPkbAutoinjectMigration.down(workspaceDir);
143
+
144
+ expect(existsSync(join(pkbDir, "_autoinject.md"))).toBe(true);
145
+ expect(readFileSync(join(pkbDir, "_autoinject.md"), "utf-8")).toBe(
146
+ customContent,
147
+ );
148
+ });
149
+
150
+ test("no-op when _autoinject.md does not exist", () => {
151
+ seedPkbAutoinjectMigration.down(workspaceDir);
152
+ // Should not throw
153
+ });
154
+
155
+ test("no-op when pkb/ does not exist", () => {
156
+ rmSync(pkbDir, { recursive: true, force: true });
157
+ seedPkbAutoinjectMigration.down(workspaceDir);
158
+ // Should not throw
159
+ });
160
+
161
+ test("idempotent — calling down() twice is safe", () => {
162
+ seedPkbAutoinjectMigration.run(workspaceDir);
163
+ seedPkbAutoinjectMigration.down(workspaceDir);
164
+ seedPkbAutoinjectMigration.down(workspaceDir);
165
+ // Should not throw
166
+ });
167
+ });
168
+ });
@@ -198,7 +198,7 @@ describe("isWorkspaceScopedInvocation", () => {
198
198
  // ── Bash ───────────────────────────────────────────────────────────
199
199
 
200
200
  describe("bash", () => {
201
- test("returns true (sandbox handles isolation)", () => {
201
+ test("returns true (container handles isolation)", () => {
202
202
  expect(
203
203
  isWorkspaceScopedInvocation(
204
204
  "bash",
@@ -255,12 +255,7 @@ describe("isWorkspaceScopedInvocation", () => {
255
255
  // ── Always-scoped safe tools ───────────────────────────────────────
256
256
 
257
257
  describe("always-scoped tools", () => {
258
- const safeTools = [
259
- "skill_load",
260
- "recall",
261
- "ui_update",
262
- "ui_dismiss",
263
- ];
258
+ const safeTools = ["skill_load", "recall", "ui_update", "ui_dismiss"];
264
259
 
265
260
  for (const tool of safeTools) {
266
261
  test(`${tool} is workspace-scoped`, () => {
package/src/agent/loop.ts CHANGED
@@ -31,6 +31,8 @@ export interface AgentLoopConfig {
31
31
  | { type: "tool"; name: string };
32
32
  /** Minimum interval (ms) between consecutive LLM calls to prevent spin when tools return instantly */
33
33
  minTurnIntervalMs?: number;
34
+ /** Override the default prompt cache TTL sent to the provider (e.g. "5m" for short-lived subagents). */
35
+ cacheTtl?: "5m" | "1h";
34
36
  }
35
37
 
36
38
  export interface CheckpointInfo {
@@ -199,7 +201,6 @@ export class AgentLoop {
199
201
  ): Promise<Message[]> {
200
202
  const history = [...messages];
201
203
  let toolUseTurns = 0;
202
- let nudgedForEmptyResponse = false;
203
204
  let consecutiveErrorTurns = 0;
204
205
  let lastLlmCallTime = 0;
205
206
  const rlog = requestId ? log.child({ requestId }) : log;
@@ -252,6 +253,10 @@ export class AgentLoop {
252
253
  providerConfig.tool_choice = this.config.toolChoice;
253
254
  }
254
255
 
256
+ if (this.config.cacheTtl) {
257
+ providerConfig.cacheTtl = this.config.cacheTtl;
258
+ }
259
+
255
260
  const preLlmResult = await getHookManager().trigger("pre-llm-call", {
256
261
  systemPrompt: turnSystemPrompt,
257
262
  messages: history,
@@ -397,35 +402,7 @@ export class AgentLoop {
397
402
  block.type === "tool_use",
398
403
  );
399
404
 
400
- // Check if the assistant turn contained any visible text (used for
401
- // the empty-response nudge).
402
- const hasTextBlock = response.content.some(
403
- (block) => block.type === "text" && block.text.trim().length > 0,
404
- );
405
-
406
405
  if (toolUseBlocks.length === 0 || !this.toolExecutor) {
407
- // Check if the LLM returned no text after tool results — nudge it to respond
408
- const lastUserMsg =
409
- history.length >= 2 ? history[history.length - 2] : undefined;
410
- const lastWasToolResult =
411
- lastUserMsg?.role === "user" &&
412
- lastUserMsg.content.some((block) => block.type === "tool_result");
413
-
414
- if (!hasTextBlock && lastWasToolResult && !nudgedForEmptyResponse) {
415
- nudgedForEmptyResponse = true;
416
- history.push({
417
- role: "user",
418
- content: [
419
- {
420
- type: "text",
421
- text: "<system_notice>You executed tools but didn't tell the user what happened. Provide a brief, conversational summary of the results.</system_notice>",
422
- },
423
- ],
424
- });
425
- continue;
426
- }
427
-
428
- // No tool calls or no executor — done
429
406
  break;
430
407
  }
431
408
 
@@ -13,6 +13,7 @@
13
13
 
14
14
  import { answerCall } from "../calls/call-domain.js";
15
15
  import { getGatewayInternalBaseUrl } from "../config/env.js";
16
+ import { findContactChannel } from "../contacts/contact-store.js";
16
17
  import { upsertContactChannel } from "../contacts/contacts-write.js";
17
18
  import {
18
19
  type CanonicalGuardianRequest,
@@ -396,6 +397,25 @@ const accessRequestResolver: GuardianRequestResolver = {
396
397
  const desktopDeliverUrl = resolveDeliverCallbackUrlForChannel(channel);
397
398
  const desktopBearerToken = mintDaemonDeliveryToken();
398
399
 
400
+ // Resolve display names from the contacts database for enriched payloads
401
+ const requesterContactResult = requesterExternalUserId
402
+ ? findContactChannel({
403
+ channelType: channel,
404
+ externalUserId: requesterExternalUserId,
405
+ })
406
+ : null;
407
+ const requesterDisplayName =
408
+ requesterContactResult?.contact.displayName ?? null;
409
+
410
+ const decidedByContactResult = decidedByExternalUserId
411
+ ? findContactChannel({
412
+ channelType: channel,
413
+ externalUserId: decidedByExternalUserId,
414
+ })
415
+ : null;
416
+ const decidedByDisplayName =
417
+ decidedByContactResult?.contact.displayName ?? null;
418
+
399
419
  if (decision.action === "reject") {
400
420
  log.info(
401
421
  { event: "resolver_access_request_denied", requestId: request.id },
@@ -435,6 +455,8 @@ const accessRequestResolver: GuardianRequestResolver = {
435
455
  requesterExternalUserId,
436
456
  requesterChatId,
437
457
  decidedByExternalUserId,
458
+ requesterDisplayName,
459
+ decidedByDisplayName,
438
460
  decision: "denied" as const,
439
461
  };
440
462
 
@@ -726,6 +748,8 @@ const accessRequestResolver: GuardianRequestResolver = {
726
748
  sourceChannel: channel,
727
749
  requesterExternalUserId,
728
750
  requesterChatId,
751
+ requesterDisplayName,
752
+ decidedByDisplayName,
729
753
  verificationSessionId: session.sessionId,
730
754
  },
731
755
  dedupeKey: `trusted-contact:verification-sent:${session.sessionId}`,