@vellumai/assistant 0.8.3 → 0.8.4

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 (342) hide show
  1. package/docker-entrypoint.sh +0 -1
  2. package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
  3. package/openapi.yaml +610 -16
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-loop-exit-reason.test.ts +4 -5
  6. package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
  7. package/src/__tests__/agent-loop.test.ts +88 -3
  8. package/src/__tests__/anthropic-provider.test.ts +272 -0
  9. package/src/__tests__/approval-cascade.test.ts +1 -1
  10. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  11. package/src/__tests__/channel-delivery-store.test.ts +193 -0
  12. package/src/__tests__/channel-reply-delivery.test.ts +284 -5
  13. package/src/__tests__/channel-retry-sweep.test.ts +274 -1
  14. package/src/__tests__/compaction-events.test.ts +1 -1
  15. package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
  16. package/src/__tests__/config-watcher.test.ts +1 -1
  17. package/src/__tests__/context-token-estimator.test.ts +91 -1
  18. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  19. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +25 -7
  22. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
  23. package/src/__tests__/conversation-clean-command.test.ts +137 -0
  24. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  25. package/src/__tests__/conversation-fork-crud.test.ts +161 -0
  26. package/src/__tests__/conversation-lifecycle.test.ts +1 -1
  27. package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
  28. package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
  29. package/src/__tests__/conversation-pairing.test.ts +2 -2
  30. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  31. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  32. package/src/__tests__/conversation-queue.test.ts +1 -1
  33. package/src/__tests__/conversation-runtime-assembly.test.ts +264 -81
  34. package/src/__tests__/conversation-seed-composer.test.ts +66 -4
  35. package/src/__tests__/conversation-slash-commands.test.ts +36 -8
  36. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  37. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  38. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  39. package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
  40. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  41. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  42. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  43. package/src/__tests__/credential-security-invariants.test.ts +6 -0
  44. package/src/__tests__/cu-unified-flow.test.ts +10 -1
  45. package/src/__tests__/dm-backfill.test.ts +64 -0
  46. package/src/__tests__/dm-persistence.test.ts +33 -0
  47. package/src/__tests__/document-find-replace.test.ts +501 -0
  48. package/src/__tests__/first-greeting.test.ts +23 -2
  49. package/src/__tests__/headless-browser-navigate.test.ts +172 -0
  50. package/src/__tests__/host-bash-proxy.test.ts +6 -0
  51. package/src/__tests__/host-browser-proxy.test.ts +10 -0
  52. package/src/__tests__/host-cu-proxy.test.ts +8 -1
  53. package/src/__tests__/host-file-proxy.test.ts +8 -1
  54. package/src/__tests__/host-transfer-proxy.test.ts +8 -1
  55. package/src/__tests__/identity-routes.test.ts +57 -0
  56. package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
  57. package/src/__tests__/injector-chain.test.ts +2 -0
  58. package/src/__tests__/injector-document-comments.test.ts +378 -0
  59. package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
  60. package/src/__tests__/list-messages-attachments.test.ts +21 -17
  61. package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
  62. package/src/__tests__/list-messages-page-latest.test.ts +130 -14
  63. package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
  64. package/src/__tests__/llm-context-normalization.test.ts +0 -2
  65. package/src/__tests__/llm-resolver.test.ts +85 -1
  66. package/src/__tests__/log-export-routes.test.ts +99 -2
  67. package/src/__tests__/message-queue-steer.test.ts +114 -0
  68. package/src/__tests__/openai-provider.test.ts +105 -0
  69. package/src/__tests__/openai-responses-provider.test.ts +4 -4
  70. package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
  71. package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
  72. package/src/__tests__/platform.test.ts +0 -3
  73. package/src/__tests__/plugin-source-watcher.test.ts +302 -0
  74. package/src/__tests__/process-message-background-slack.test.ts +1 -51
  75. package/src/__tests__/process-message-display-content.test.ts +21 -16
  76. package/src/__tests__/server-history-render.test.ts +83 -4
  77. package/src/__tests__/steer-tool-repair.test.ts +249 -0
  78. package/src/__tests__/system-prompt.test.ts +51 -28
  79. package/src/__tests__/terminal-tools.test.ts +11 -1
  80. package/src/__tests__/thinking-block-replay.test.ts +113 -0
  81. package/src/__tests__/thread-backfill.test.ts +370 -22
  82. package/src/__tests__/tool-executor.test.ts +90 -1
  83. package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
  84. package/src/__tests__/twilio-routes.test.ts +1 -1
  85. package/src/__tests__/web-fetch.test.ts +2 -2
  86. package/src/__tests__/workspace-git-service.test.ts +88 -5
  87. package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
  88. package/src/agent/attachments.ts +1 -0
  89. package/src/agent/loop.ts +57 -20
  90. package/src/background-wake/next-wake.test.ts +289 -0
  91. package/src/background-wake/next-wake.ts +172 -0
  92. package/src/browser/operations.ts +15 -0
  93. package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
  94. package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
  95. package/src/cli/commands/conversations.ts +128 -1
  96. package/src/cli/commands/inference-providers.ts +147 -1
  97. package/src/cli/commands/memory-v2.ts +308 -0
  98. package/src/cli/commands/notifications.ts +24 -2
  99. package/src/cli/utils/conversation-id.ts +17 -5
  100. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  101. package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
  102. package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
  103. package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
  104. package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
  105. package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
  106. package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
  107. package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
  108. package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
  109. package/src/config/bundled-skills/schedule/SKILL.md +8 -0
  110. package/src/config/bundled-tool-registry.ts +22 -12
  111. package/src/config/call-site-defaults.ts +19 -0
  112. package/src/config/feature-flag-registry.json +99 -3
  113. package/src/config/llm-resolver.ts +16 -2
  114. package/src/config/schemas/__tests__/memory-v2.test.ts +4 -0
  115. package/src/config/schemas/call-site-catalog.ts +21 -0
  116. package/src/config/schemas/llm.ts +3 -0
  117. package/src/config/schemas/memory-v2.ts +48 -1
  118. package/src/context/compactor.ts +8 -1
  119. package/src/context/token-estimator.ts +47 -4
  120. package/src/context/window-manager.ts +25 -0
  121. package/src/credential-health/credential-health-service.ts +34 -19
  122. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
  123. package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
  124. package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
  125. package/src/daemon/conversation-agent-loop-handlers.ts +153 -23
  126. package/src/daemon/conversation-agent-loop.ts +223 -54
  127. package/src/daemon/conversation-lifecycle.ts +142 -116
  128. package/src/daemon/conversation-messaging.ts +3 -0
  129. package/src/daemon/conversation-process.ts +273 -0
  130. package/src/daemon/conversation-queue-manager.ts +14 -0
  131. package/src/daemon/conversation-runtime-assembly.ts +135 -75
  132. package/src/daemon/conversation-slash.ts +37 -5
  133. package/src/daemon/conversation-surfaces.ts +45 -2
  134. package/src/daemon/conversation-tool-setup.ts +7 -0
  135. package/src/daemon/conversation.ts +42 -5
  136. package/src/daemon/first-greeting.ts +10 -0
  137. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
  138. package/src/daemon/handlers/config-a2a.ts +160 -0
  139. package/src/daemon/handlers/config-model.test.ts +1 -0
  140. package/src/daemon/handlers/conversations.ts +79 -0
  141. package/src/daemon/handlers/shared.ts +92 -29
  142. package/src/daemon/host-bash-proxy.ts +1 -1
  143. package/src/daemon/host-cu-proxy.ts +1 -1
  144. package/src/daemon/host-file-proxy.ts +1 -1
  145. package/src/daemon/host-transfer-proxy.ts +1 -1
  146. package/src/daemon/lifecycle.ts +18 -4
  147. package/src/daemon/message-protocol.ts +4 -0
  148. package/src/daemon/message-types/conversations.ts +8 -0
  149. package/src/daemon/message-types/document-comments.ts +50 -0
  150. package/src/daemon/message-types/messages.ts +68 -1
  151. package/src/daemon/message-types/surfaces.ts +3 -1
  152. package/src/daemon/message-types/web-activity.ts +57 -0
  153. package/src/daemon/plugin-source-watcher.ts +135 -3
  154. package/src/daemon/process-message.ts +69 -12
  155. package/src/daemon/query-complexity-router.ts +75 -0
  156. package/src/daemon/trust-context.ts +6 -0
  157. package/src/documents/document-comments-store.test.ts +338 -0
  158. package/src/documents/document-comments-store.ts +237 -0
  159. package/src/documents/document-store.ts +202 -0
  160. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  161. package/src/heartbeat/heartbeat-service.ts +1 -0
  162. package/src/home/__tests__/suggested-prompts.test.ts +33 -2
  163. package/src/home/feed-types.ts +6 -1
  164. package/src/home/home-content-refresh.ts +52 -0
  165. package/src/home/home-greeting-cache.ts +69 -0
  166. package/src/home/home-greeting.ts +94 -0
  167. package/src/home/suggested-prompts.ts +177 -9
  168. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
  169. package/src/memory/__tests__/memory-retrospective-job.test.ts +320 -6
  170. package/src/memory/conversation-crud.ts +133 -43
  171. package/src/memory/db-init.ts +16 -0
  172. package/src/memory/delivery-crud.ts +41 -0
  173. package/src/memory/delivery-status.ts +141 -15
  174. package/src/memory/external-conversation-store.ts +32 -1
  175. package/src/memory/jobs-worker.ts +21 -1
  176. package/src/memory/memory-retrospective-constants.ts +28 -0
  177. package/src/memory/memory-retrospective-enqueue.ts +3 -2
  178. package/src/memory/memory-retrospective-job.ts +408 -18
  179. package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
  180. package/src/memory/memory-v2-activation-log-store.ts +26 -8
  181. package/src/memory/migrations/100-core-tables.ts +1 -0
  182. package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
  183. package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
  184. package/src/memory/migrations/253-document-comments.ts +47 -0
  185. package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
  186. package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
  187. package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
  188. package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
  189. package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
  190. package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
  191. package/src/memory/migrations/index.ts +17 -0
  192. package/src/memory/migrations/registry.ts +25 -0
  193. package/src/memory/onboarding-events-store.ts +7 -0
  194. package/src/memory/schema/calls.ts +1 -0
  195. package/src/memory/schema/conversations.ts +3 -0
  196. package/src/memory/schema/infrastructure.ts +1 -0
  197. package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
  198. package/src/memory/v2/__tests__/injection.test.ts +31 -14
  199. package/src/memory/v2/__tests__/page-index.test.ts +365 -1
  200. package/src/memory/v2/__tests__/router.test.ts +489 -1
  201. package/src/memory/v2/consolidation-job.ts +14 -0
  202. package/src/memory/v2/injection-events.ts +101 -0
  203. package/src/memory/v2/injection.ts +21 -10
  204. package/src/memory/v2/page-index.ts +209 -7
  205. package/src/memory/v2/page-store.ts +18 -0
  206. package/src/memory/v2/router.ts +209 -55
  207. package/src/messaging/providers/index.ts +7 -1
  208. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
  209. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
  210. package/src/messaging/providers/slack/adapter.ts +178 -25
  211. package/src/messaging/providers/slack/api.test.ts +54 -0
  212. package/src/messaging/providers/slack/api.ts +119 -3
  213. package/src/messaging/providers/slack/client.ts +12 -0
  214. package/src/messaging/providers/slack/deep-link.ts +20 -1
  215. package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
  216. package/src/messaging/providers/slack/message-metadata.ts +156 -0
  217. package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
  218. package/src/messaging/providers/slack/render-transcript.ts +176 -49
  219. package/src/messaging/providers/slack/send.test.ts +77 -0
  220. package/src/messaging/providers/slack/send.ts +8 -2
  221. package/src/messaging/providers/slack/types.ts +14 -0
  222. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +4 -1
  223. package/src/notifications/__tests__/home-feed-side-effect.test.ts +116 -54
  224. package/src/notifications/conversation-seed-composer.ts +14 -2
  225. package/src/notifications/deferred-emit.ts +135 -0
  226. package/src/notifications/emit-signal.ts +9 -1
  227. package/src/notifications/home-feed-side-effect.ts +60 -30
  228. package/src/oauth/connect-orchestrator.ts +3 -0
  229. package/src/oauth/credential-token-resolver.ts +2 -0
  230. package/src/oauth/manual-token-connection.ts +19 -0
  231. package/src/oauth/oauth-store.ts +12 -0
  232. package/src/oauth/seed-providers.ts +22 -0
  233. package/src/permissions/prompter.ts +5 -2
  234. package/src/permissions/secret-prompter.ts +4 -1
  235. package/src/plugins/defaults/injectors.ts +82 -9
  236. package/src/prompts/__tests__/system-prompt.test.ts +46 -2
  237. package/src/prompts/normalize-onboarding.ts +40 -0
  238. package/src/prompts/sections.ts +32 -14
  239. package/src/prompts/system-prompt.ts +105 -68
  240. package/src/prompts/template-detection.ts +37 -0
  241. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
  242. package/src/prompts/templates/BOOTSTRAP.md +8 -0
  243. package/src/prompts/templates/VOICE.md +3 -0
  244. package/src/prompts/templates/system-sections.ts +53 -3
  245. package/src/providers/anthropic/client.ts +132 -5
  246. package/src/providers/fireworks/client.ts +20 -2
  247. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
  248. package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
  249. package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
  250. package/src/providers/inference/adapter-factory.ts +15 -1
  251. package/src/providers/inference/auth.ts +3 -3
  252. package/src/providers/inference/codex-token-refresh.ts +128 -0
  253. package/src/providers/inference/resolve-auth.ts +49 -6
  254. package/src/providers/model-catalog.ts +48 -1
  255. package/src/providers/openai/chat-completions-provider.ts +57 -20
  256. package/src/providers/openai/responses-provider.ts +9 -3
  257. package/src/providers/openrouter/client.ts +5 -1
  258. package/src/providers/types.ts +25 -0
  259. package/src/runtime/__tests__/agent-wake.test.ts +214 -0
  260. package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
  261. package/src/runtime/agent-wake.ts +151 -56
  262. package/src/runtime/auth/route-policy.ts +7 -3
  263. package/src/runtime/background-job-runner.ts +26 -0
  264. package/src/runtime/channel-reply-delivery.ts +182 -47
  265. package/src/runtime/channel-retry-sweep.ts +141 -16
  266. package/src/runtime/http-types.ts +7 -4
  267. package/src/runtime/pending-interactions.ts +51 -8
  268. package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
  269. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +55 -1
  270. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
  271. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
  272. package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
  273. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
  274. package/src/runtime/routes/approval-routes.ts +4 -1
  275. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
  276. package/src/runtime/routes/content-source-routes.ts +78 -0
  277. package/src/runtime/routes/conversation-cli-routes.ts +146 -1
  278. package/src/runtime/routes/conversation-query-routes.ts +60 -1
  279. package/src/runtime/routes/conversation-routes.ts +281 -76
  280. package/src/runtime/routes/document-comments-routes.ts +287 -0
  281. package/src/runtime/routes/documents-routes.ts +33 -0
  282. package/src/runtime/routes/home-feed-routes.ts +6 -3
  283. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  284. package/src/runtime/routes/host-browser-routes.ts +8 -1
  285. package/src/runtime/routes/identity-routes.ts +21 -0
  286. package/src/runtime/routes/inbound-message-handler.ts +288 -58
  287. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
  288. package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
  289. package/src/runtime/routes/index.ts +12 -4
  290. package/src/runtime/routes/inference-provider-connection-routes.ts +63 -7
  291. package/src/runtime/routes/integrations/a2a.ts +60 -1
  292. package/src/runtime/routes/log-export-routes.ts +39 -0
  293. package/src/runtime/routes/memory-v2-routes.ts +217 -0
  294. package/src/runtime/routes/notification-routes.ts +19 -2
  295. package/src/runtime/routes/question-routes.ts +4 -1
  296. package/src/runtime/routes/sanity-routes.ts +159 -0
  297. package/src/runtime/routes/slack-channel-routes.ts +187 -0
  298. package/src/runtime/services/conversation-serializer.ts +30 -4
  299. package/src/schedule/integration-status.ts +3 -1
  300. package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
  301. package/src/security/oauth2-device-code.ts +307 -0
  302. package/src/security/oauth2.ts +26 -9
  303. package/src/security/secure-keys.ts +5 -0
  304. package/src/skills/catalog-install.ts +6 -2
  305. package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
  306. package/src/tools/browser/browser-execution.ts +93 -0
  307. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
  308. package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
  309. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
  310. package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
  311. package/src/tools/browser/cdp-client/factory.ts +87 -3
  312. package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
  313. package/src/tools/browser/cdp-client/types.ts +36 -0
  314. package/src/tools/browser/pinned-tabs.ts +90 -0
  315. package/src/tools/document/document-comment-tool.test.ts +379 -0
  316. package/src/tools/document/document-comment-tool.ts +156 -0
  317. package/src/tools/document/document-tool.ts +128 -2
  318. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
  319. package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
  320. package/src/tools/network/domain-normalize.ts +17 -0
  321. package/src/tools/network/web-fetch.ts +213 -64
  322. package/src/tools/network/web-search.ts +191 -66
  323. package/src/tools/terminal/safe-env.ts +3 -2
  324. package/src/tools/tool-approval-handler.ts +19 -12
  325. package/src/tools/types.ts +4 -0
  326. package/src/tools/ui-surface/definitions.ts +3 -1
  327. package/src/types/onboarding-context.ts +4 -0
  328. package/src/util/__tests__/favicon.test.ts +84 -0
  329. package/src/util/favicon.ts +40 -0
  330. package/src/util/platform.ts +0 -5
  331. package/src/workspace/git-service.ts +75 -4
  332. package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
  333. package/src/workspace/migrations/registry.ts +2 -0
  334. package/src/config/bundled-skills/document/SKILL.md +0 -54
  335. package/src/config/bundled-skills/document/TOOLS.json +0 -106
  336. package/src/daemon/seed-files.ts +0 -18
  337. package/src/runtime/routes/interface-routes.ts +0 -43
  338. /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
  339. /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
  340. /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
  341. /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
  342. /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Tests for handleListMessages metadata.hidden filtering.
