@vellumai/assistant 0.8.2 → 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 (503) hide show
  1. package/ARCHITECTURE.md +11 -12
  2. package/docker-entrypoint.sh +13 -2
  3. package/docker-init-apt-root.sh +79 -6
  4. package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
  5. package/openapi.yaml +945 -36
  6. package/package.json +1 -1
  7. package/src/__tests__/agent-loop-exit-reason.test.ts +271 -0
  8. package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
  9. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  10. package/src/__tests__/agent-loop.test.ts +88 -3
  11. package/src/__tests__/anthropic-provider.test.ts +272 -0
  12. package/src/__tests__/approval-cascade.test.ts +1 -1
  13. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  14. package/src/__tests__/channel-delivery-store.test.ts +193 -0
  15. package/src/__tests__/channel-reply-delivery.test.ts +284 -5
  16. package/src/__tests__/channel-retry-sweep.test.ts +274 -1
  17. package/src/__tests__/compaction-events.test.ts +1 -1
  18. package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
  19. package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
  20. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  21. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  22. package/src/__tests__/config-watcher.test.ts +1 -1
  23. package/src/__tests__/context-token-estimator.test.ts +112 -57
  24. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  25. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
  26. package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
  27. package/src/__tests__/conversation-agent-loop.test.ts +77 -3
  28. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
  29. package/src/__tests__/conversation-clean-command.test.ts +137 -0
  30. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  31. package/src/__tests__/conversation-fork-crud.test.ts +161 -0
  32. package/src/__tests__/conversation-lifecycle.test.ts +1 -1
  33. package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
  34. package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
  35. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  36. package/src/__tests__/conversation-pairing.test.ts +2 -2
  37. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  38. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  39. package/src/__tests__/conversation-queue.test.ts +1 -1
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +290 -85
  41. package/src/__tests__/conversation-seed-composer.test.ts +66 -4
  42. package/src/__tests__/conversation-slash-commands.test.ts +36 -8
  43. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  44. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  45. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  46. package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
  47. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  48. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  49. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  50. package/src/__tests__/credential-security-invariants.test.ts +6 -0
  51. package/src/__tests__/cu-unified-flow.test.ts +10 -1
  52. package/src/__tests__/date-context.test.ts +45 -0
  53. package/src/__tests__/dm-backfill.test.ts +64 -0
  54. package/src/__tests__/dm-persistence.test.ts +33 -0
  55. package/src/__tests__/document-find-replace.test.ts +501 -0
  56. package/src/__tests__/external-plugin-loader.test.ts +91 -19
  57. package/src/__tests__/first-greeting.test.ts +23 -2
  58. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  59. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  60. package/src/__tests__/headless-browser-navigate.test.ts +172 -0
  61. package/src/__tests__/heartbeat-service.test.ts +24 -164
  62. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  63. package/src/__tests__/host-app-control-proxy.test.ts +241 -0
  64. package/src/__tests__/host-bash-proxy.test.ts +6 -0
  65. package/src/__tests__/host-browser-proxy.test.ts +10 -0
  66. package/src/__tests__/host-cu-proxy.test.ts +8 -1
  67. package/src/__tests__/host-file-proxy.test.ts +8 -1
  68. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  69. package/src/__tests__/host-transfer-proxy.test.ts +8 -1
  70. package/src/__tests__/identity-routes.test.ts +57 -0
  71. package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
  72. package/src/__tests__/injector-background-turn.test.ts +153 -0
  73. package/src/__tests__/injector-chain.test.ts +7 -0
  74. package/src/__tests__/injector-document-comments.test.ts +378 -0
  75. package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
  76. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
  77. package/src/__tests__/list-messages-attachments.test.ts +21 -17
  78. package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
  79. package/src/__tests__/list-messages-page-latest.test.ts +130 -14
  80. package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
  81. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  82. package/src/__tests__/llm-catalog-parity.test.ts +3 -0
  83. package/src/__tests__/llm-context-normalization.test.ts +0 -2
  84. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  85. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  86. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
  87. package/src/__tests__/llm-resolver.test.ts +340 -3
  88. package/src/__tests__/log-export-routes.test.ts +99 -2
  89. package/src/__tests__/managed-profile-guard.test.ts +10 -0
  90. package/src/__tests__/message-queue-steer.test.ts +114 -0
  91. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  92. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  93. package/src/__tests__/notification-deep-link.test.ts +15 -0
  94. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  95. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  96. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  97. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  98. package/src/__tests__/openai-provider.test.ts +323 -3
  99. package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
  100. package/src/__tests__/openai-responses-provider.test.ts +4 -4
  101. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  102. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  103. package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
  104. package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
  105. package/src/__tests__/platform-proxy-context.test.ts +6 -1
  106. package/src/__tests__/platform.test.ts +0 -3
  107. package/src/__tests__/plugin-source-watcher.test.ts +302 -0
  108. package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
  109. package/src/__tests__/plugin-types.test.ts +2 -2
  110. package/src/__tests__/process-message-background-slack.test.ts +1 -51
  111. package/src/__tests__/process-message-display-content.test.ts +21 -16
  112. package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
  113. package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
  114. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
  115. package/src/__tests__/server-history-render.test.ts +83 -4
  116. package/src/__tests__/steer-tool-repair.test.ts +249 -0
  117. package/src/__tests__/system-prompt.test.ts +57 -101
  118. package/src/__tests__/terminal-tools.test.ts +11 -1
  119. package/src/__tests__/thinking-block-replay.test.ts +113 -0
  120. package/src/__tests__/thread-backfill.test.ts +370 -22
  121. package/src/__tests__/tool-executor.test.ts +90 -1
  122. package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
  123. package/src/__tests__/twilio-routes.test.ts +1 -1
  124. package/src/__tests__/web-fetch.test.ts +2 -2
  125. package/src/__tests__/workspace-git-service.test.ts +88 -5
  126. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  127. package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
  128. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  129. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  130. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  131. package/src/a2a/__tests__/task-store.test.ts +246 -0
  132. package/src/a2a/agent-card.ts +58 -0
  133. package/src/a2a/feature-gate.ts +8 -0
  134. package/src/a2a/protocol-constants.ts +21 -0
  135. package/src/a2a/protocol-errors.ts +50 -0
  136. package/src/a2a/protocol-types.ts +162 -0
  137. package/src/a2a/task-store.ts +168 -0
  138. package/src/agent/attachments.ts +1 -0
  139. package/src/agent/loop.ts +208 -22
  140. package/src/background-wake/next-wake.test.ts +289 -0
  141. package/src/background-wake/next-wake.ts +172 -0
  142. package/src/browser/operations.ts +15 -0
  143. package/src/channels/config.ts +9 -0
  144. package/src/channels/types.ts +14 -0
  145. package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
  146. package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
  147. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  148. package/src/cli/commands/__tests__/schedules.test.ts +469 -0
  149. package/src/cli/commands/conversations.ts +128 -1
  150. package/src/cli/commands/inference-providers.ts +147 -1
  151. package/src/cli/commands/memory-v2.ts +308 -0
  152. package/src/cli/commands/notifications.ts +89 -37
  153. package/src/cli/commands/plugins.ts +67 -0
  154. package/src/cli/commands/schedules.ts +297 -5
  155. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  156. package/src/cli/lib/install-from-github.ts +8 -9
  157. package/src/cli/lib/search-plugins.ts +163 -0
  158. package/src/cli/program.ts +14 -0
  159. package/src/cli/utils/conversation-id.ts +17 -5
  160. package/src/config/assistant-feature-flags.ts +24 -54
  161. package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
  162. package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
  163. package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
  164. package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
  165. package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
  166. package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
  167. package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
  168. package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
  169. package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
  170. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  171. package/src/config/bundled-skills/schedule/SKILL.md +8 -0
  172. package/src/config/bundled-tool-registry.ts +22 -12
  173. package/src/config/call-site-defaults.ts +124 -0
  174. package/src/config/feature-flag-registry.json +111 -23
  175. package/src/config/llm-resolver.ts +66 -1
  176. package/src/config/schema.ts +2 -0
  177. package/src/config/schemas/__tests__/memory-v2.test.ts +7 -3
  178. package/src/config/schemas/call-site-catalog.ts +21 -0
  179. package/src/config/schemas/channels.ts +9 -0
  180. package/src/config/schemas/conversations.ts +10 -0
  181. package/src/config/schemas/heartbeat.ts +14 -0
  182. package/src/config/schemas/llm.ts +4 -3
  183. package/src/config/schemas/memory-retrospective.ts +1 -1
  184. package/src/config/schemas/memory-v2.ts +51 -4
  185. package/src/config/schemas/memory.ts +3 -1
  186. package/src/config/seed-inference-profiles.ts +99 -29
  187. package/src/context/compactor.ts +80 -13
  188. package/src/context/token-estimator.ts +72 -31
  189. package/src/context/window-manager.ts +25 -0
  190. package/src/credential-health/credential-health-service.ts +34 -19
  191. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
  192. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
  193. package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
  194. package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
  195. package/src/daemon/conversation-agent-loop-handlers.ts +231 -23
  196. package/src/daemon/conversation-agent-loop.ts +252 -56
  197. package/src/daemon/conversation-lifecycle.ts +142 -116
  198. package/src/daemon/conversation-messaging.ts +3 -0
  199. package/src/daemon/conversation-process.ts +273 -0
  200. package/src/daemon/conversation-queue-manager.ts +14 -0
  201. package/src/daemon/conversation-runtime-assembly.ts +144 -75
  202. package/src/daemon/conversation-slash.ts +37 -5
  203. package/src/daemon/conversation-surfaces.ts +45 -2
  204. package/src/daemon/conversation-tool-setup.ts +7 -0
  205. package/src/daemon/conversation.ts +42 -12
  206. package/src/daemon/date-context.ts +40 -0
  207. package/src/daemon/first-greeting.ts +10 -0
  208. package/src/daemon/guardian-action-generators.ts +1 -125
  209. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
  210. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  211. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  212. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  213. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  214. package/src/daemon/handlers/config-a2a.ts +449 -0
  215. package/src/daemon/handlers/config-model.test.ts +1 -0
  216. package/src/daemon/handlers/conversations.ts +80 -0
  217. package/src/daemon/handlers/shared.ts +92 -29
  218. package/src/daemon/host-app-control-proxy.ts +69 -18
  219. package/src/daemon/host-bash-proxy.ts +1 -1
  220. package/src/daemon/host-cu-proxy.ts +1 -1
  221. package/src/daemon/host-file-proxy.ts +1 -1
  222. package/src/daemon/host-proxy-preactivation.ts +85 -18
  223. package/src/daemon/host-transfer-proxy.ts +1 -1
  224. package/src/daemon/lifecycle.ts +67 -65
  225. package/src/daemon/memory-v2-startup.ts +49 -13
  226. package/src/daemon/message-protocol.ts +4 -0
  227. package/src/daemon/message-types/conversations.ts +8 -0
  228. package/src/daemon/message-types/document-comments.ts +50 -0
  229. package/src/daemon/message-types/messages.ts +68 -1
  230. package/src/daemon/message-types/notifications.ts +21 -0
  231. package/src/daemon/message-types/surfaces.ts +3 -1
  232. package/src/daemon/message-types/web-activity.ts +57 -0
  233. package/src/daemon/pkb-reminder-builder.test.ts +10 -53
  234. package/src/daemon/pkb-reminder-builder.ts +4 -19
  235. package/src/daemon/plugin-source-watcher.ts +135 -3
  236. package/src/daemon/process-message.ts +72 -12
  237. package/src/daemon/query-complexity-router.ts +75 -0
  238. package/src/daemon/skill-memory-refresh.ts +5 -1
  239. package/src/daemon/trust-context.ts +6 -0
  240. package/src/daemon/wake-target-adapter.ts +2 -0
  241. package/src/documents/document-comments-store.test.ts +338 -0
  242. package/src/documents/document-comments-store.ts +237 -0
  243. package/src/documents/document-store.ts +202 -0
  244. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  245. package/src/export/transcript-formatter.ts +54 -20
  246. package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -1
  247. package/src/heartbeat/heartbeat-service.ts +35 -191
  248. package/src/home/__tests__/feed-types.test.ts +40 -0
  249. package/src/home/__tests__/suggested-prompts.test.ts +33 -2
  250. package/src/home/feed-types.ts +20 -3
  251. package/src/home/home-content-refresh.ts +52 -0
  252. package/src/home/home-greeting-cache.ts +69 -0
  253. package/src/home/home-greeting.ts +94 -0
  254. package/src/home/suggested-prompts.ts +177 -9
  255. package/src/ipc/cli-client.ts +147 -45
  256. package/src/memory/__tests__/conversation-queries.test.ts +220 -0
  257. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
  258. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  259. package/src/memory/__tests__/memory-retrospective-job.test.ts +407 -10
  260. package/src/memory/conversation-crud.ts +133 -43
  261. package/src/memory/conversation-queries.ts +87 -1
  262. package/src/memory/conversation-title-service.ts +26 -4
  263. package/src/memory/db-init.ts +22 -0
  264. package/src/memory/delivery-crud.ts +41 -0
  265. package/src/memory/delivery-status.ts +141 -15
  266. package/src/memory/external-conversation-store.ts +32 -1
  267. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
  268. package/src/memory/graph/conversation-graph-memory.ts +18 -6
  269. package/src/memory/graph/tools.ts +6 -37
  270. package/src/memory/invite-store.ts +53 -0
  271. package/src/memory/jobs-worker.ts +21 -1
  272. package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
  273. package/src/memory/llm-request-log-store.ts +92 -1
  274. package/src/memory/memory-retrospective-constants.ts +28 -0
  275. package/src/memory/memory-retrospective-enqueue.ts +4 -22
  276. package/src/memory/memory-retrospective-job.ts +438 -21
  277. package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
  278. package/src/memory/memory-v2-activation-log-store.ts +26 -8
  279. package/src/memory/migrations/100-core-tables.ts +1 -0
  280. package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
  281. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  282. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  283. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  284. package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
  285. package/src/memory/migrations/253-document-comments.ts +47 -0
  286. package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
  287. package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
  288. package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
  289. package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
  290. package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
  291. package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
  292. package/src/memory/migrations/index.ts +20 -0
  293. package/src/memory/migrations/registry.ts +33 -0
  294. package/src/memory/onboarding-events-store.ts +7 -0
  295. package/src/memory/schema/a2a.ts +15 -0
  296. package/src/memory/schema/calls.ts +1 -0
  297. package/src/memory/schema/conversations.ts +3 -0
  298. package/src/memory/schema/index.ts +1 -0
  299. package/src/memory/schema/inference.ts +2 -0
  300. package/src/memory/schema/infrastructure.ts +2 -0
  301. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  302. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  303. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  304. package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
  305. package/src/memory/v2/__tests__/injection.test.ts +221 -17
  306. package/src/memory/v2/__tests__/page-index.test.ts +365 -1
  307. package/src/memory/v2/__tests__/router.test.ts +489 -1
  308. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  309. package/src/memory/v2/activation-store.ts +14 -16
  310. package/src/memory/v2/cli-command-content.ts +19 -0
  311. package/src/memory/v2/cli-command-store.ts +304 -0
  312. package/src/memory/v2/consolidation-job.ts +14 -0
  313. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  314. package/src/memory/v2/injection-events.ts +101 -0
  315. package/src/memory/v2/injection.ts +69 -29
  316. package/src/memory/v2/page-index.ts +246 -19
  317. package/src/memory/v2/page-store.ts +18 -0
  318. package/src/memory/v2/router.ts +209 -55
  319. package/src/memory/v2/static-context.ts +4 -4
  320. package/src/memory/v2/types.ts +23 -0
  321. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  322. package/src/messaging/providers/a2a/deliver.ts +156 -0
  323. package/src/messaging/providers/gmail/client.ts +9 -2
  324. package/src/messaging/providers/index.ts +18 -3
  325. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
  326. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
  327. package/src/messaging/providers/slack/adapter.ts +178 -25
  328. package/src/messaging/providers/slack/api.test.ts +54 -0
  329. package/src/messaging/providers/slack/api.ts +119 -3
  330. package/src/messaging/providers/slack/client.ts +12 -0
  331. package/src/messaging/providers/slack/deep-link.ts +20 -1
  332. package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
  333. package/src/messaging/providers/slack/message-metadata.ts +156 -0
  334. package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
  335. package/src/messaging/providers/slack/render-transcript.ts +176 -49
  336. package/src/messaging/providers/slack/send.test.ts +77 -0
  337. package/src/messaging/providers/slack/send.ts +8 -2
  338. package/src/messaging/providers/slack/types.ts +14 -0
  339. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  340. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  341. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  342. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +5 -1
  343. package/src/notifications/__tests__/home-feed-side-effect.test.ts +521 -36
  344. package/src/notifications/adapters/macos.ts +12 -2
  345. package/src/notifications/broadcaster.ts +29 -4
  346. package/src/notifications/conversation-seed-composer.ts +14 -2
  347. package/src/notifications/copy-composer.ts +17 -64
  348. package/src/notifications/decision-engine.ts +111 -44
  349. package/src/notifications/deferred-emit.ts +135 -0
  350. package/src/notifications/deterministic-checks.ts +96 -0
  351. package/src/notifications/emit-signal.ts +10 -1
  352. package/src/notifications/home-feed-side-effect.ts +136 -27
  353. package/src/notifications/signal.ts +0 -4
  354. package/src/notifications/types.ts +8 -0
  355. package/src/oauth/connect-orchestrator.ts +3 -0
  356. package/src/oauth/credential-token-resolver.ts +2 -0
  357. package/src/oauth/manual-token-connection.ts +19 -0
  358. package/src/oauth/oauth-store.ts +12 -0
  359. package/src/oauth/platform-connection.test.ts +43 -3
  360. package/src/oauth/platform-connection.ts +13 -4
  361. package/src/oauth/seed-providers.ts +22 -0
  362. package/src/permissions/prompter.ts +5 -2
  363. package/src/permissions/secret-prompter.ts +4 -1
  364. package/src/plugins/defaults/injectors.ts +118 -26
  365. package/src/plugins/external-plugin-loader.ts +82 -10
  366. package/src/plugins/types.ts +16 -7
  367. package/src/prompts/__tests__/system-prompt.test.ts +44 -45
  368. package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
  369. package/src/prompts/normalize-onboarding.ts +40 -0
  370. package/src/prompts/sections.ts +32 -14
  371. package/src/prompts/system-prompt.ts +105 -76
  372. package/src/prompts/template-detection.ts +37 -0
  373. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
  374. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  375. package/src/prompts/templates/VOICE.md +3 -0
  376. package/src/prompts/templates/system-sections.ts +51 -10
  377. package/src/providers/__tests__/inference.test.ts +2 -0
  378. package/src/providers/anthropic/client.ts +132 -5
  379. package/src/providers/call-site-routing.ts +24 -6
  380. package/src/providers/connection-resolution.ts +63 -13
  381. package/src/providers/fireworks/client.ts +20 -2
  382. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  383. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
  384. package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
  385. package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
  386. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  387. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  388. package/src/providers/inference/adapter-factory.ts +24 -21
  389. package/src/providers/inference/auth.ts +15 -3
  390. package/src/providers/inference/backfill.ts +14 -1
  391. package/src/providers/inference/codex-token-refresh.ts +128 -0
  392. package/src/providers/inference/connections.ts +85 -5
  393. package/src/providers/inference/resolve-auth.ts +50 -5
  394. package/src/providers/model-catalog.ts +244 -242
  395. package/src/providers/model-intents.ts +3 -3
  396. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  397. package/src/providers/openai/chat-completions-provider.ts +215 -25
  398. package/src/providers/openai/responses-provider.ts +9 -3
  399. package/src/providers/openrouter/client.ts +46 -4
  400. package/src/providers/platform-proxy/constants.ts +3 -4
  401. package/src/providers/provider-catalog-visibility.ts +3 -1
  402. package/src/providers/provider-send-message.ts +27 -12
  403. package/src/providers/registry.ts +30 -1
  404. package/src/providers/types.ts +25 -0
  405. package/src/runtime/__tests__/agent-wake.test.ts +214 -0
  406. package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
  407. package/src/runtime/agent-wake.ts +212 -57
  408. package/src/runtime/auth/route-policy.ts +20 -3
  409. package/src/runtime/background-job-runner.ts +26 -0
  410. package/src/runtime/channel-reply-delivery.ts +182 -47
  411. package/src/runtime/channel-retry-sweep.ts +141 -16
  412. package/src/runtime/http-server.ts +7 -16
  413. package/src/runtime/http-types.ts +7 -51
  414. package/src/runtime/pending-interactions.ts +51 -8
  415. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  416. package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
  417. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +121 -5
  418. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  419. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  420. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
  421. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
  422. package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
  423. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
  424. package/src/runtime/routes/approval-routes.ts +4 -1
  425. package/src/runtime/routes/channel-availability-routes.ts +5 -0
  426. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
  427. package/src/runtime/routes/consolidation-routes.ts +100 -0
  428. package/src/runtime/routes/content-source-routes.ts +78 -0
  429. package/src/runtime/routes/conversation-cli-routes.ts +146 -1
  430. package/src/runtime/routes/conversation-query-routes.ts +130 -12
  431. package/src/runtime/routes/conversation-routes.ts +288 -76
  432. package/src/runtime/routes/document-comments-routes.ts +287 -0
  433. package/src/runtime/routes/documents-routes.ts +33 -0
  434. package/src/runtime/routes/home-feed-routes.ts +6 -3
  435. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  436. package/src/runtime/routes/host-browser-routes.ts +8 -1
  437. package/src/runtime/routes/identity-routes.ts +21 -0
  438. package/src/runtime/routes/inbound-message-handler.ts +288 -58
  439. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
  440. package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
  441. package/src/runtime/routes/index.ts +14 -4
  442. package/src/runtime/routes/inference-provider-connection-routes.ts +192 -3
  443. package/src/runtime/routes/integrations/a2a.ts +294 -0
  444. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  445. package/src/runtime/routes/log-export-routes.ts +39 -0
  446. package/src/runtime/routes/memory-v2-routes.ts +217 -0
  447. package/src/runtime/routes/notification-routes.ts +19 -2
  448. package/src/runtime/routes/question-routes.ts +4 -1
  449. package/src/runtime/routes/sanity-routes.ts +159 -0
  450. package/src/runtime/routes/slack-channel-routes.ts +187 -0
  451. package/src/runtime/routes/subagents-routes.ts +41 -0
  452. package/src/runtime/services/conversation-serializer.ts +30 -4
  453. package/src/schedule/integration-status.ts +3 -1
  454. package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
  455. package/src/security/oauth2-device-code.ts +307 -0
  456. package/src/security/oauth2.ts +26 -9
  457. package/src/security/secure-keys.ts +5 -0
  458. package/src/skills/catalog-install.ts +6 -2
  459. package/src/subagent/manager.ts +2 -0
  460. package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
  461. package/src/tools/browser/browser-execution.ts +93 -0
  462. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
  463. package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
  464. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
  465. package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
  466. package/src/tools/browser/cdp-client/factory.ts +87 -3
  467. package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
  468. package/src/tools/browser/cdp-client/types.ts +36 -0
  469. package/src/tools/browser/pinned-tabs.ts +90 -0
  470. package/src/tools/document/document-comment-tool.test.ts +379 -0
  471. package/src/tools/document/document-comment-tool.ts +156 -0
  472. package/src/tools/document/document-tool.ts +128 -2
  473. package/src/tools/memory/register.ts +1 -9
  474. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
  475. package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
  476. package/src/tools/network/domain-normalize.ts +17 -0
  477. package/src/tools/network/web-fetch.ts +213 -64
  478. package/src/tools/network/web-search.ts +191 -66
  479. package/src/tools/registry.ts +2 -2
  480. package/src/tools/terminal/safe-env.ts +3 -2
  481. package/src/tools/tool-approval-handler.ts +19 -12
  482. package/src/tools/types.ts +41 -2
  483. package/src/tools/ui-surface/definitions.ts +3 -1
  484. package/src/types/onboarding-context.ts +4 -0
  485. package/src/util/__tests__/favicon.test.ts +84 -0
  486. package/src/util/favicon.ts +40 -0
  487. package/src/util/platform.ts +0 -5
  488. package/src/workspace/git-service.ts +75 -4
  489. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  490. package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
  491. package/src/workspace/migrations/registry.ts +4 -0
  492. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  493. package/src/config/bundled-skills/document/SKILL.md +0 -54
  494. package/src/config/bundled-skills/document/TOOLS.json +0 -106
  495. package/src/daemon/seed-files.ts +0 -18
  496. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  497. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
  498. package/src/runtime/routes/interface-routes.ts +0 -43
  499. /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
  500. /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
  501. /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
  502. /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
  503. /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
