@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
@@ -28,8 +28,14 @@ import {
28
28
  getConnection,
29
29
  listConnections,
30
30
  MANAGED_CONNECTION_NAMES,
31
+ PROVIDERS_REQUIRING_BASE_URL_AND_MODELS,
31
32
  updateConnection,
32
33
  } from "../../providers/inference/connections.js";
34
+ import {
35
+ isPrivateOrLocalHost,
36
+ resolveHostAddresses,
37
+ resolveRequestAddress,
38
+ } from "../../tools/network/url-safety.js";
33
39
  import { BadRequestError, ConflictError, NotFoundError } from "./errors.js";
34
40
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
35
41
 
@@ -59,10 +65,26 @@ function rejectDisabledOpenAICompatibleProvider(provider: string): void {
59
65
  // Custom provider field parsing (openai-compatible base_url + models)
60
66
  // ---------------------------------------------------------------------------
61
67
 
62
- function parseCustomProviderFields(body: Record<string, unknown>): {
68
+ /**
69
+ * Parse and validate `base_url` and `models` from the request body.
70
+ *
71
+ * `base_url` is only accepted for providers in
72
+ * `PROVIDERS_REQUIRING_BASE_URL_AND_MODELS` (currently `openai-compatible`).
73
+ * For all other providers, supplying `base_url` returns a 400. This prevents
74
+ * API-key exfiltration: an attacker cannot create an `anthropic` connection
75
+ * with a `base_url` pointing to their own server, which would redirect all
76
+ * LLM calls (and the API key) to the attacker.
77
+ *
78
+ * Even for `openai-compatible`, the `base_url` must not point to private
79
+ * networks or cloud metadata endpoints (SSRF protection).
80
+ */
81
+ async function parseCustomProviderFields(
82
+ body: Record<string, unknown>,
83
+ provider: string,
84
+ ): Promise<{
63
85
  baseUrl?: string | null;
64
86
  models?: ConnectionModel[] | null;
65
- } {
87
+ }> {
66
88
  const out: {
67
89
  baseUrl?: string | null;
68
90
  models?: ConnectionModel[] | null;
@@ -70,11 +92,24 @@ function parseCustomProviderFields(body: Record<string, unknown>): {
70
92
 
71
93
  if ("base_url" in body) {
72
94
  const raw = body.base_url;
95
+
96
+ // Gate: base_url is only valid for openai-compatible providers.
97
+ if (
98
+ raw !== null &&
99
+ raw !== undefined &&
100
+ !PROVIDERS_REQUIRING_BASE_URL_AND_MODELS.has(provider)
101
+ ) {
102
+ throw new BadRequestError(
103
+ `base_url is only valid for openai-compatible providers. Remove base_url or use the openai-compatible provider type.`,
104
+ );
105
+ }
106
+
73
107
  if (raw === null) {
74
108
  out.baseUrl = null;
75
109
  } else if (typeof raw === "string" && raw.length > 0) {
110
+ let parsed: URL;
76
111
  try {
77
- const parsed = new URL(raw);
112
+ parsed = new URL(raw);
78
113
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
79
114
  throw new BadRequestError(`Invalid base_url: must be an http(s) URL`);
80
115
  }
@@ -84,6 +119,27 @@ function parseCustomProviderFields(body: Record<string, unknown>): {
84
119
  `Invalid base_url: must be a valid http(s) URL`,
85
120
  );
86
121
  }
122
+
123
+ // SSRF protection: reject private IPs, localhost, cloud metadata endpoints.
124
+ const hostname = parsed.hostname;
125
+ if (isPrivateOrLocalHost(hostname)) {
126
+ throw new BadRequestError(
127
+ `Invalid base_url: must not point to a private or local network address.`,
128
+ );
129
+ }
130
+
131
+ // DNS resolution check: hostname may resolve to a private IP.
132
+ const resolved = await resolveRequestAddress(
133
+ hostname,
134
+ resolveHostAddresses,
135
+ /* allowPrivateNetwork */ false,
136
+ );
137
+ if (resolved.blockedAddress) {
138
+ throw new BadRequestError(
139
+ `Invalid base_url: hostname resolves to a private network address.`,
140
+ );
141
+ }
142
+
87
143
  out.baseUrl = raw;
88
144
  } else {
89
145
  throw new BadRequestError(
@@ -145,7 +201,7 @@ function handleGetConnection({ pathParams = {} }: RouteHandlerArgs) {
145
201
  return conn;
146
202
  }
147
203
 
148
- function handleCreateConnection({ body = {} }: RouteHandlerArgs) {
204
+ async function handleCreateConnection({ body = {} }: RouteHandlerArgs) {
149
205
  const name = body.name;
150
206
  const provider = body.provider;
151
207
  const auth = body.auth;
@@ -186,7 +242,7 @@ function handleCreateConnection({ body = {} }: RouteHandlerArgs) {
186
242
  );
187
243
  }
188
244
 
189
- const customFields = parseCustomProviderFields(body);
245
+ const customFields = await parseCustomProviderFields(body, providerResult.data);
190
246
 
191
247
  const result = createConnection(getDb(), {
192
248
  name,
@@ -224,7 +280,7 @@ function handleCreateConnection({ body = {} }: RouteHandlerArgs) {
224
280
  return result.connection;
225
281
  }
226
282
 
227
- function handleUpdateConnection({
283
+ async function handleUpdateConnection({
228
284
  pathParams = {},
229
285
  body = {},
230
286
  }: RouteHandlerArgs) {
@@ -278,7 +334,7 @@ function handleUpdateConnection({
278
334
  );
279
335
  }
280
336
 
281
- const customFields = parseCustomProviderFields(body);
337
+ const customFields = await parseCustomProviderFields(body, existing.provider);
282
338
 
283
339
  const result = updateConnection(getDb(), name, {
284
340
  auth: authResult.data,
@@ -7,11 +7,13 @@
7
7
  * POST /v1/integrations/a2a/invite — create a shareable A2A invite token
8
8
  * POST /v1/integrations/a2a/invite/complete — sender-side invite completion
9
9
  * POST /v1/integrations/a2a/invite/redeem — receiver-side invite redemption
10
+ * POST /v1/integrations/a2a/invite/accept — self-hosted broker: orchestrate complete + redeem
10
11
  */
11
12
 
12
13
  import { isA2AEnabled } from "../../../a2a/feature-gate.js";
13
14
  import { getConfig } from "../../../config/loader.js";
14
15
  import {
16
+ acceptA2AInvite,
15
17
  clearA2AConfig,
16
18
  completeA2AInvite,
17
19
  createA2AInvite,
@@ -19,7 +21,7 @@ import {
19
21
  redeemA2AInvite,
20
22
  setA2AConfig,
21
23
  } from "../../../daemon/handlers/config-a2a.js";
22
- import { BadRequestError } from "../errors.js";
24
+ import { BadGatewayError, BadRequestError } from "../errors.js";
23
25
  import type { RouteDefinition, RouteHandlerArgs } from "../types.js";
24
26
 
25
27
  // ---------------------------------------------------------------------------
@@ -164,6 +166,52 @@ function handleRedeemA2AInvite({ body = {} }: RouteHandlerArgs) {
164
166
  return result;
165
167
  }
166
168
 
169
+ async function handleAcceptA2AInvite({ body = {} }: RouteHandlerArgs) {
170
+ assertA2AFlag();
171
+ const { senderGatewayUrl, senderAssistantId, token } = body as {
172
+ senderGatewayUrl?: unknown;
173
+ senderAssistantId?: unknown;
174
+ token?: unknown;
175
+ };
176
+
177
+ if (typeof senderGatewayUrl !== "string" || !senderGatewayUrl) {
178
+ throw new BadRequestError(
179
+ "senderGatewayUrl is required and must be a non-empty string",
180
+ );
181
+ }
182
+ try {
183
+ new URL(senderGatewayUrl);
184
+ } catch {
185
+ throw new BadRequestError("senderGatewayUrl must be a valid URL");
186
+ }
187
+ if (typeof senderAssistantId !== "string" || !senderAssistantId) {
188
+ throw new BadRequestError(
189
+ "senderAssistantId is required and must be a non-empty string",
190
+ );
191
+ }
192
+ if (typeof token !== "string" || !token) {
193
+ throw new BadRequestError(
194
+ "token is required and must be a non-empty string",
195
+ );
196
+ }
197
+
198
+ const result = await acceptA2AInvite({
199
+ senderGatewayUrl,
200
+ senderAssistantId,
201
+ token,
202
+ });
203
+ if (!result.success) {
204
+ const isSenderFault =
205
+ result.errorCode === "sender_unreachable" ||
206
+ result.errorCode === "complete_failed";
207
+ if (isSenderFault) {
208
+ throw new BadGatewayError(result.error ?? "Failed to accept A2A invite");
209
+ }
210
+ throw new BadRequestError(result.error ?? "Failed to accept A2A invite");
211
+ }
212
+ return result;
213
+ }
214
+
167
215
  // ---------------------------------------------------------------------------
168
216
  // Route definitions
169
217
  // ---------------------------------------------------------------------------
@@ -232,4 +280,15 @@ export const ROUTES: RouteDefinition[] = [
232
280
  requirePolicyEnforcement: true,
233
281
  handler: handleRedeemA2AInvite,
234
282
  },
283
+ {
284
+ operationId: "integrations_a2a_invite_accept_post",
285
+ endpoint: "integrations/a2a/invite/accept",
286
+ method: "POST",
287
+ summary: "Accept A2A invite (self-hosted broker)",
288
+ description:
289
+ "Orchestrate cross-daemon invite acceptance for self-hosted deployments. Calls the sender's invite/complete, then creates a local contact via invite/redeem.",
290
+ tags: ["integrations"],
291
+ requirePolicyEnforcement: true,
292
+ handler: handleAcceptA2AInvite,
293
+ },
235
294
  ];
@@ -36,6 +36,7 @@ import {
36
36
  getWorkspaceConfigPath,
37
37
  } from "../../util/platform.js";
38
38
  import { APP_VERSION, COMMIT_SHA } from "../../version.js";
39
+ import { assistantEventHub } from "../assistant-event-hub.js";
39
40
  import { createTarGz } from "./archive-utils.js";
40
41
  import { InternalError } from "./errors.js";
41
42
  import { collectWorkspaceData } from "./log-export/workspace-allowlist.js";
@@ -270,6 +271,44 @@ async function handleExport({
270
271
  );
271
272
  }
272
273
 
274
+ // --- Connected clients (`assistant clients list --json`) ---
275
+ try {
276
+ const clients = assistantEventHub.listClients();
277
+ const clientsList = {
278
+ clients: clients.map((c) => ({
279
+ clientId: c.clientId,
280
+ interfaceId: c.interfaceId,
281
+ capabilities: c.capabilities,
282
+ machineName: c.machineName,
283
+ connectedAt: c.connectedAt.toISOString(),
284
+ lastActiveAt: c.lastActiveAt.toISOString(),
285
+ })),
286
+ };
287
+ writeFileSync(
288
+ join(staging, "clients-list.json"),
289
+ JSON.stringify(clientsList, null, 2),
290
+ "utf-8",
291
+ );
292
+ } catch (err) {
293
+ const message = err instanceof Error ? err.message : String(err);
294
+ writeFileSync(
295
+ join(staging, "clients-list-error.json"),
296
+ JSON.stringify(
297
+ {
298
+ error: message,
299
+ collectedAt: new Date().toISOString(),
300
+ },
301
+ null,
302
+ 2,
303
+ ),
304
+ "utf-8",
305
+ );
306
+ log.warn(
307
+ { err },
308
+ "Failed to collect connected clients list, continuing without it",
309
+ );
310
+ }
311
+
273
312
  // --- Export manifest ---
274
313
  const manifestType = conversationId
275
314
  ? ("conversation-export" as const)
@@ -8,6 +8,8 @@ import { join } from "node:path";
8
8
  import { z } from "zod";
9
9
 
10
10
  import { loadConfig } from "../../config/loader.js";
11
+ import type { AssistantConfig } from "../../config/types.js";
12
+ import { getDb } from "../../memory/db-connection.js";
11
13
  import {
12
14
  enqueueMemoryJob,
13
15
  type MemoryJobType,
@@ -21,12 +23,16 @@ import {
21
23
  totalEdgeCount,
22
24
  validateEdgeTargets,
23
25
  } from "../../memory/v2/edge-index.js";
26
+ import { computeInjectionScores } from "../../memory/v2/injection-events.js";
27
+ import { loadNowText } from "../../memory/v2/now-text.js";
28
+ import { getPageIndex } from "../../memory/v2/page-index.js";
24
29
  import {
25
30
  getConceptsDir,
26
31
  listPages,
27
32
  readPage,
28
33
  renderPageContent,
29
34
  } from "../../memory/v2/page-store.js";
35
+ import { type RouterSource, runRouter } from "../../memory/v2/router.js";
30
36
  import { seedV2SkillEntries } from "../../memory/v2/skill-store.js";
31
37
  import { getLogger } from "../../util/logger.js";
32
38
  import { getWorkspaceDir } from "../../util/platform.js";
@@ -290,6 +296,195 @@ async function handleConceptFrequency({
290
296
  return getConceptFrequencySummary(workspaceDir, { conversationId, sinceMs });
291
297
  }
292
298
 
299
+ // ── EMA scores ──────────────────────────────────────────────────────────
300
+
301
+ const MemoryV2EmaScoresParams = z.object({}).strict();
302
+
303
+ export interface MemoryV2EmaScoresEntry {
304
+ slug: string;
305
+ /** Time-decayed injection frequency; 0 when no events in the read window. */
306
+ score: number;
307
+ /** File mtime in epoch ms; 0 for synthetic entries (skills, CLI commands). */
308
+ modifiedAt: number;
309
+ }
310
+
311
+ export interface MemoryV2EmaScoresResult {
312
+ /** Every page index entry, sorted by score descending then slug ASCII. */
313
+ entries: MemoryV2EmaScoresEntry[];
314
+ }
315
+
316
+ async function handleEmaScores({
317
+ body = {},
318
+ }: RouteHandlerArgs): Promise<MemoryV2EmaScoresResult> {
319
+ // Intentionally NOT gated on `memory.v2.enabled` — operators inspecting
320
+ // EMA data before flipping tier-2 routing on is a legitimate dry-run
321
+ // use case, mirroring `memory_v2_validate`.
322
+ MemoryV2EmaScoresParams.parse(body);
323
+
324
+ const pageIndex = await getPageIndex(getWorkspaceDir());
325
+ const slugs = pageIndex.entries.map((e) => e.slug);
326
+ const scores = computeInjectionScores(getDb(), slugs, Date.now());
327
+
328
+ const entries: MemoryV2EmaScoresEntry[] = pageIndex.entries.map((entry) => ({
329
+ slug: entry.slug,
330
+ score: scores.get(entry.slug) ?? 0,
331
+ modifiedAt: entry.modifiedAt,
332
+ }));
333
+ entries.sort((a, b) => {
334
+ if (a.score !== b.score) return b.score - a.score;
335
+ return a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0;
336
+ });
337
+ return { entries };
338
+ }
339
+
340
+ // ── Simulate router (dry-run playground) ────────────────────────────────
341
+
342
+ const SimulateRouterOverridesSchema = z
343
+ .object({
344
+ tier1_size: z.number().int().min(1).nullable().optional(),
345
+ tier2_size: z.number().int().min(1).nullable().optional(),
346
+ batch_size: z.number().int().min(1).nullable().optional(),
347
+ })
348
+ .strict();
349
+
350
+ const MemoryV2SimulateRouterParams = z
351
+ .object({
352
+ query: z.string().min(1, "query must be non-empty"),
353
+ configOverrides: SimulateRouterOverridesSchema.optional(),
354
+ })
355
+ .strict();
356
+
357
+ export interface MemoryV2SimulateRouterEffectiveConfig {
358
+ tier1_size: number | null;
359
+ tier2_size: number | null;
360
+ batch_size: number | null;
361
+ max_page_ids: number;
362
+ }
363
+
364
+ export interface MemoryV2SimulateRouterResult {
365
+ /** Slugs the router would select, in model-returned order. */
366
+ selectedSlugs: string[];
367
+ /** Per-slug provenance: `"tier1"`, `"tier2"`, or `"tier3:<bucket>"`. */
368
+ sourceBySlug: Record<string, RouterSource>;
369
+ /** EMA scores for the selected slugs (0 when the slug has no events). */
370
+ scores: Record<string, number>;
371
+ /** `null` on success; otherwise one of the router failure reasons. */
372
+ failureReason: string | null;
373
+ /** The router config that actually ran (live merged with overrides). */
374
+ effectiveConfig: MemoryV2SimulateRouterEffectiveConfig;
375
+ /** The overrides the caller submitted, for display. */
376
+ overrides: {
377
+ tier1_size?: number | null;
378
+ tier2_size?: number | null;
379
+ batch_size?: number | null;
380
+ };
381
+ /** Page index size the router was given (post-tier-carve, all batches). */
382
+ totalCandidatePages: number;
383
+ }
384
+
385
+ /**
386
+ * Build the config the router will see by overlaying override values on top
387
+ * of the live workspace config. Only the three new tier knobs are exposed —
388
+ * everything else (provider, prompts, weights) stays exactly as it would on
389
+ * a real turn. `undefined` means "inherit live"; `null` is a valid override
390
+ * value (meaning "disable this tier").
391
+ */
392
+ function applySimulateOverrides(
393
+ live: AssistantConfig,
394
+ overrides: z.infer<typeof SimulateRouterOverridesSchema> | undefined,
395
+ ): AssistantConfig {
396
+ if (!overrides) return live;
397
+ const liveRouter = live.memory.v2.router;
398
+ const mergedRouter = {
399
+ ...liveRouter,
400
+ ...("tier1_size" in overrides && overrides.tier1_size !== undefined
401
+ ? { tier1_size: overrides.tier1_size }
402
+ : {}),
403
+ ...("tier2_size" in overrides && overrides.tier2_size !== undefined
404
+ ? { tier2_size: overrides.tier2_size }
405
+ : {}),
406
+ ...("batch_size" in overrides && overrides.batch_size !== undefined
407
+ ? { batch_size: overrides.batch_size }
408
+ : {}),
409
+ };
410
+ return {
411
+ ...live,
412
+ memory: {
413
+ ...live.memory,
414
+ v2: {
415
+ ...live.memory.v2,
416
+ router: mergedRouter,
417
+ },
418
+ },
419
+ };
420
+ }
421
+
422
+ export async function handleSimulateRouter({
423
+ body = {},
424
+ }: RouteHandlerArgs): Promise<MemoryV2SimulateRouterResult> {
425
+ requireMemoryV2Enabled();
426
+ const { query, configOverrides } = MemoryV2SimulateRouterParams.parse(body);
427
+
428
+ const liveConfig = loadConfig();
429
+ const mergedConfig = applySimulateOverrides(liveConfig, configOverrides);
430
+ const effectiveRouter = mergedConfig.memory.v2.router;
431
+
432
+ const workspaceDir = getWorkspaceDir();
433
+ const nowText = await loadNowText(workspaceDir);
434
+
435
+ const routerResult = await runRouter({
436
+ workspaceDir,
437
+ userMessage: query,
438
+ assistantMessage: "",
439
+ nowText,
440
+ priorEverInjected: [],
441
+ config: mergedConfig,
442
+ database: getDb(),
443
+ });
444
+
445
+ const pageIndex = await getPageIndex(workspaceDir);
446
+ const scores = computeInjectionScores(
447
+ getDb(),
448
+ routerResult.selectedSlugs,
449
+ Date.now(),
450
+ );
451
+
452
+ const sourceBySlug: Record<string, RouterSource> = {};
453
+ for (const [slug, source] of routerResult.sourceBySlug.entries()) {
454
+ sourceBySlug[slug] = source;
455
+ }
456
+
457
+ const scoresOut: Record<string, number> = {};
458
+ for (const slug of routerResult.selectedSlugs) {
459
+ scoresOut[slug] = scores.get(slug) ?? 0;
460
+ }
461
+
462
+ return {
463
+ selectedSlugs: routerResult.selectedSlugs,
464
+ sourceBySlug,
465
+ scores: scoresOut,
466
+ failureReason: routerResult.failureReason,
467
+ effectiveConfig: {
468
+ tier1_size: effectiveRouter.tier1_size,
469
+ tier2_size: effectiveRouter.tier2_size,
470
+ batch_size: effectiveRouter.batch_size,
471
+ max_page_ids: effectiveRouter.max_page_ids,
472
+ },
473
+ overrides: {
474
+ ...(configOverrides?.tier1_size !== undefined
475
+ ? { tier1_size: configOverrides.tier1_size }
476
+ : {}),
477
+ ...(configOverrides?.tier2_size !== undefined
478
+ ? { tier2_size: configOverrides.tier2_size }
479
+ : {}),
480
+ ...(configOverrides?.batch_size !== undefined
481
+ ? { batch_size: configOverrides.batch_size }
482
+ : {}),
483
+ },
484
+ totalCandidatePages: pageIndex.entries.length,
485
+ };
486
+ }
487
+
293
488
  // ── Route definitions ───────────────────────────────────────────────────
294
489
 
295
490
  export const ROUTES: RouteDefinition[] = [
@@ -359,4 +554,26 @@ export const ROUTES: RouteDefinition[] = [
359
554
  tags: ["memory"],
360
555
  requestBody: MemoryV2ConceptFrequencyParams,
361
556
  },
557
+ {
558
+ operationId: "memory_v2_ema_scores",
559
+ method: "POST",
560
+ endpoint: "memory/v2/ema-scores",
561
+ handler: handleEmaScores,
562
+ summary: "List every concept page with its injection-frequency EMA score",
563
+ description:
564
+ "Computes the time-decayed injection frequency (3-day half-life) for every entry in the current page index by reading memory_v2_injection_events. Returns entries sorted by score descending then slug ASCII, including zero-score pages so callers can decide whether to filter. Read-only; tier 2 of the v4 router uses the same computation to pick its top-M.",
565
+ tags: ["memory"],
566
+ requestBody: MemoryV2EmaScoresParams,
567
+ },
568
+ {
569
+ operationId: "memory_v2_simulate_router",
570
+ method: "POST",
571
+ endpoint: "memory/v2/simulate-router",
572
+ handler: handleSimulateRouter,
573
+ summary: "Dry-run the v4 router with config overrides (read-only)",
574
+ description:
575
+ "Runs the memory router against the live page index + EMA scores with optional tier_size / batch_size overrides, without recording an injection event or writing an activation log. Returns the slugs that would have been selected, per-slug tier provenance, EMA scores, and the effective router config so operators can validate knob changes before flipping them in workspace config.",
576
+ tags: ["memory"],
577
+ requestBody: MemoryV2SimulateRouterParams,
578
+ },
362
579
  ];
@@ -7,6 +7,7 @@ import { z } from "zod";
7
7
 
8
8
  import { getDb } from "../../memory/db-connection.js";
9
9
  import { notificationDeliveries } from "../../memory/schema.js";
10
+ import { bufferIfDeferred } from "../../notifications/deferred-emit.js";
10
11
  import { emitNotificationSignal } from "../../notifications/emit-signal.js";
11
12
  import { listEvents } from "../../notifications/events-store.js";
12
13
  import type { AttentionHints } from "../../notifications/signal.js";
@@ -78,6 +79,9 @@ const EmitSignalParams = z.object({
78
79
  conversationAffinityHint: z.record(z.string(), z.string()).optional(),
79
80
  dedupeKey: z.string().optional(),
80
81
  throwOnError: z.boolean().optional(),
82
+ // Conversation that originated this signal — used by `deferred-emit` to
83
+ // buffer notifications during in-band background-job tool calls.
84
+ originatingConversationId: z.string().optional(),
81
85
  });
82
86
 
83
87
  const ListNotificationEventsParams = z.object({
@@ -89,7 +93,7 @@ const ListNotificationEventsParams = z.object({
89
93
 
90
94
  async function handleEmitSignal({ body = {} }: RouteHandlerArgs) {
91
95
  const validated = EmitSignalParams.parse(body);
92
- const result = await emitNotificationSignal({
96
+ const params = {
93
97
  sourceEventName: validated.sourceEventName,
94
98
  sourceChannel: validated.sourceChannel,
95
99
  sourceContextId: validated.sourceContextId,
@@ -99,7 +103,20 @@ async function handleEmitSignal({ body = {} }: RouteHandlerArgs) {
99
103
  conversationAffinityHint: validated.conversationAffinityHint,
100
104
  dedupeKey: validated.dedupeKey,
101
105
  throwOnError: validated.throwOnError,
102
- });
106
+ };
107
+ const buffered = bufferIfDeferred(
108
+ validated.originatingConversationId,
109
+ params,
110
+ );
111
+ if (buffered) {
112
+ return {
113
+ signalId: buffered.signalId,
114
+ dispatched: buffered.dispatched,
115
+ deduplicated: buffered.deduplicated,
116
+ reason: buffered.reason,
117
+ };
118
+ }
119
+ const result = await emitNotificationSignal(params);
103
120
  return {
104
121
  signalId: result.signalId,
105
122
  dispatched: result.dispatched,
@@ -148,7 +148,10 @@ function handleQuestionResponse({ body }: RouteHandlerArgs) {
148
148
 
149
149
  // Validation passed — deregister now to clear the prompter timer, then
150
150
  // hand the result to the prompter's caller via rpcResolve.
151
- pendingInteractions.resolve(requestId);
151
+ pendingInteractions.resolve(
152
+ requestId,
153
+ response.kind === "close" ? "cancelled" : "answered",
154
+ );
152
155
 
153
156
  log.info(
154
157
  {