3
+ *
4
+ * Messages persisted with `metadata: { hidden: true }` (e.g. internal
5
+ * scaffolding like retrospective instructions) must be omitted from the
6
+ * UI history list while remaining visible to the LLM-side history loader.
7
+ */
8
+
9
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
10
+
11
+ mock.module("../util/logger.js", () => ({
12
+ getLogger: () =>
13
+ new Proxy({} as Record<string, unknown>, {
14
+ get: () => () => {},
15
+ }),
16
+ }));
17
+
18
+ mock.module("../config/loader.js", () => ({
19
+ getConfig: () => ({
20
+ ui: {},
21
+ model: "test",
22
+ provider: "test",
23
+ memory: { enabled: false },
24
+ rateLimit: { maxRequestsPerMinute: 0 },
25
+ }),
26
+ }));
27
+
28
+ import {
29
+ addMessage,
30
+ createConversation,
31
+ getMessages,
32
+ } from "../memory/conversation-crud.js";
33
+ import { getDb } from "../memory/db-connection.js";
34
+ import { initializeDb } from "../memory/db-init.js";
35
+ import { handleListMessages } from "../runtime/routes/conversation-routes.js";
36
+
37
+ initializeDb();
38
+
39
+ function resetTables() {
40
+ const db = getDb();
41
+ db.run("DELETE FROM message_attachments");
42
+ db.run("DELETE FROM attachments");
43
+ db.run("DELETE FROM messages");
44
+ db.run("DELETE FROM conversations");
45
+ }
46
+
47
+ interface MessagePayload {
48
+ role: string;
49
+ content: string;
50
+ }
51
+
52
+ describe("handleListMessages metadata.hidden filtering", () => {
53
+ beforeEach(resetTables);
54
+
55
+ test("UI serializer omits hidden messages but LLM-side getMessages includes them", async () => {
56
+ const conv = createConversation();
57
+ await addMessage(
58
+ conv.id,
59
+ "user",
60
+ JSON.stringify([{ type: "text", text: "first visible" }]),
61
+ );
62
+ await addMessage(
63
+ conv.id,
64
+ "assistant",
65
+ JSON.stringify([{ type: "text", text: "internal scaffolding" }]),
66
+ { hidden: true },
67
+ );
68
+ await addMessage(
69
+ conv.id,
70
+ "user",
71
+ JSON.stringify([{ type: "text", text: "second visible" }]),
72
+ );
73
+
74
+ const response = handleListMessages({
75
+ queryParams: { conversationId: conv.id },
76
+ });
77
+ const body = response as { messages: MessagePayload[] };
78
+
79
+ expect(body.messages).toHaveLength(2);
80
+ expect(body.messages[0].content).toBe("first visible");
81
+ expect(body.messages[1].content).toBe("second visible");
82
+ expect(
83
+ body.messages.some((m) => m.content.includes("internal scaffolding")),
84
+ ).toBe(false);
85
+
86
+ // LLM-side loader must include the hidden row so agent context is intact.
87
+ const llmRows = getMessages(conv.id);
88
+ expect(llmRows).toHaveLength(3);
89
+ expect(llmRows[1].metadata).toContain('"hidden":true');
90
+ });
91
+
92
+ test("messages without metadata or with hidden=false are returned", async () => {
93
+ const conv = createConversation();
94
+ await addMessage(
95
+ conv.id,
96
+ "user",
97
+ JSON.stringify([{ type: "text", text: "no metadata" }]),
98
+ );
99
+ await addMessage(
100
+ conv.id,
101
+ "assistant",
102
+ JSON.stringify([{ type: "text", text: "hidden false" }]),
103
+ { hidden: false },
104
+ );
105
+
106
+ const response = handleListMessages({
107
+ queryParams: { conversationId: conv.id },
108
+ });
109
+ const body = response as { messages: MessagePayload[] };
110
+
111
+ expect(body.messages).toHaveLength(2);
112
+ });
113
+
114
+ test("pagination skips hidden rows so hasMore and oldest cursor reflect visible rows", async () => {
115
+ const conv = createConversation();
116
+ // 4 visible older rows, then a block of 3 hidden rows, then 2 visible newer.
117
+ // With limit=2 and page=latest we should get the 2 newest visible rows,
118
+ // hasMore=true (older visible rows exist), and a cursor pointing at the
119
+ // oldest visible row in the page rather than null.
120
+ for (let i = 0; i < 4; i++) {
121
+ await addMessage(
122
+ conv.id,
123
+ "user",
124
+ JSON.stringify([{ type: "text", text: `old visible ${i}` }]),
125
+ );
126
+ }
127
+ for (let i = 0; i < 3; i++) {
128
+ await addMessage(
129
+ conv.id,
130
+ "assistant",
131
+ JSON.stringify([{ type: "text", text: `hidden ${i}` }]),
132
+ { hidden: true },
133
+ );
134
+ }
135
+ for (let i = 0; i < 2; i++) {
136
+ await addMessage(
137
+ conv.id,
138
+ "user",
139
+ JSON.stringify([{ type: "text", text: `new visible ${i}` }]),
140
+ );
141
+ }
142
+
143
+ const latest = handleListMessages({
144
+ queryParams: { conversationId: conv.id, page: "latest", limit: "2" },
145
+ }) as {
146
+ messages: MessagePayload[];
147
+ hasMore: boolean;
148
+ oldestTimestamp: number | null;
149
+ oldestMessageId: string | null;
150
+ };
151
+
152
+ expect(latest.messages.map((m) => m.content)).toEqual([
153
+ "new visible 0",
154
+ "new visible 1",
155
+ ]);
156
+ expect(latest.hasMore).toBe(true);
157
+ expect(latest.oldestTimestamp).not.toBeNull();
158
+ expect(latest.oldestMessageId).not.toBeNull();
159
+
160
+ // Older page request — anchored before the latest page's oldest row —
161
+ // should skip the hidden block entirely and return the next 2 visible rows.
162
+ const older = handleListMessages({
163
+ queryParams: {
164
+ conversationId: conv.id,
165
+ beforeTimestamp: String(latest.oldestTimestamp),
166
+ limit: "2",
167
+ },
168
+ }) as {
169
+ messages: MessagePayload[];
170
+ hasMore: boolean;
171
+ };
172
+
173
+ expect(older.messages.map((m) => m.content)).toEqual([
174
+ "old visible 2",
175
+ "old visible 3",
176
+ ]);
177
+ expect(older.hasMore).toBe(true);
178
+ });
179
+
180
+ test("pagination drains DB when every row in a page is hidden", async () => {
181
+ const conv = createConversation();
182
+ // 5 hidden rows then 2 visible older rows. With limit=2, the naive
183
+ // implementation fetches 3 newest (all hidden), filters to 0 visible, and
184
+ // returns hasMore=true with no cursor. We expect the loop to keep going
185
+ // and surface the visible rows instead.
186
+ for (let i = 0; i < 2; i++) {
187
+ await addMessage(
188
+ conv.id,
189
+ "user",
190
+ JSON.stringify([{ type: "text", text: `old visible ${i}` }]),
191
+ );
192
+ }
193
+ for (let i = 0; i < 5; i++) {
194
+ await addMessage(
195
+ conv.id,
196
+ "assistant",
197
+ JSON.stringify([{ type: "text", text: `hidden ${i}` }]),
198
+ { hidden: true },
199
+ );
200
+ }
201
+
202
+ const latest = handleListMessages({
203
+ queryParams: { conversationId: conv.id, page: "latest", limit: "2" },
204
+ }) as {
205
+ messages: MessagePayload[];
206
+ hasMore: boolean;
207
+ oldestTimestamp: number | null;
208
+ };
209
+
210
+ expect(latest.messages.map((m) => m.content)).toEqual([
211
+ "old visible 0",
212
+ "old visible 1",
213
+ ]);
214
+ expect(latest.hasMore).toBe(false);
215
+ expect(latest.oldestTimestamp).not.toBeNull();
216
+ });
217
+ });
@@ -32,6 +32,11 @@ mock.module("../config/loader.js", () => ({
32
32
  }),
33
33
  }));