@@ -18,7 +18,14 @@
18
18
  * resolve the interaction.
19
19
  */
20
20
 
21
+ import type { InteractionResolutionState } from "../daemon/message-types/messages.js";
21
22
  import type { UserDecision } from "../permissions/types.js";
23
+ import { getLogger } from "../util/logger.js";
24
+ import { broadcastMessage } from "./assistant-event-hub.js";
25
+
26
+ const log = getLogger("pending-interactions");
27
+
28
+ export type { InteractionResolutionState } from "../daemon/message-types/messages.js";
22
29
 
23
30
  export interface ConfirmationDetails {
24
31
  toolName: string;
@@ -98,17 +105,50 @@ export function register(
98
105
  * Remove and return the pending interaction for the given requestId.
99
106
  * Auto-clears the proxy timer and detaches the abort listener if present.
100
107
  * Returns undefined if no interaction is registered.
108
+ *
109
+ * Emits `interaction_resolved` on the event hub when an interaction is
110
+ * actually removed (no-op when the entry was already consumed by another
111
+ * path). Callers pass `state` to communicate the lifecycle outcome
112
+ * — defaults to `"cancelled"`, the safest value when the call site has
113
+ * no extra context.
101
114
  */
102
- export function resolve(requestId: string): PendingInteraction | undefined {
115
+ export function resolve(
116
+ requestId: string,
117
+ state: InteractionResolutionState = "cancelled",
118
+ ): PendingInteraction | undefined {
103
119
  const interaction = pending.get(requestId);
104
- if (interaction) {
105
- pending.delete(requestId);
106
- if (interaction.timer != null) clearTimeout(interaction.timer);
107
- interaction.detachAbort?.();
108
- }
120
+ if (!interaction) return undefined;
121
+ pending.delete(requestId);
122
+ if (interaction.timer != null) clearTimeout(interaction.timer);
123
+ interaction.detachAbort?.();
124
+ emitResolved(requestId, interaction, state);
109
125
  return interaction;
110
126
  }
111
127
 
128
+ function emitResolved(
129
+ requestId: string,
130
+ interaction: PendingInteraction,
131
+ state: InteractionResolutionState,
132
+ ): void {
133
+ log.info(
134
+ {
135
+ requestId,
136
+ conversationId: interaction.conversationId,
137
+ kind: interaction.kind,
138
+ state,
139
+ },
140
+ "Pending interaction resolved",
141
+ );
142
+ broadcastMessage({
143
+ type: "interaction_resolved",
144
+ requestId,
145
+ conversationId: interaction.conversationId,
146
+ conversationKey: interaction.conversationId,
147
+ kind: interaction.kind,
148
+ state,
149
+ });
150
+ }
151
+
112
152
  /**
113
153
  * Return the pending interaction without removing it.
114
154
  * Used by trust-rule endpoint which doesn't resolve the confirmation itself.
@@ -146,7 +186,10 @@ export function getByConversation(
146
186
  * /v1/host-transfer-result after completing the operation, get a 404, and the
147
187
  * proxy timer would fire with a spurious timeout error.
148
188
  */
149
- export function removeByConversation(conversationId: string): void {
189
+ export function removeByConversation(
190
+ conversationId: string,
191
+ state: InteractionResolutionState = "superseded",
192
+ ): void {
150
193
  // Snapshot keys to avoid mutation-during-iteration.
151
194
  for (const [requestId, interaction] of [...pending]) {
152
195
  if (
@@ -160,7 +203,7 @@ export function removeByConversation(conversationId: string): void {
160
203
  interaction.kind !== "acp_confirmation"
161
204
  ) {
162
205
  // resolve() clears the stored timer and detaches abort listeners.
163
- resolve(requestId);
206
+ resolve(requestId, state);
164
207
  }
165
208
  }
166
209
  }
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Asserts `listConsolidationRuns` maps background-conversation rows tagged
3
+ * with `source = MEMORY_V2_CONSOLIDATION_SOURCE` into the heartbeat-runs
4
+ * response shape, derives `status` / `finishedAt` / `durationMs` from
5
+ * **assistant-message presence** (not `lastMessageAt`), and clamps the
6
+ * `limit` query param.
7
+ *
8
+ * Synthetic-field semantics covered here:
9
+ * - `id` and `conversationId` both equal the conversation row's id.
10
+ * - `scheduledFor` and `startedAt` both equal `conversation.createdAt`
11
+ * (no separate schedule timestamp on the row).
12
+ * - `finishedAt` is the `createdAt` of the LATEST assistant message,
13
+ * NOT `conversation.lastMessageAt` — the kickoff user prompt bumps
14
+ * `lastMessageAt` before the agent runs, so it cannot be used as a
15
+ * completion signal.
16
+ * - `durationMs` is `finishedAt − startedAt` when both are present, else
17
+ * null.
18
+ * - `status` is `"ok"` when the conversation has at least one assistant
19
+ * message (positive evidence the agent emitted output) and `"running"`
20
+ * otherwise — including the case where only the kickoff user prompt
21
+ * has been persisted.
22
+ * - `skipReason` and `error` are always null — the conversation row
23
+ * alone cannot distinguish a clean run from a mid-flight crash even
24
+ * once assistant output exists.
25
+ */
26
+
27
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
28
+
29
+ mock.module("../../../util/logger.js", () => ({
30
+ getLogger: () =>
31
+ new Proxy({} as Record<string, unknown>, {
32
+ get: () => () => {},
33
+ }),
34
+ }));
35
+
36
+ import { createConversation } from "../../../memory/conversation-crud.js";
37
+ import { getDb } from "../../../memory/db-connection.js";
38
+ import { initializeDb } from "../../../memory/db-init.js";
39
+ import { rawRun } from "../../../memory/raw-query.js";
40
+ import { ROUTES } from "../consolidation-routes.js";
41
+ import type { RouteDefinition } from "../types.js";
42
+
43
+ initializeDb();
44
+
45
+ function resetTables(): void {
46
+ const db = getDb();
47
+ db.run(`DELETE FROM messages`);
48
+ db.run(`DELETE FROM conversations`);
49
+ }
50
+
51
+ function findHandler(operationId: string): RouteDefinition["handler"] {
52
+ const route = ROUTES.find((r) => r.operationId === operationId);
53
+ if (!route) throw new Error(`Route ${operationId} not found`);
54
+ return route.handler;
55
+ }
56
+
57
+ function insertMessage(
58
+ conversationId: string,
59
+ role: string,
60
+ createdAt: number,
61
+ ): void {
62
+ rawRun(
63
+ "INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES (?, ?, ?, ?, ?)",
64
+ `msg-${conversationId}-${role}-${createdAt}`,
65
+ conversationId,
66
+ role,
67
+ "x",
68
+ createdAt,
69
+ );
70
+ }
71
+
72
+ interface RunRecord {
73
+ id: string;
74
+ scheduledFor: number;
75
+ startedAt: number | null;
76
+ finishedAt: number | null;
77
+ durationMs: number | null;
78
+ status: "ok" | "running";
79
+ skipReason: string | null;
80
+ error: string | null;
81
+ conversationId: string | null;
82
+ createdAt: number;
83
+ }
84
+
85
+ interface ListRunsResponse {
86
+ runs: RunRecord[];
87
+ }
88
+
89
+ describe("listConsolidationRuns handler", () => {
90
+ beforeEach(() => {
91
+ resetTables();
92
+ });
93
+
94
+ test("returns only conversations sourced from memory_v2_consolidation", async () => {
95
+ createConversation({ title: "c1", source: "memory_v2_consolidation" });
96
+ createConversation({ title: "h1", source: "heartbeat" });
97
+ createConversation({ title: "u1", source: "user" });
98
+
99
+ const handler = findHandler("listConsolidationRuns");
100
+ const result = (await handler({})) as ListRunsResponse;
101
+
102
+ expect(result.runs).toHaveLength(1);
103
+ });
104
+
105
+ test("synthesizes status='ok' with finishedAt from latest assistant message", async () => {
106
+ const conv = createConversation({
107
+ title: "c1",
108
+ source: "memory_v2_consolidation",
109
+ });
110
+ rawRun(
111
+ "UPDATE conversations SET created_at = ? WHERE id = ?",
112
+ 1000,
113
+ conv.id,
114
+ );
115
+ // Kickoff user prompt at t=1100 (bumps lastMessageAt — must NOT be
116
+ // mistaken for completion).
117
+ insertMessage(conv.id, "user", 1100);
118
+ // Agent's first assistant turn at t=2000.
119
+ insertMessage(conv.id, "assistant", 2000);
120
+ // Agent's final assistant turn at t=2500.
121
+ insertMessage(conv.id, "assistant", 2500);
122
+
123
+ const handler = findHandler("listConsolidationRuns");
124
+ const result = (await handler({})) as ListRunsResponse;
125
+
126
+ expect(result.runs).toHaveLength(1);
127
+ const run = result.runs[0]!;
128
+ expect(run.id).toBe(conv.id);
129
+ expect(run.conversationId).toBe(conv.id);
130
+ expect(run.status).toBe("ok");
131
+ expect(run.scheduledFor).toBe(1000);
132
+ expect(run.startedAt).toBe(1000);
133
+ // finishedAt = createdAt of LATEST assistant message (2500), NOT
134
+ // the conversation's lastMessageAt (which sqlite triggers may or may
135
+ // not have updated here — irrelevant to this endpoint).
136
+ expect(run.finishedAt).toBe(2500);
137
+ expect(run.durationMs).toBe(1500);
138
+ expect(run.createdAt).toBe(1000);
139
+ });
140
+
141
+ test("synthesizes status='running' when conversation has no assistant message", async () => {
142
+ createConversation({ title: "c1", source: "memory_v2_consolidation" });
143
+
144
+ const handler = findHandler("listConsolidationRuns");
145
+ const result = (await handler({})) as ListRunsResponse;
146
+
147
+ expect(result.runs).toHaveLength(1);
148
+ const run = result.runs[0]!;
149
+ expect(run.status).toBe("running");
150
+ expect(run.finishedAt).toBeNull();
151
+ expect(run.durationMs).toBeNull();
152
+ });
153
+
154
+ test("status stays 'running' when only the kickoff user prompt exists (Codex bug regression guard)", async () => {
155
+ // Regression guard for the original `status from lastMessageAt`
156
+ // heuristic. `processMessage` persists the background kickoff prompt as
157
+ // a user message BEFORE the agent runs, which bumps
158
+ // `conversation.lastMessageAt`. A run that timed out / threw before
159
+ // emitting any assistant turn must still report status='running' (or
160
+ // an explicit failure status once one exists) — never 'ok'.
161
+ const conv = createConversation({
162
+ title: "c1",
163
+ source: "memory_v2_consolidation",
164
+ });
165
+ rawRun(
166
+ "UPDATE conversations SET created_at = ?, last_message_at = ? WHERE id = ?",
167
+ 1000,
168
+ 1100,
169
+ conv.id,
170
+ );
171
+ insertMessage(conv.id, "user", 1100);
172
+
173
+ const handler = findHandler("listConsolidationRuns");
174
+ const result = (await handler({})) as ListRunsResponse;
175
+
176
+ expect(result.runs).toHaveLength(1);
177
+ const run = result.runs[0]!;
178
+ expect(run.status).toBe("running");
179
+ expect(run.finishedAt).toBeNull();
180
+ expect(run.durationMs).toBeNull();
181
+ });
182
+
183
+ test("skipReason and error are always null (not derivable from conversation row)", async () => {
184
+ const conv = createConversation({
185
+ title: "c1",
186
+ source: "memory_v2_consolidation",
187
+ });
188
+ insertMessage(conv.id, "assistant", 2000);
189
+
190
+ const handler = findHandler("listConsolidationRuns");
191
+ const result = (await handler({})) as ListRunsResponse;
192
+
193
+ expect(result.runs[0]!.skipReason).toBeNull();
194
+ expect(result.runs[0]!.error).toBeNull();
195
+ });
196
+
197
+ test("orders runs by createdAt descending", async () => {
198
+ const a = createConversation({
199
+ title: "a",
200
+ source: "memory_v2_consolidation",
201
+ });
202
+ const b = createConversation({
203
+ title: "b",
204
+ source: "memory_v2_consolidation",
205
+ });
206
+ const c = createConversation({
207
+ title: "c",
208
+ source: "memory_v2_consolidation",
209
+ });
210
+ rawRun("UPDATE conversations SET created_at = ? WHERE id = ?", 1000, a.id);
211
+ rawRun("UPDATE conversations SET created_at = ? WHERE id = ?", 3000, b.id);
212
+ rawRun("UPDATE conversations SET created_at = ? WHERE id = ?", 2000, c.id);
213
+
214
+ const handler = findHandler("listConsolidationRuns");
215
+ const result = (await handler({})) as ListRunsResponse;
216
+
217
+ expect(result.runs.map((r) => r.id)).toEqual([b.id, c.id, a.id]);
218
+ });
219
+
220
+ test("limit defaults to 20, clamps to [1, 100], and falls back on non-numeric input", async () => {
221
+ for (let i = 0; i < 5; i++) {
222
+ createConversation({
223
+ title: `c${i}`,
224
+ source: "memory_v2_consolidation",
225
+ });
226
+ }
227
+
228
+ const handler = findHandler("listConsolidationRuns");
229
+
230
+ // Default — all 5 returned (under the 20 default).
231
+ const def = (await handler({})) as ListRunsResponse;
232
+ expect(def.runs).toHaveLength(5);
233
+
234
+ // Explicit limit honored.
235
+ const lim2 = (await handler({
236
+ queryParams: { limit: "2" },
237
+ })) as ListRunsResponse;
238
+ expect(lim2.runs).toHaveLength(2);
239
+
240
+ // Negative clamps to 1.
241
+ const neg = (await handler({
242
+ queryParams: { limit: "-5" },
243
+ })) as ListRunsResponse;
244
+ expect(neg.runs).toHaveLength(1);
245
+
246
+ // Zero clamps to 1.
247
+ const zero = (await handler({
248
+ queryParams: { limit: "0" },
249
+ })) as ListRunsResponse;
250
+ expect(zero.runs).toHaveLength(1);
251
+
252
+ // Non-numeric falls back to the default (20 → all 5 here).
253
+ const bad = (await handler({
254
+ queryParams: { limit: "garbage" },
255
+ })) as ListRunsResponse;
256
+ expect(bad.runs).toHaveLength(5);
257
+ });
258
+ });
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Unit tests for content-source-routes.ts.
3
+ *
4
+ * Drives the handler function directly (bypassing the router) and mocks
5
+ * out node:fs writes so no real I/O occurs.
6
+ */
7
+
8
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Module mocks — must appear before any imports of the module under test
12
+ // ---------------------------------------------------------------------------
13
+
14
+ mock.module("../../../util/logger.js", () => ({
15
+ getLogger: () =>
16
+ new Proxy({} as Record<string, unknown>, {
17
+ get: () => () => {},
18
+ }),
19
+ }));
20
+
21
+ const FAKE_WORKSPACE = "/tmp/content-source-routes-test-workspace";
22
+
23
+ mock.module("../../../util/platform.js", () => ({
24
+ getWorkspaceDir: () => FAKE_WORKSPACE,
25
+ }));
26
+
27
+ const writtenFiles = new Map<string, string>();
28
+
29
+ mock.module("node:fs", () => ({
30
+ mkdirSync: () => {},
31
+ writeFileSync: (path: string, content: string) => {
32
+ writtenFiles.set(path, content);
33
+ },
34
+ }));
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Imports after mocks
38
+ // ---------------------------------------------------------------------------
39
+
40
+ import { ROUTES } from "../content-source-routes.js";
41
+ import type { RouteDefinition, RouteHandlerArgs } from "../types.js";
42
+
43
+ afterAll(() => {
44
+ mock.restore();
45
+ });
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Helpers
49
+ // ---------------------------------------------------------------------------
50
+
51
+ function findHandler(operationId: string): RouteDefinition["handler"] {
52
+ const route = ROUTES.find((r) => r.operationId === operationId);
53
+ if (!route) throw new Error(`Route ${operationId} not found`);
54
+ return route.handler;
55
+ }
56
+
57
+ function makeArgs(body?: Record<string, unknown>): RouteHandlerArgs {
58
+ return { body };
59
+ }
60
+
61
+ const handler = findHandler("content_source_set");
62
+
63
+ beforeEach(() => {
64
+ writtenFiles.clear();
65
+ });
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // URL validation
69
+ // ---------------------------------------------------------------------------
70
+
71
+ describe("content_source_set — URL validation", () => {
72
+ test("https URL is accepted and sidecar written", () => {
73
+ const result = handler(makeArgs({ url: "https://myblog.com/posts" }));
74
+ expect(result).toEqual({ ok: true });
75
+
76
+ const expectedPath = `${FAKE_WORKSPACE}/data/content-source.json`;
77
+ expect(writtenFiles.has(expectedPath)).toBe(true);
78
+ const written = JSON.parse(writtenFiles.get(expectedPath)!);
79
+ expect(written.url).toBe("https://myblog.com/posts");
80
+ });
81
+
82
+ test("http URL is accepted and sidecar written", () => {
83
+ const result = handler(makeArgs({ url: "http://intranet.example.com" }));
84
+ expect(result).toEqual({ ok: true });
85
+ });
86
+
87
+ test("URL with leading/trailing whitespace is trimmed", () => {
88
+ const result = handler(makeArgs({ url: " https://blog.example.com " }));
89
+ expect(result).toEqual({ ok: true });
90
+
91
+ const expectedPath = `${FAKE_WORKSPACE}/data/content-source.json`;
92
+ const written = JSON.parse(writtenFiles.get(expectedPath)!);
93
+ expect(written.url).toBe("https://blog.example.com/");
94
+ });
95
+
96
+ test("bare hostname without protocol returns invalid_url", () => {
97
+ const result = handler(makeArgs({ url: "myblog.com" }));
98
+ expect(result).toEqual({ ok: false, error: "invalid_url" });
99
+ expect(writtenFiles.size).toBe(0);
100
+ });
101
+
102
+ test("ftp:// URL is rejected", () => {
103
+ const result = handler(makeArgs({ url: "ftp://files.example.com" }));
104
+ expect(result).toEqual({ ok: false, error: "invalid_url" });
105
+ expect(writtenFiles.size).toBe(0);
106
+ });
107
+
108
+ test("empty string returns invalid_url", () => {
109
+ const result = handler(makeArgs({ url: "" }));
110
+ expect(result).toEqual({ ok: false, error: "invalid_url" });
111
+ expect(writtenFiles.size).toBe(0);
112
+ });
113
+
114
+ test("whitespace-only string returns invalid_url", () => {
115
+ const result = handler(makeArgs({ url: " " }));
116
+ expect(result).toEqual({ ok: false, error: "invalid_url" });
117
+ expect(writtenFiles.size).toBe(0);
118
+ });
119
+
120
+ test("javascript: URL is rejected", () => {
121
+ const result = handler(makeArgs({ url: "javascript:alert(1)" }));
122
+ expect(result).toEqual({ ok: false, error: "invalid_url" });
123
+ expect(writtenFiles.size).toBe(0);
124
+ });
125
+
126
+ test("missing url field returns invalid_url", () => {
127
+ const result = handler(makeArgs({}));
128
+ expect(result).toEqual({ ok: false, error: "invalid_url" });
129
+ });
130
+ });
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Sidecar content verification
134
+ // ---------------------------------------------------------------------------
135
+
136
+ describe("content_source_set — sidecar contents", () => {
137
+ test("writes url to data/content-source.json", () => {
138
+ handler(makeArgs({ url: "https://example.com/blog" }));
139
+
140
+ const expectedPath = `${FAKE_WORKSPACE}/data/content-source.json`;
141
+ expect(writtenFiles.has(expectedPath)).toBe(true);
142
+
143
+ const written = JSON.parse(writtenFiles.get(expectedPath)!);
144
+ expect(Object.keys(written)).toEqual(["url"]);
145
+ });
146
+
147
+ test("no sidecar written on invalid URL", () => {
148
+ handler(makeArgs({ url: "not-a-url" }));
149
+ expect(writtenFiles.size).toBe(0);
150
+ });
151
+ });
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // Policy key verification
155
+ // ---------------------------------------------------------------------------
156
+
157
+ describe("route policy key", () => {
158
+ test("content_source_set uses policyKey: secrets", () => {
159
+ const route = ROUTES.find((r) => r.operationId === "content_source_set");
160
+ expect(route?.policyKey).toBe("secrets");
161
+ });
162
+ });
@@ -71,6 +71,10 @@ import {
71
71
  memoryV2ActivationLogs,
72
72
  messages,
73
73
  } from "../../../memory/schema.js";
74
+ import {
75
+ createConnection,
76
+ getConnection,
77
+ } from "../../../providers/inference/connections.js";
74
78
  import { ROUTES } from "../conversation-query-routes.js";
75
79
 
76
80
  // Local subset: this test only exercises a single concept row.
@@ -137,7 +141,11 @@ function seedConversationAndMessage(args: {
137
141
  .run();
138
142
  }
139
143
 
140
- function seedRequestLog(messageId: string, id: string): void {
144
+ function seedRequestLog(
145
+ messageId: string,
146
+ id: string,
147
+ options: { agentLoopExitReason?: string | null } = {},
148
+ ): void {
141
149
  getDb()
142
150
  .insert(llmRequestLogs)
143
151
  .values({
@@ -150,6 +158,9 @@ function seedRequestLog(messageId: string, id: string): void {
150
158
  choices: [{ message: { content: "hi" } }],
151
159
  }),
152
160
  createdAt: 1_700_000_000_000,
161
+ ...(options.agentLoopExitReason != null
162
+ ? { agentLoopExitReason: options.agentLoopExitReason }
163
+ : {}),
153
164
  })
154
165
  .run();
155
166
  }
@@ -313,6 +324,53 @@ describe("GET /v1/messages/:id/llm-context — conversationTotalEstimatedCostUsd
313
324
  });
314
325
  });
315
326
 
327
+ describe("GET /v1/messages/:id/llm-context — agentLoopExitReason", () => {
328
+ beforeEach(() => {
329
+ clearTables();
330
+ });
331
+
332
+ test("surfaces the stamped agent_loop_exit_reason on the terminal log", async () => {
333
+ const messageId = "msg-with-exit";
334
+ seedConversationAndMessage({
335
+ conversationId: "conv-1",
336
+ messageId,
337
+ source: "user",
338
+ conversationType: "standard",
339
+ });
340
+ // Two logs in the same turn — only the terminal one is stamped.
341
+ seedRequestLog(messageId, "log-non-terminal");
342
+ seedRequestLog(messageId, "log-terminal", {
343
+ agentLoopExitReason: "no_tool_calls",
344
+ });
345
+
346
+ const body = (await dispatchLlmContext(messageId)) as {
347
+ logs: Array<{ id: string; agentLoopExitReason: string | null }>;
348
+ };
349
+
350
+ const byId = new Map(body.logs.map((l) => [l.id, l.agentLoopExitReason]));
351
+ expect(byId.get("log-non-terminal")).toBeNull();
352
+ expect(byId.get("log-terminal")).toBe("no_tool_calls");
353
+ });
354
+
355
+ test("returns null when no log in the turn has been stamped", async () => {
356
+ const messageId = "msg-no-exit";
357
+ seedConversationAndMessage({
358
+ conversationId: "conv-1",
359
+ messageId,
360
+ source: "user",
361
+ conversationType: "standard",
362
+ });
363
+ seedRequestLog(messageId, "log-unstamped");
364
+
365
+ const body = (await dispatchLlmContext(messageId)) as {
366
+ logs: Array<{ id: string; agentLoopExitReason: string | null }>;
367
+ };
368
+
369
+ expect(body.logs).toHaveLength(1);
370
+ expect(body.logs[0]!.agentLoopExitReason).toBeNull();
371
+ });
372
+ });
373
+
316
374
  describe("PUT /v1/config/llm/profiles/:name", () => {
317
375
  beforeEach(() => {
318
376
  savedRawConfig = null;
@@ -427,7 +485,7 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
427
485
  expect(savedProfile.provider_connection).toBe("personal-openai");
428
486
  });
429
487
 
430
- test("clears provider_connection when omitted from body (UI-owned key)", async () => {
488
+ test("auto-derives provider_connection when omitted from body (Any active)", async () => {
431
489
  // Seed an existing binding so the test starts from a non-empty state.
432
490
  (
433
491
  rawConfigFixture.llm as {
@@ -441,8 +499,37 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
441
499
  provider: "openai",
442
500
  model: "gpt-5.5",
443
501
  // provider_connection deliberately omitted — the UI cleared the
444
- // picker back to "Any active" and the route must wipe the saved
445
- // binding, not silently round-trip it.
502
+ // picker back to "Any active". The route auto-derives an active
503
+ // connection for the provider to prevent stale inheritance during
504
+ // config deep-merge.
505
+ },
506
+ });
507
+
508
+ expect(result).toEqual({ ok: true });
509
+ const savedProfile = (
510
+ savedRawConfig?.llm as {
511
+ profiles: Record<string, Record<string, unknown>>;
512
+ }
513
+ ).profiles.custom;
514
+
515
+ // The canonical "openai-managed" connection exists in the test DB;
516
+ // the route auto-derives it when the UI omits provider_connection.
517
+ expect(savedProfile.provider_connection).toBe("openai-managed");
518
+ });
519
+
520
+ test("auto-derives provider_connection for BYOK provider (Any active)", async () => {
521
+ // Seed a fireworks connection in the DB.
522
+ createConnection(getDb(), {
523
+ name: "fireworks",
524
+ provider: "fireworks",
525
+ auth: { type: "api_key", credential: "fireworks:api_key" },
526
+ });
527
+
528
+ const result = await replaceProfileRoute.handler({
529
+ pathParams: { name: "custom" },
530
+ body: {
531
+ provider: "fireworks",
532
+ model: "accounts/fireworks/models/llama-v3p1-8b-instruct",
446
533
  },
447
534
  });
448
535
 
@@ -453,7 +540,36 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
453
540
  }
454
541
  ).profiles.custom;
455
542
 
456
- expect(savedProfile.provider_connection).toBeUndefined();
543
+ expect(savedProfile.provider).toBe("fireworks");
544
+ expect(savedProfile.provider_connection).toBe("fireworks");
545
+ });
546
+
547
+ test("auto-creates provider_connection when no connection exists for provider", async () => {
548
+ const result = await replaceProfileRoute.handler({
549
+ pathParams: { name: "custom" },
550
+ body: {
551
+ provider: "openrouter",
552
+ model: "anthropic/claude-sonnet-4-6",
553
+ },
554
+ });
555
+
556
+ expect(result).toEqual({ ok: true });
557
+ const savedProfile = (
558
+ savedRawConfig?.llm as {
559
+ profiles: Record<string, Record<string, unknown>>;
560
+ }
561
+ ).profiles.custom;
562
+
563
+ expect(savedProfile.provider).toBe("openrouter");
564
+ expect(savedProfile.provider_connection).toBe("openrouter-personal");
565
+
566
+ const conn = getConnection(getDb(), "openrouter-personal");
567
+ expect(conn).not.toBeNull();
568
+ expect(conn!.provider).toBe("openrouter");
569
+ expect(conn!.auth).toEqual({
570
+ type: "api_key",
571
+ credential: "credential/openrouter/api_key",
572
+ });
457
573
  });
458
574
 
459
575
  describe("managed profile guard", () => {