@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
@@ -94,6 +94,8 @@ mock.module("../pre-first-message-gate.js", () => ({
94
94
 
95
95
  // Import after mocks are in place.
96
96
  const { runBackgroundJob } = await import("../background-job-runner.js");
97
+ const { bufferIfDeferred, resetDeferredForTest } =
98
+ await import("../../notifications/deferred-emit.js");
97
99
 
98
100
  // ── Shared fixtures ──────────────────────────────────────────────────
99
101
 
@@ -121,6 +123,7 @@ beforeEach(() => {
121
123
  processMessageCalls.length = 0;
122
124
  emitCalls.length = 0;
123
125
  addMessageCalls.length = 0;
126
+ resetDeferredForTest();
124
127
  preFirstMessageGateOpen = true;
125
128
  processMessageImpl = async () => ({ messageId: "msg-1" });
126
129
  emitImpl = async () => ({
@@ -354,4 +357,129 @@ describe("runBackgroundJob", () => {
354
357
  expect(processMessageCalls).toHaveLength(1);
355
358
  });
356
359
  });
360
+
361
+ describe("deferNotifications", () => {
362
+ function buildSkillNotificationParams(message: string) {
363
+ return {
364
+ sourceEventName: "assistant.share",
365
+ sourceChannel: "assistant_tool" as const,
366
+ sourceContextId: "skill-ctx",
367
+ contextPayload: { requestedMessage: message },
368
+ attentionHints: {
369
+ requiresAction: false,
370
+ urgency: "low" as const,
371
+ isAsyncBackground: false,
372
+ visibleInSourceNow: false,
373
+ },
374
+ };
375
+ }
376
+
377
+ test("success path commits buffered in-band notifications", async () => {
378
+ processMessageImpl = async () => {
379
+ // Stand in for the IPC route's bufferIfDeferred call when the model
380
+ // invokes `notifications send` mid-turn.
381
+ const buffered = bufferIfDeferred(
382
+ STUB_CONVERSATION_ID,
383
+ buildSkillNotificationParams("all green"),
384
+ );
385
+ expect(buffered).not.toBeNull();
386
+ expect(buffered!.dispatched).toBe(false);
387
+ return { messageId: "msg-success" };
388
+ };
389
+
390
+ const result = await runBackgroundJob(
391
+ baseOpts({ deferNotifications: true }),
392
+ );
393
+
394
+ expect(result.ok).toBe(true);
395
+ // Commit flushed the buffered notification through emitNotificationSignal.
396
+ const successEmits = emitCalls.filter(
397
+ (e) => e.sourceEventName !== "activity.failed",
398
+ );
399
+ expect(successEmits).toHaveLength(1);
400
+ expect(successEmits[0].sourceEventName).toBe("assistant.share");
401
+ });
402
+
403
+ // Regression for the PR #31216 Codex P1 finding: a heartbeat that calls
404
+ // the notifications skill and then times out must not leave a "success"
405
+ // notification standing alongside the runner's `activity.failed` emit.
406
+ test("timeout drops buffered in-band notifications; only activity.failed emits", async () => {
407
+ processMessageImpl = () => {
408
+ bufferIfDeferred(
409
+ STUB_CONVERSATION_ID,
410
+ buildSkillNotificationParams("premature success"),
411
+ );
412
+ return new Promise(() => {});
413
+ };
414
+
415
+ const result = await runBackgroundJob(
416
+ baseOpts({ deferNotifications: true, timeoutMs: 30 }),
417
+ );
418
+
419
+ expect(result.ok).toBe(false);
420
+ expect(result.errorKind).toBe("timeout");
421
+ // Only the runner's failure signal makes it out — the buffered
422
+ // "success" notification is discarded.
423
+ expect(emitCalls).toHaveLength(1);
424
+ expect(emitCalls[0].sourceEventName).toBe("activity.failed");
425
+ });
426
+
427
+ test("thrown exception also drops buffered notifications", async () => {
428
+ processMessageImpl = async () => {
429
+ bufferIfDeferred(
430
+ STUB_CONVERSATION_ID,
431
+ buildSkillNotificationParams("doomed"),
432
+ );
433
+ throw new Error("kaboom");
434
+ };
435
+
436
+ const result = await runBackgroundJob(
437
+ baseOpts({ deferNotifications: true }),
438
+ );
439
+
440
+ expect(result.ok).toBe(false);
441
+ expect(emitCalls).toHaveLength(1);
442
+ expect(emitCalls[0].sourceEventName).toBe("activity.failed");
443
+ });
444
+
445
+ // Regression: after timeout, `processMessage` keeps running and may
446
+ // emit a late skill call. The tombstone must swallow it instead of
447
+ // letting it bypass the buffer and reach the dispatch pipeline.
448
+ test("late skill call after timeout is swallowed by the tombstone", async () => {
449
+ processMessageImpl = () => new Promise(() => {});
450
+
451
+ const result = await runBackgroundJob(
452
+ baseOpts({ deferNotifications: true, timeoutMs: 20 }),
453
+ );
454
+ expect(result.ok).toBe(false);
455
+
456
+ const late = bufferIfDeferred(
457
+ STUB_CONVERSATION_ID,
458
+ buildSkillNotificationParams("post-timeout"),
459
+ );
460
+ expect(late).not.toBeNull();
461
+ expect(late!.dispatched).toBe(false);
462
+ expect(late!.reason).toMatch(/did not complete/);
463
+
464
+ // Only the runner's failure signal made it out.
465
+ expect(emitCalls).toHaveLength(1);
466
+ expect(emitCalls[0].sourceEventName).toBe("activity.failed");
467
+ });
468
+
469
+ test("without deferNotifications, bufferIfDeferred is a no-op", async () => {
470
+ processMessageImpl = async () => {
471
+ const buffered = bufferIfDeferred(
472
+ STUB_CONVERSATION_ID,
473
+ buildSkillNotificationParams("immediate"),
474
+ );
475
+ // Buffer was never armed, so the call returns null and the IPC
476
+ // handler would emit directly.
477
+ expect(buffered).toBeNull();
478
+ return { messageId: "msg-success" };
479
+ };
480
+
481
+ const result = await runBackgroundJob(baseOpts());
482
+ expect(result.ok).toBe(true);
483
+ });
484
+ });
357
485
  });
@@ -68,9 +68,6 @@ import { getLogger } from "../util/logger.js";
68
68
 
69
69
  const log = getLogger("agent-wake");
70
70
 
71
- /** Number of messages injected for the wake hint (user + assistant + user). */
72
- const WAKE_HINT_MESSAGE_COUNT = 3;
73
-
74
71
  /** Static preamble user message — no dynamic content, injection-safe. */
75
72
  const WAKE_PREAMBLE =
76
73
  "[system] The following assistant message comes from an external system.";
@@ -196,6 +193,51 @@ export interface WakeOptions {
196
193
  * tune the model/profile and observability bucket independently.
197
194
  */
198
195
  callSite?: LLMCallSite;
196
+ /**
197
+ * Role to use for the injected hint message. Defaults to `"assistant"` so
198
+ * the hint is sandwiched between two static user bookends — the canonical
199
+ * anti-injection pattern for hints that may carry text from an external
200
+ * source. Trusted internal callers (e.g. fork-based memory retrospectives)
201
+ * can pass `"user"` to inject a single user-role message containing the
202
+ * hint directly, which reads more naturally as an instruction from the
203
+ * user/system rather than a self-directed assistant note.
204
+ */
205
+ hintRole?: "assistant" | "user";
206
+ /**
207
+ * Documented intent: this wake must not trigger auto-threshold compaction.
208
+ *
209
+ * Today this is automatically satisfied because the wake invokes
210
+ * `target.agentLoop.run()` directly, bypassing the daemon orchestrator
211
+ * (`conversation-agent-loop.ts`) where the compaction pipeline lives. The
212
+ * flag is recorded in the wake's structured log line so operators can
213
+ * verify the contract holds across refactors. If compaction is ever moved
214
+ * into `AgentLoop.run` or invoked from the wake path, callers that pass
215
+ * `true` here MUST be updated to suppress it; callers that pass `false`
216
+ * (or omit it) MUST tolerate compaction firing.
217
+ *
218
+ * Used by fork-based memory retrospectives: the wake operates on a
219
+ * freshly-forked conversation that may already be near (or past) the
220
+ * source's auto-threshold, but the goal is to operate on that exact
221
+ * context — running a compaction LLM call before the wake's own first
222
+ * call would waste tokens and defeat prompt-cache reuse.
223
+ */
224
+ suppressAutoCompaction?: boolean;
225
+ /**
226
+ * Skip injection of the hint sandwich entirely. Used when the caller has
227
+ * already persisted the instruction as a real message in the conversation
228
+ * (e.g. fork-based memory retrospectives that append a user message to the
229
+ * forked conversation before waking). When `true`, `hint` is ignored.
230
+ */
231
+ skipHintInjection?: boolean;
232
+ /**
233
+ * Skip injection of the "Conversation Woke" `ui_surface` card into the
234
+ * first assistant tail message and the corresponding live
235
+ * `onWakeProducedOutput` broadcast. Default false (existing behavior).
236
+ * Used by callers whose conversation context already makes it obvious
237
+ * that the agent's output came from a wake (e.g. fork-based memory
238
+ * retrospectives whose conversation title already says "(Retrospective)").
239
+ */
240
+ suppressWakeSurface?: boolean;
199
241
  }
200
242
 
201
243
  /**
@@ -224,13 +266,18 @@ export interface WakeResult {
224
266
  */
225
267
  export interface WakeDeps {
226
268
  /**
227
- * Resolve the wake target for a conversationId.
269
+ * Resolve the wake target for a wake invocation.
228
270
  * Returns `null` if the conversation doesn't exist, `"archived"` if it
229
271
  * exists but is archived, or a `WakeTarget` to proceed with the wake.
272
+ *
273
+ * Receives the full {@link WakeOptions} so the default resolver can
274
+ * thread `trustContext` into `getOrCreateConversation`. Without that
275
+ * threading, the conversation hydrates with `trustContext === undefined`
276
+ * and `loadFromDb` fail-closes to `trustClass: "unknown"`, which filters
277
+ * out every guardian-provenance message — fatal for fork-based memory
278
+ * retrospectives.
230
279
  */
231
- resolveTarget: (
232
- conversationId: string,
233
- ) => Promise<WakeTarget | null | "archived">;
280
+ resolveTarget: (opts: WakeOptions) => Promise<WakeTarget | null | "archived">;
234
281
  /** Timestamp source (for deterministic tests). */
235
282
  now?: () => number;
236
283
  }
@@ -242,8 +289,9 @@ export interface WakeDeps {
242
289
  // `getOrCreateConversation`, and `conversationToWakeTarget`.
243
290
 
244
291
  async function defaultResolveTarget(
245
- conversationId: string,
292
+ opts: WakeOptions,
246
293
  ): Promise<WakeTarget | null | "archived"> {
294
+ const { conversationId } = opts;
247
295
  // Lazy-import daemon modules to avoid pulling heavyweight transitive
248
296
  // deps (conversation store → config/loader → provider catalogs) at
249
297
  // module-evaluation time. Callers that only import agent-wake for
@@ -264,7 +312,15 @@ async function defaultResolveTarget(
264
312
  );
265
313
  return "archived";
266
314
  }
267
- const conversation = await getOrCreateConversation(conversationId);
315
+ // Thread trustContext through to getOrCreateConversation so the
316
+ // hydration path applies setTrustContext + ensureActorScopedHistory
317
+ // (conversation-store.ts:281-289) BEFORE the agent loop's per-turn
318
+ // snapshot reads. Without this, fork-based memory retrospectives see
319
+ // an empty history because loadFromDb ran with trustClass="unknown"
320
+ // and filtered out every guardian-provenance message.
321
+ const conversation = await getOrCreateConversation(conversationId, {
322
+ trustContext: opts.trustContext,
323
+ });
268
324
  return conversationToWakeTarget(conversation);
269
325
  } catch (err) {
270
326
  log.warn(
@@ -376,6 +432,7 @@ function buildWakeTurnContext(
376
432
  */
377
433
  function inspectWakeOutput(
378
434
  baselineLength: number,
435
+ hintMessageCount: number,
379
436
  updatedHistory: Message[],
380
437
  ): {
381
438
  tailMessages: Message[];
@@ -383,10 +440,10 @@ function inspectWakeOutput(
383
440
  toolUseNames: string[];
384
441
  } {
385
442
  // The agent loop appends messages onto the history it was given. We
386
- // injected 3 hint messages (user preamble + assistant hint + user
387
- // postamble), so anything at index >= baselineLength + 3 came from
388
- // the run.
389
- const firstAssistantIndex = baselineLength + WAKE_HINT_MESSAGE_COUNT;
443
+ // injected `hintMessageCount` hint messages (0, 1, or 3 depending on
444
+ // hint mode), so anything at index >= baselineLength + hintMessageCount
445
+ // came from the run.
446
+ const firstAssistantIndex = baselineLength + hintMessageCount;
390
447
  if (updatedHistory.length <= firstAssistantIndex) {
391
448
  return { tailMessages: [], hasVisibleText: false, toolUseNames: [] };
392
449
  }
@@ -436,7 +493,7 @@ export async function wakeAgentForOpportunity(
436
493
  const startedAt = nowFn();
437
494
 
438
495
  return runSingleFlight(conversationId, async () => {
439
- const resolved = await resolveTarget(conversationId);
496
+ const resolved = await resolveTarget(opts);
440
497
  if (resolved === "archived") {
441
498
  log.info(
442
499
  { conversationId, source },
@@ -505,28 +562,45 @@ export async function wakeAgentForOpportunity(
505
562
  // tail-slice math would skip every message.
506
563
  const baselineLength = baseline.length;
507
564
  const wakeTurnContext = buildWakeTurnContext(opts, diskPressureDecision);
508
- const hintContent = `[opportunity:${source}] ${hint}`;
509
- // Sandwich the hint as an assistant message between two hardcoded
510
- // user messages. The assistant role prevents prompt injection — LLMs
511
- // don't follow instructions in their own prior output. The trailing
512
- // user message satisfies providers that reject assistant prefill
513
- // (conversation must end on a user turn). Both user messages are
514
- // static strings with no dynamic content so they cannot carry
515
- // injection payloads.
516
- const wakeMessages: Message[] = [
517
- {
518
- role: "user",
519
- content: [{ type: "text", text: WAKE_PREAMBLE }],
520
- },
521
- {
522
- role: "assistant",
523
- content: [{ type: "text", text: hintContent }],
524
- },
525
- {
526
- role: "user",
527
- content: [{ type: "text", text: WAKE_POSTAMBLE }],
528
- },
529
- ];
565
+ // Build the hint injection. Three modes:
566
+ // - `skipHintInjection`: caller has already persisted an instruction
567
+ // message into the conversation history (typical for fork-based
568
+ // memory retrospectives that append a user message before waking).
569
+ // - `hintRole === "user"`: single user-role message containing the
570
+ // hint directly. Used by trusted internal callers where the hint
571
+ // reads naturally as an instruction.
572
+ // - default (`hintRole === "assistant"`): sandwich the hint as an
573
+ // assistant message between two hardcoded user bookends. The
574
+ // assistant role defangs prompt injection (LLMs don't follow
575
+ // instructions in their own prior output) and the trailing user
576
+ // message satisfies providers that reject assistant prefill.
577
+ const hintRole = opts.hintRole ?? "assistant";
578
+ const wakeMessages: Message[] = opts.skipHintInjection
579
+ ? []
580
+ : hintRole === "user"
581
+ ? [
582
+ {
583
+ role: "user",
584
+ content: [{ type: "text", text: hint }],
585
+ },
586
+ ]
587
+ : [
588
+ {
589
+ role: "user",
590
+ content: [{ type: "text", text: WAKE_PREAMBLE }],
591
+ },
592
+ {
593
+ role: "assistant",
594
+ content: [
595
+ { type: "text", text: `[opportunity:${source}] ${hint}` },
596
+ ],
597
+ },
598
+ {
599
+ role: "user",
600
+ content: [{ type: "text", text: WAKE_POSTAMBLE }],
601
+ },
602
+ ];
603
+ const wakeHintMessageCount = wakeMessages.length;
530
604
  const runInput: Message[] = [...baseline, ...wakeMessages];
531
605
 
532
606
  // Event handling runs in two modes. While `mode === "buffering"`,
@@ -663,26 +737,28 @@ export async function wakeAgentForOpportunity(
663
737
  const goLive = (currentHistory: Message[]): void => {
664
738
  if (mode === "live") return;
665
739
  if (!surfaceInjected) {
666
- const tailStart = baselineLength + WAKE_HINT_MESSAGE_COUNT;
667
- const tail = currentHistory.slice(tailStart);
668
- const firstAssistant = tail.find((m) => m.role === "assistant");
669
- if (firstAssistant && Array.isArray(firstAssistant.content)) {
670
- firstAssistant.content.unshift({
671
- type: "ui_surface",
672
- surfaceId: wakeSurfaceId,
673
- surfaceType: "card",
674
- title: "Conversation Woke",
675
- data: {
740
+ if (!opts.suppressWakeSurface) {
741
+ const tailStart = baselineLength + wakeHintMessageCount;
742
+ const tail = currentHistory.slice(tailStart);
743
+ const firstAssistant = tail.find((m) => m.role === "assistant");
744
+ if (firstAssistant && Array.isArray(firstAssistant.content)) {
745
+ firstAssistant.content.unshift({
746
+ type: "ui_surface",
747
+ surfaceId: wakeSurfaceId,
748
+ surfaceType: "card",
676
749
  title: "Conversation Woke",
677
- body: hint,
678
- metadata: [{ label: "Source", value: source }],
679
- },
680
- display: "inline",
681
- } as never);
750
+ data: {
751
+ title: "Conversation Woke",
752
+ body: hint,
753
+ metadata: [{ label: "Source", value: source }],
754
+ },
755
+ display: "inline",
756
+ } as never);
757
+ }
682
758
  }
683
759
  surfaceInjected = true;
684
760
  }
685
- if (target.onWakeProducedOutput) {
761
+ if (!opts.suppressWakeSurface && target.onWakeProducedOutput) {
686
762
  try {
687
763
  target.onWakeProducedOutput(source, hint, wakeSurfaceId);
688
764
  } catch (err) {
@@ -721,8 +797,7 @@ export async function wakeAgentForOpportunity(
721
797
  const flushPendingTail = async (
722
798
  currentHistory: Message[],
723
799
  ): Promise<void> => {
724
- const start =
725
- baselineLength + WAKE_HINT_MESSAGE_COUNT + persistedTailIndex;
800
+ const start = baselineLength + wakeHintMessageCount + persistedTailIndex;
726
801
  if (start >= currentHistory.length) return;
727
802
  const newMessages = currentHistory.slice(start);
728
803
  for (const msg of newMessages) {
@@ -825,7 +900,11 @@ export async function wakeAgentForOpportunity(
825
900
  tailMessages,
826
901
  hasVisibleText,
827
902
  toolUseNames: names,
828
- } = inspectWakeOutput(baselineLength, updatedHistory);
903
+ } = inspectWakeOutput(
904
+ baselineLength,
905
+ wakeHintMessageCount,
906
+ updatedHistory,
907
+ );
829
908
  toolUseNames = names;
830
909
  producedToolCalls = names.length > 0;
831
910
  const producedOutput = producedToolCalls || hasVisibleText;
@@ -904,9 +983,19 @@ export async function wakeAgentForOpportunity(
904
983
  }
905
984
 
906
985
  const durationMs = nowFn() - startedAt;
986
+ const suppressAutoCompaction = opts.suppressAutoCompaction === true;
987
+ const suppressWakeSurface = opts.suppressWakeSurface === true;
907
988
  if (runError) {
908
989
  log.error(
909
- { conversationId, source, durationMs, err: runError },
990
+ {
991
+ conversationId,
992
+ source,
993
+ durationMs,
994
+ suppressAutoCompaction,
995
+ suppressWakeSurface,
996
+ hintRole,
997
+ err: runError,
998
+ },
910
999
  "agent-wake: agent loop threw; treating as no-op",
911
1000
  );
912
1001
  } else if (tailMessageCount === 0) {
@@ -915,6 +1004,9 @@ export async function wakeAgentForOpportunity(
915
1004
  source,
916
1005
  conversationId,
917
1006
  durationMs,
1007
+ suppressAutoCompaction,
1008
+ suppressWakeSurface,
1009
+ hintRole,
918
1010
  producedToolCalls: false,
919
1011
  toolNamesCalled: [],
920
1012
  },
@@ -926,6 +1018,9 @@ export async function wakeAgentForOpportunity(
926
1018
  source,
927
1019
  conversationId,
928
1020
  durationMs,
1021
+ suppressAutoCompaction,
1022
+ suppressWakeSurface,
1023
+ hintRole,
929
1024
  producedToolCalls,
930
1025
  toolNamesCalled: toolUseNames,
931
1026
  tailMessageCount,
@@ -153,6 +153,7 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
153
153
  { endpoint: "conversations/undo", scopes: ["chat.write"] },
154
154
  { endpoint: "conversations/regenerate", scopes: ["chat.write"] },
155
155
  { endpoint: "conversations/attention", scopes: ["chat.read"] },
156
+ { endpoint: "conversations/slack-channel/resolve", scopes: ["chat.read"] },
156
157
  { endpoint: "conversations/seen", scopes: ["chat.write"] },
157
158
  { endpoint: "conversations/unread", scopes: ["chat.write"] },
158
159
  { endpoint: "conversations/import", scopes: ["chat.write"] },
@@ -451,9 +452,6 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
451
452
  { endpoint: "bookmarks:POST", scopes: ["chat.write"] },
452
453
  { endpoint: "bookmarks/by-message:DELETE", scopes: ["chat.write"] },
453
454
 
454
- // Interfaces
455
- { endpoint: "interfaces", scopes: ["settings.read"] },
456
-
457
455
  // Skills
458
456
  { endpoint: "skills:GET", scopes: ["settings.read"] },
459
457
  { endpoint: "skills:POST", scopes: ["settings.write"] },
@@ -471,6 +469,8 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
471
469
  { endpoint: "memory/v2/list-concept-pages:POST", scopes: ["settings.read"] },
472
470
  { endpoint: "memory/v2/reembed-skills:POST", scopes: ["settings.write"] },
473
471
  { endpoint: "memory/v2/concept-frequency:POST", scopes: ["settings.read"] },
472
+ { endpoint: "memory/v2/ema-scores:POST", scopes: ["settings.read"] },
473
+ { endpoint: "memory/v2/simulate-router:POST", scopes: ["settings.read"] },
474
474
 
475
475
  // Trust rule listing
476
476
  { endpoint: "trust-rules/manage:GET", scopes: ["settings.read"] },
@@ -992,6 +992,10 @@ registerPolicy("conversations/cli/export", {
992
992
  requiredScopes: ["settings.read"],
993
993
  allowedPrincipalTypes: ["local"],
994
994
  });
995
+ registerPolicy("conversations/cli/slack/detach", {
996
+ requiredScopes: ["settings.write"],
997
+ allowedPrincipalTypes: ["local"],
998
+ });
995
999
  // `conversations/cli/clear` wipes every conversation + message + vector
996
1000
  // collection. Elevated to settings.write and locked to local callers,
997
1001
  // mirroring the `conversations/clear-all` and `conversations/wipe` gates.
@@ -24,6 +24,11 @@ import type { TrustContext } from "../daemon/trust-context.js";
24
24
  import { bootstrapConversation } from "../memory/conversation-bootstrap.js";
25
25
  import { addMessage } from "../memory/conversation-crud.js";
26
26
  import type { TitleOrigin } from "../memory/conversation-title-service.js";
27
+ import {
28
+ commitDeferredConversation,
29
+ discardDeferredConversation,
30
+ registerDeferredConversation,
31
+ } from "../notifications/deferred-emit.js";
27
32
  import { emitNotificationSignal } from "../notifications/emit-signal.js";
28
33
  import type { AttentionHints } from "../notifications/signal.js";
29
34
  import { getLogger } from "../util/logger.js";
@@ -121,6 +126,11 @@ export interface RunBackgroundJobOptions {
121
126
  * the `assistant` role and cannot override the action prompt.
122
127
  */
123
128
  assistantSandwich?: { preamble: string; content: string; postamble: string };
129
+ /**
130
+ * Buffer in-band `notifications send` calls and only flush them after the
131
+ * run completes successfully. See `notifications/deferred-emit.ts`.
132
+ */
133
+ deferNotifications?: boolean;
124
134
  }
125
135
 
126
136
  export interface RunBackgroundJobResult {
@@ -205,6 +215,10 @@ export async function runBackgroundJob(
205
215
  ...(opts.scheduleJobId ? { scheduleJobId: opts.scheduleJobId } : {}),
206
216
  });
207
217
 
218
+ if (opts.deferNotifications) {
219
+ registerDeferredConversation(conversation.id);
220
+ }
221
+
208
222
  // Fire the sidebar-creation callback synchronously after bootstrap so
209
223
  // connected clients (macOS sidebar, etc.) see the conversation appear
210
224
  // immediately rather than after `processMessage` returns. Wrapped so a
@@ -273,6 +287,14 @@ export async function runBackgroundJob(
273
287
  });
274
288
 
275
289
  await Promise.race([work, timeout]);
290
+ // Symmetric with the `work.catch` above: once `work` has won the race,
291
+ // the orphan timeout promise can still reject during the await below
292
+ // (commitDeferredConversation). Swallow so it doesn't surface as an
293
+ // unhandled rejection that Bun can use to terminate the process.
294
+ timeout.catch(() => {});
295
+ if (opts.deferNotifications) {
296
+ await commitDeferredConversation(conversation.id);
297
+ }
276
298
  return { conversationId: conversation.id, ok: true };
277
299
  } catch (err) {
278
300
  const errorKind = classifyError(err);
@@ -281,6 +303,10 @@ export async function runBackgroundJob(
281
303
  // so the structured failure result still flows to the caller.
282
304
  const conversationId = conversation?.id ?? "";
283
305
 
306
+ if (opts.deferNotifications && conversationId) {
307
+ discardDeferredConversation(conversationId);
308
+ }
309
+
284
310
  log.error(
285
311
  {
286
312
  err: error.message,