34
34
 
35
+ let mockAssistantName: string | null = null;
36
+ mock.module("../daemon/identity-helpers.js", () => ({
37
+ getAssistantName: () => mockAssistantName,
38
+ }));
39
+
35
40
  import { createConversation } from "../memory/conversation-crud.js";
36
41
  import { getDb } from "../memory/db-connection.js";
37
42
  import { initializeDb } from "../memory/db-init.js";
@@ -89,8 +94,10 @@ interface MessagePayload {
89
94
  timestamp: string;
90
95
  slackMessage?: {
91
96
  channelId: string;
97
+ channelName?: string;
92
98
  channelTs: string;
93
99
  threadTs?: string;
100
+ sender?: { displayName?: string; externalUserId?: string };
94
101
  messageLink?: { appUrl?: string; webUrl?: string };
95
102
  threadLink?: { appUrl?: string; webUrl?: string };
96
103
  };
@@ -104,14 +111,16 @@ interface ListResponse {
104
111
  }
105
112
 
106
113
  function callList(query: Record<string, string>): ListResponse {
107
- return handleListMessages(
108
- { queryParams: query },
109
- null,
110
- ) as unknown as ListResponse;
114
+ return handleListMessages({
115
+ queryParams: query,
116
+ }) as unknown as ListResponse;
111
117
  }
112
118
 
113
119
  describe("handleListMessages page=latest", () => {
114
- beforeEach(resetTables);
120
+ beforeEach(() => {
121
+ resetTables();
122
+ mockAssistantName = null;
123
+ });
115
124
 
116
125
  test("page=latest with no limit returns all messages chronologically", () => {
117
126
  const conv = createConversation();
@@ -219,8 +228,11 @@ describe("handleListMessages page=latest", () => {
219
228
  slackMeta: writeSlackMetadata({
220
229
  source: "slack",
221
230
  channelId: "C123ABCDEF",
231
+ channelName: "engineering",
222
232
  channelTs: "1710000000.000200",
223
233
  threadTs: "1710000000.000100",
234
+ displayName: "Alice",
235
+ actorExternalUserId: "U_ALICE",
224
236
  eventKind: "message",
225
237
  }),
226
238
  }),
@@ -232,13 +244,18 @@ describe("handleListMessages page=latest", () => {
232
244
 
233
245
  expect(body.messages[0].slackMessage).toEqual({
234
246
  channelId: "C123ABCDEF",
247
+ channelName: "engineering",
235
248
  channelTs: "1710000000.000200",
236
249
  threadTs: "1710000000.000100",
250
+ sender: {
251
+ displayName: "Alice",
252
+ externalUserId: "U_ALICE",
253
+ },
237
254
  messageLink: {
238
255
  appUrl:
239
256
  "slack://channel?team=T123&id=C123ABCDEF&message=1710000000.000200",
240
257
  webUrl:
241
- "https://example.slack.com/archives/C123ABCDEF/p1710000000000200",
258
+ "https://example.slack.com/archives/C123ABCDEF/p1710000000000200?thread_ts=1710000000.000100&cid=C123ABCDEF",
242
259
  },
243
260
  threadLink: {
244
261
  appUrl:
@@ -249,6 +266,107 @@ describe("handleListMessages page=latest", () => {
249
266
  });
250
267
  });
251
268
 
269
+ test("top-level Slack messages with matching threadTs use plain permalinks", () => {
270
+ const conv = createConversation();
271
+ const db = getDb();
272
+ db.insert(messages)
273
+ .values({
274
+ id: "msg-slack-top-level",
275
+ conversationId: conv.id,
276
+ role: "user",
277
+ content: JSON.stringify([{ type: "text", text: "Slack top-level" }]),
278
+ metadata: JSON.stringify({
279
+ slackMeta: writeSlackMetadata({
280
+ source: "slack",
281
+ channelId: "C123ABCDEF",
282
+ channelName: "engineering",
283
+ channelTs: "1710000000.000200",
284
+ threadTs: "1710000000.000200",
285
+ displayName: "Alice",
286
+ actorExternalUserId: "U_ALICE",
287
+ eventKind: "message",
288
+ }),
289
+ }),
290
+ createdAt: 1,
291
+ })
292
+ .run();
293
+
294
+ const body = callList({ conversationId: conv.id, page: "latest" });
295
+
296
+ expect(body.messages[0].slackMessage).toEqual({
297
+ channelId: "C123ABCDEF",
298
+ channelName: "engineering",
299
+ channelTs: "1710000000.000200",
300
+ threadTs: "1710000000.000200",
301
+ sender: {
302
+ displayName: "Alice",
303
+ externalUserId: "U_ALICE",
304
+ },
305
+ messageLink: {
306
+ appUrl:
307
+ "slack://channel?team=T123&id=C123ABCDEF&message=1710000000.000200",
308
+ webUrl:
309
+ "https://example.slack.com/archives/C123ABCDEF/p1710000000000200",
310
+ },
311
+ });
312
+ });
313
+
314
+ test("assistant Slack messages without stored sender use the assistant name", () => {
315
+ mockAssistantName = "Nova";
316
+ const conv = createConversation();
317
+ const db = getDb();
318
+ db.insert(messages)
319
+ .values({
320
+ id: "msg-slack-assistant",
321
+ conversationId: conv.id,
322
+ role: "assistant",
323
+ content: JSON.stringify([{ type: "text", text: "Slack response" }]),
324
+ metadata: JSON.stringify({
325
+ slackMeta: writeSlackMetadata({
326
+ source: "slack",
327
+ channelId: "C123ABCDEF",
328
+ channelTs: "1710000000.000300",
329
+ eventKind: "message",
330
+ }),
331
+ }),
332
+ createdAt: 1,
333
+ })
334
+ .run();
335
+
336
+ const body = callList({ conversationId: conv.id, page: "latest" });
337
+
338
+ expect(body.messages[0].slackMessage?.sender).toEqual({
339
+ displayName: "Nova",
340
+ });
341
+ });
342
+
343
+ test("user Slack messages without stored sender do not use the assistant name", () => {
344
+ mockAssistantName = "Nova";
345
+ const conv = createConversation();
346
+ const db = getDb();
347
+ db.insert(messages)
348
+ .values({
349
+ id: "msg-slack-user",
350
+ conversationId: conv.id,
351
+ role: "user",
352
+ content: JSON.stringify([{ type: "text", text: "Slack request" }]),
353
+ metadata: JSON.stringify({
354
+ slackMeta: writeSlackMetadata({
355
+ source: "slack",
356
+ channelId: "C123ABCDEF",
357
+ channelTs: "1710000000.000300",
358
+ eventKind: "message",
359
+ }),
360
+ }),
361
+ createdAt: 1,
362
+ })
363
+ .run();
364
+
365
+ const body = callList({ conversationId: conv.id, page: "latest" });
366
+
367
+ expect(body.messages[0].slackMessage?.sender).toBeUndefined();
368
+ });
369
+
252
370
  test("page=latest on unresolved conversationKey returns null metadata contract", () => {
253
371
  const body = callList({
254
372
  conversationKey: "no-such-key",
@@ -274,16 +392,14 @@ describe("handleListMessages page=latest", () => {
274
392
  const conv = createConversation();
275
393
 
276
394
  expect(() =>
277
- handleListMessages(
278
- { queryParams: { conversationId: conv.id, page: "invalid" } },
279
- null,
280
- ),
395
+ handleListMessages({
396
+ queryParams: { conversationId: conv.id, page: "invalid" },
397
+ }),
281
398
  ).toThrow(BadRequestError);
282
399
  expect(() =>
283
- handleListMessages(
284
- { queryParams: { conversationId: conv.id, page: "invalid" } },
285
- null,
286
- ),
400
+ handleListMessages({
401
+ queryParams: { conversationId: conv.id, page: "invalid" },
402
+ }),
287
403
  ).toThrow("page must be 'latest' when provided");
288
404
  });
289
405
 
@@ -91,7 +91,7 @@ describe("handleListMessages tool_result merging", () => {
91
91
  ]),
92
92
  );
93
93
 
94
- const response = handleListMessages(createTestArgs(conv.id), null);
94
+ const response = handleListMessages(createTestArgs(conv.id));
95
95
  const body = response as { messages: MessagePayload[] };
96
96
 
97
97
  // Should be 2 messages: user prompt + assistant (tool_result user msg suppressed)
@@ -137,7 +137,7 @@ describe("handleListMessages tool_result merging", () => {
137
137
  ]),
138
138
  );
139
139
 
140
- const response = handleListMessages(createTestArgs(conv.id), null);
140
+ const response = handleListMessages(createTestArgs(conv.id));
141
141
  const body = response as { messages: MessagePayload[] };
142
142
 
143
143
  expect(body.messages).toHaveLength(2);
@@ -167,7 +167,7 @@ describe("handleListMessages tool_result merging", () => {
167
167
  JSON.stringify([{ type: "text", text: "how are you?" }]),
168
168
  );
169
169
 
170
- const response = handleListMessages(createTestArgs(conv.id), null);
170
+ const response = handleListMessages(createTestArgs(conv.id));
171
171
  const body = response as { messages: MessagePayload[] };
172
172
 
173
173
  expect(body.messages).toHaveLength(3);
@@ -175,11 +175,15 @@ describe("handleListMessages tool_result merging", () => {
175
175
  expect(body.messages[2].content).toBe("how are you?");
176
176
  });
177
177
 
178
- test("tool_result at start of array (no preceding assistant) is preserved", async () => {
178
+ test("tool_result at start of array (no preceding assistant) is dropped", async () => {
179
179
  const conv = createConversation();
180
- // Orphan tool_result with no preceding assistant (pagination boundary).
181
- // The preceding assistant tool_use lives in the previous page dropping
182
- // the result would be unrecoverable, so it is kept as-is.
180
+ // Orphan tool_result with no preceding assistant. Without the parent
181
+ // tool_use we can't tell the user what tool ran, so the result is
182
+ // meaningless renderHistoryContent drops it rather than synthesizing
183
+ // a phantom "unknown" tool call. See shared.ts comment.
184
+ // The user message itself is preserved at the pagination boundary
185
+ // (mergeToolResultsIntoAssistantMessages keeps it to avoid data loss
186
+ // in case the matching tool_use lives on the previous page).
183
187
  await addMessage(
184
188
  conv.id,
185
189
  "user",
@@ -197,17 +201,14 @@ describe("handleListMessages tool_result merging", () => {
197
201
  JSON.stringify([{ type: "text", text: "response" }]),
198
202
  );
199
203
 
200
- const response = handleListMessages(createTestArgs(conv.id), null);
204
+ const response = handleListMessages(createTestArgs(conv.id));
201
205
  const body = response as { messages: MessagePayload[] };
202
206
 
203
- // Orphan tool_result is preserved (not suppressed) to avoid data loss
207
+ // Both messages exist user message preserved at pagination boundary,
208
+ // but the orphan tool_result is dropped (no phantom toolCalls)
204
209
  expect(body.messages).toHaveLength(2);
205
210
  expect(body.messages[0].role).toBe("user");
206
- // The preserved message must retain the actual tool_result payload
207
- const orphanToolCalls = body.messages[0].toolCalls;
208
- expect(orphanToolCalls).toBeDefined();
209
- expect(orphanToolCalls).toHaveLength(1);
210
- expect(orphanToolCalls![0].result).toBe("stale result");
211
+ expect(body.messages[0].toolCalls).toBeUndefined();
211
212
  expect(body.messages[1].role).toBe("assistant");
212
213
  expect(body.messages[1].content).toBe("response");
213
214
  });
@@ -262,7 +263,7 @@ describe("handleListMessages tool_result merging", () => {
262
263
  JSON.stringify([{ type: "text", text: "thanks" }]),
263
264
  );
264
265
 
265
- const response = handleListMessages(createTestArgs(conv.id), null);
266
+ const response = handleListMessages(createTestArgs(conv.id));
266
267
  const body = response as { messages: MessagePayload[] };
267
268
 
268
269
  // Consecutive assistant messages are merged at query time so the client
@@ -312,7 +313,7 @@ describe("handleListMessages tool_result merging", () => {
312
313
  ]),
313
314
  );
314
315
 
315
- const response = handleListMessages(createTestArgs(conv.id), null);
316
+ const response = handleListMessages(createTestArgs(conv.id));
316
317
  const body = response as { messages: MessagePayload[] };
317
318
 
318
319
  expect(body.messages).toHaveLength(2);
@@ -1152,7 +1152,6 @@ describe("normalizeLlmContextPayloads", () => {
1152
1152
  reasoning: { effort: "high" },
1153
1153
  max_output_tokens: 64000,
1154
1154
  stream: true,
1155
- store: false,
1156
1155
  },
1157
1156
  responsePayload: {
1158
1157
  id: "resp_abc123",
@@ -1245,7 +1244,6 @@ describe("normalizeLlmContextPayloads", () => {
1245
1244
  reasoning: { effort: "high" },
1246
1245
  max_output_tokens: 64000,
1247
1246
  stream: true,
1248
- store: false,
1249
1247
  },
1250
1248
  language: "json",
1251
1249
  },
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import { z } from "zod";
4
4
 
5
- import { resolveCallSiteConfig } from "../config/llm-resolver.js";
5
+ import { resolveCallSiteConfig, resolveDefaultProfileKey } from "../config/llm-resolver.js";
6
6
  import { type LLMCallSite, LLMSchema } from "../config/schemas/llm.js";
7
7
 
8
8
  const fullDefault = {
@@ -790,3 +790,87 @@ describe("resolveCallSiteConfig", () => {
790
790
  expect(resolved.provider_connection).toBe("anthropic-managed");
791
791
  });
792
792
  });
793
+
794
+ describe("resolveDefaultProfileKey", () => {
795
+ test("mainAgent returns activeProfile when set and enabled", () => {
796
+ const llm = LLMSchema.parse({
797
+ default: fullDefault,
798
+ profiles: {
799
+ balanced: { provider: "anthropic", model: "claude-sonnet-4-7" },
800
+ gemini: { provider: "gemini", model: "gemini-2.5-pro" },
801
+ },
802
+ activeProfile: "gemini",
803
+ });
804
+ expect(resolveDefaultProfileKey("mainAgent", llm)).toBe("gemini");
805
+ });
806
+
807
+ test("mainAgent falls back to catalog default when activeProfile is unset", () => {
808
+ const llm = LLMSchema.parse({
809
+ default: fullDefault,
810
+ profiles: {
811
+ balanced: { provider: "anthropic", model: "claude-sonnet-4-7" },
812
+ },
813
+ });
814
+ // mainAgent's CALL_SITE_DEFAULTS profile is `balanced`.
815
+ expect(resolveDefaultProfileKey("mainAgent", llm)).toBe("balanced");
816
+ });
817
+
818
+ test("mainAgent falls back to catalog default when activeProfile points to a missing profile", () => {
819
+ const llm = LLMSchema.parse({
820
+ default: fullDefault,
821
+ profiles: {
822
+ balanced: { provider: "anthropic", model: "claude-sonnet-4-7" },
823
+ },
824
+ });
825
+ // `LLMSchema.superRefine` rejects unknown `activeProfile` references at
826
+ // config-load time, so this branch is unreachable for a parsed config.
827
+ // Mutate after parse to exercise the resolver's defensive fall-through.
828
+ const mutated = { ...llm, activeProfile: "does-not-exist" };
829
+ expect(resolveDefaultProfileKey("mainAgent", mutated)).toBe("balanced");
830
+ });
831
+
832
+ test("mainAgent falls back to catalog default when activeProfile is disabled", () => {
833
+ const llm = LLMSchema.parse({
834
+ default: fullDefault,
835
+ profiles: {
836
+ balanced: { provider: "anthropic", model: "claude-sonnet-4-7" },
837
+ gemini: {
838
+ provider: "gemini",
839
+ model: "gemini-2.5-pro",
840
+ status: "disabled",
841
+ },
842
+ },
843
+ activeProfile: "gemini",
844
+ });
845
+ expect(resolveDefaultProfileKey("mainAgent", llm)).toBe("balanced");
846
+ });
847
+
848
+ test("non-mainAgent ignores activeProfile and returns catalog default", () => {
849
+ const llm = LLMSchema.parse({
850
+ default: fullDefault,
851
+ profiles: {
852
+ balanced: { provider: "anthropic", model: "claude-sonnet-4-7" },
853
+ "cost-optimized": { provider: "openai", model: "gpt-5-mini" },
854
+ gemini: { provider: "gemini", model: "gemini-2.5-pro" },
855
+ },
856
+ activeProfile: "gemini",
857
+ });
858
+ // filingAgent's CALL_SITE_DEFAULTS profile is `cost-optimized` — not gemini.
859
+ expect(resolveDefaultProfileKey("filingAgent", llm)).toBe("cost-optimized");
860
+ });
861
+
862
+ test("non-mainAgent falls back to custom-* when catalog profile is missing", () => {
863
+ const llm = LLMSchema.parse({
864
+ default: fullDefault,
865
+ profiles: {
866
+ "custom-cost-optimized": {
867
+ provider: "openai",
868
+ model: "gpt-5-mini",
869
+ },
870
+ },
871
+ });
872
+ expect(resolveDefaultProfileKey("filingAgent", llm)).toBe(
873
+ "custom-cost-optimized",
874
+ );
875
+ });
876
+ });